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)