3
\$\begingroup\$

I am building a game engine and considering the design for the core game loop that will drive the game.

In the most basic form, the implementation looks similar to this (in pseudo-code):

while (game.running) 
{
    // Calc time delta from last update
    var timeDelta = Time.now - lastUpdate

    engine.components.update(timeDelta)

    engine.drawAll()
}

This loop updates and draws in a 1:1 ratio (for every update there's a draw).

Should there be any compensation for the engine when it runs slower than its target update rate?

I've seen frameworks such as XNA for example, with the IsRunningSlowly property: http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.gametime.isrunningslowly.aspx

Is it preferred to keep a 1:1 update/draw ratio? or should i compensate for slower updates?

\$\endgroup\$
  • \$\begingroup\$ I would personally say, it's extremely NON-preferred to keep "update" ticks in sync with display logic. Particularly if you think any of your users might have a multi-core CPU or dedicated GPU, e.g. you're targeting most common devices (PC's, iOS, Android, modern consoles) \$\endgroup\$ – BRPocock Dec 30 '13 at 15:37
  • \$\begingroup\$ Why? care to elaborate? either here or as an answer? \$\endgroup\$ – spaceOwl Dec 30 '13 at 20:11
  • \$\begingroup\$ Actually, the article that @Sean Middleditch linked below gives a really decent example of how and covers "why" in some detail. But, essentially: when your video drawing takes more or less time to occur (drawing frame rate will often vary greatly by machine, load, scene complexity...) you don't want things like physics to respond differently; consider suddenly encountering a more complex graphics scene, or players with more or less sophisticated GPU's in a multi-player game, causing objects to respond differently... \$\endgroup\$ – BRPocock Dec 30 '13 at 23:52
7
\$\begingroup\$

First, Fix Your Timestep. The component update should always have a fixed time interval. This is critical for stable physics and can avoid bugs in other systems as well.

You may have cases where the time interval becomes huge, too. This can happen if you set a breakpoint while debugging. You'll want to cap the update time used for the time accumulator (or the raw update value if you don't follow the linked article's advice) to avoid really broken behavior when debugging.

Additionally, you can interpolate between the current frame's data and the last frame so the motion stays smooth and buttery. Simply keep two copies of game object animation data (position, skeletal animation state, etc.) and then interpolate between them. Thus your game will mostly look like it's running at a full 60+ FPS even if updates are running less often.

\$\endgroup\$
1
\$\begingroup\$

I've +1'd @Sean Midleditch 's answer, but as some considerations:

(I'm not really happy with how this is kind of a disorderly rant, but...)

  • Video hardware varies wildly between hosts, unless you're on a Gaming Console (3DS, Wii U, PS4, Xbox1). Even "the same device" will have different GPU capabilities between generations (e.g. "iPad"), or may have different screen resolutions connected (e.g. SDTV, HDTV)

  • All major platforms (the Big Four consoles, iOS, Android, and PC/Lin/Mac/Win) commonly have at least dual-core CPU's, and often more (many home PC's have 4, 6, or 8 cores). Having a single driver function push physics and graphics means you're binding most of your game's functionality to one core.

  • The rate at which your simulation occurs shouldn't be limited to – nor impose a limit upon – your graphics systems.

EG: the sudden spike in scene complexity when walking out a doorway into an open space might drive the graphics framerate from 65fps to 50fps suddenly. If your physics continues to run smoothly, though, the human eye will compensate for it.

Now, unless you're using a hardcore ECS model with immutable entities, you're probably going to encounter the situation of mutating (i.e. writing/setting) state onto a shared "object" of some kind that is read/written by the simulation and read by the graphics subsystem.

The difficulty is maintaining sync points between them.

One simple way is to maintain a mutex which the simulation system must acquire before writing to the shared state. When the simulation wants to mutate an object that the graphics engine might want to access, it has to acquire the mutex in read-write mode; when the graphics system is in its "critical path" and needs to grab data on a great many objects very quickly, it acquires a "read-only, but locked for writing" condition on the entire affected area.

If you're simply running two threads in parallel on different cores, now you have a time during which your simulation might "stall," but you can hopefully still show improvement over using a single core and doing (loop (simulate) (draw))

(You might have one mutex per "room" or other subdivision of the game world, for example.)

  • Warning: I am a fierce proponent of "Pure" ECS systems and immutable objects. Other people will tell you that the below is over-the-top or silly, and they might be right.

The "hardcore" way I referred-to above is something that OOP generally is poorly-equipped to handle, so it's not often used in C++/Java/C#, but is used sometimes in Haskell/ML/Erlang/Lisp type systems. The essential idea is that an object is immutable; in Java terms, it has "only final fields". When the simulation wants to "mutate" the object, it instead must create a new object with the mutated value(s), and then cause it to replace the "old one."

Think of it in terms of read-only files in a read-write directory:

 echo "(:SIZE 92 :X 100 :Y 100 :Z 100)" > ~/world/thing1

 echo "(:SIZE 92 :X 102.2 :Y 100 :Z 100)" > ~/tmp/.#thing1.new

 echo "(:SIZE 92 :X 102.2 :Y 104.3 :Z 100)" > ~/tmp/.#thing1.new

 mv -f ~/tmp/.#thing1.new ~/world/thing1 

The new instance replaces the old instance, requiring memory policing or garbage collection on every mutation.

If you were instead sharing a mutable object without mutex protection of some kind, then it's possible that you might make multiple mutations and the graphics system could display an inconsistent state. EG: imagine the "Draw" of thing1 occurring after the second echo mutation above, when X had been updated, but not yet Y.

(Furthermore, you can make the memory objects recycleable, by allowing them to be re-written and re-used, just never doing so while they're linked to the world database, to avoid constructing new objects inside your simulation loop, since memory allocation and garbage collection are relatively slow operations. That's an optimization that you might wish to keep in mind – e.g. having a common place to construct and replace the objects, so you can more easily implement some kind of recycling pool later if you need it – but probably not the kind of thing you need to worry about until/unless you profile your simulation system and discover that you're spending too much time in construction or garbage collection.)

Why go to such oddball lengths? In theory, you don't need to have only one simulation system! On a multicore system, you can break out various systems to run in their own threads or processes, each potentially running at a different rate.

With the use of the seemingly-complex immutable-object model, each of them can "not really mutate, but effectively mutate" the game state at approximately the same time.

Using other models, you might need to have mutexes protecting access to some parts of your world at any given time, and handle the interaction between systems with a little more care.

For example: Consider having

  • one thread for reading player controls (keyboard, gamepad, mouse, touchscreen, whatever);
  • another for playing sounds;
  • another for graphics;
  • another for network I/O to share the game state;
  • another for performing local physics simulations;
  • another for processing "special triggers" in the UI, e.g. detecting when the player's health is beneath 10% to start flashing their health gauge;
  • another for managing "scripted events" in the game world;
  • another for running AI character logic

Each of these might have a different "best rate," and you might wish for some of them to run at a higher or lower priority than others. If your AI pathfinding solution takes 230ms instead of 200ms, it's less likely to be noticed than if your physics simulation drops from 24fps to 20fps. As long as sound, graphics, physics, and player input are consistently smooth, you can get away with less frequent updates occurring in some areas.

Yes, thread context switches have a cost – and there are ways to do the same kind of thing without threads, e.g. having a "time-shared" slot in your update loop that calls different subsystems on different "frame numbers" (do AI on every frame 1, 3, 5, and 7, modulo 8, input polling on frame 0, scripted events on frame 2, sound changes on frame 4, and triggers on frame 6, while running physics on every frame)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.