Search code examples
game-enginegame-loopcrystal-lang

Confusion with writing a game loop


I'm working on a 2D video game framework, and I've never written a game loop before. Most frameworks I've ever looked in to seem to implement both a draw and update methods.

For my project I implemented a loop that calls these 2 methods. I noticed with other frameworks, these methods don't always get called alternating. Some frameworks will have update run way more than draw does. Also, most of these types of frameworks will run at 60FPS. I figure I'll need some sort of sleep in here.

My question is, what is the best method for implementing this type of loop? Do I call draw then update, or vice versa? In my case, I'm writing a wrapper around SDL2, so maybe that library requires something to be setup in a certain way?

Here's some "pseudo" code I'm thinking of for the implementation.

loop do
  clear_screen
  draw
  update
  sleep(16.milliseconds)
  break if window_is_closed
end

Though my project is being written in Crystal-Lang, I'm more looking for a general concept that could be applied to any language.


Solution

  • It depends what you want to achieve. Some games prefer the game logic to run more frequently than the frame rate (I believe Source games do this), for some games you may want the game logic to run less frequently (the only example of this I can think of is the servers of some multiplayer games, quite famously Overwatch).

    It's important to consider as well that this is a question of resolution, not speed. A game with logic rate 120 and frame rate 60 is not necessarily running at x2 speed, any time critical operations within the game logic should be done relative to the clock*, not the tic rate, or your game will literally go into slow motion if the frames take too long to render.

    I would recommend writing a loop like this:

    loop do
        time_until_update = (update_interval + time_of_last_update) - current_time
        time_until_draw = (draw_interval + time_of_last_draw) - current_time
        work_done = false
    
        # Update the game if it's been enough time
        if time_until_update <= 0
            update
            time_of_last_update = current_time
            work_done = true
        end
    
        # Draw the screen if it's been enough time
        if time_until_draw <= 0
            clear_screen
            draw
            time_of_last_draw = current_time
            work_done = true
        end
    
        # Nothing to do, sleep for the smallest period
        if work_done == false
            smaller = time_until_update
    
            if time_until_draw < smaller
                smaller = time_until_draw
            end
    
            sleep_for(smaller)
        end
    
        # Leave, maybe
        break if window_is_closed
    end
    

    You don't want to wait for 16ms every frame otherwise you might end up over-waiting if the frame takes a non-trivial amount of time to complete. The work_done variable is so that we know whether or not the intervals we calculated at the start of the loop are still valid, we may have done 5ms of work, which would throw our sleeping completely off so in that scenario we go back around and calculate fresh values.

    * You may want to abstractify the clock, using the clock directly can have some weird effects, for example if you save the game and you save the last time you used a magical power as a clock time, it will instantly come off cooldown when you load the save, as that is now minutes, hours or even days in the past. Similar issues exist with the process being suspended by the operating system.