Doing side quests
This will be long!
A few weeks ago I started working a bit on the next version of the "engine" I have been using for making A Ruthless World (ARW). I got it rolling by branching the engine in git and starting a new project by copying ARW and minimizing it down to a barebones project that goes through boot->menus->level01->level02.
Interestingly when going over and minimizing what was included I found all sorts of small bits and pieces to fix/improve and now have a good list of things I can later move over to the old engine as well, improving the upcoming release of ARW.
Getting stuck in a time loop
A main area of interest was reviewing and trying to improve the game update loop. This is the loop that runs with physics, game logic and render updates.
The current version runs using a requestAnimationFrame, which is the standard approach to running a loop that needs to sync nicely with the browser drawing. There are a few issues however with this when doing a game, in particular if using physics.
requestAnimationFrame will always try to run at the refresh rate of the screen, meaning it can vary from the historically standard rate of 60Hz, to the more modern rates of everything from 90Hz, to 240Hz and above!
For the regular game logic loop this is all good, the faster the refresh rate the smoother and more responsive the game will become. To deal with the difference in update rates you solve it by using a delta value. Where everything happening over time is multiplied with the delta to make the end result the same, regardless of refresh rate. Example: If 60Hz is a delta of 1, then 120Hz will result in a delta of 0.5. As the update will not run exactly at the refresh rate, this delta varies a little bit for each update.
The delta gives a predictable result over time as at a high fresh rate, each update will do less, and at a lower refresh rate each update will do more, making the change happening during a set amount of time be the same.
For physics however this does not work so well. While the overall result will be somewhat the same, physics behave differently based on how often they are updated, regardless of the use of a delta. To solve this you create a fixed time loop, that runs at an as precise rate as possible. This is where using the requestAnimationFrame can be a bit problematic when doing physics and syncing physics positions/rotations to visual graphics position and rotation.
Physics considerations
If the physics update at 60Hz, then all will be good if the screen refresh rate is 60Hz as well. Physics update, game logic updates position of graphics based on the physics data, render draws the graphics when the screen refreshes. All in sync and rendered smoothly.
But already here there will be a small issue. As the physics will have a loop that tries to run at exactly 60Hz, but actual timing of each update will vary slightly, every now and then the physics will either wait one update or do two updates in a game update to keep that fixed update timing. This can cause some stuttering in the render as physics and game logic updates are no longer exactly in sync. This can be fixed, more on that later.
A second issue will be that if the screen is running at a higher refresh rate, say 120Hz, then the game logic update and render will update 120 times a second, but the physics using the fixed rate, will only update 60 times a second. This results in no visual difference for everything using physics, as it will simply be drawn in the same way twice!
You can't simply fix this by also running physics at 120Hz if the screen is capable of 120Hz, because then the physics will behave differently and you can not predict what will happen for those users. Hence you have to pick a fixed physics update rate and stick with it.
Old solution and initial improvements
My old solution was to run the physics at 120Hz, this because the games I make are not that resource hungry and as long as I can run the game at 60Hz on my almost 10 year old iPad I'm within the window of what I want to support.
The old solution was:
- Game update that runs with a requestAnimationFrame.
- Physics updates first at 120Hz.
- Game logic updates at screen refresh rate.
- Game renders at screen refresh rate.
Main issue
If the screen runs at 60Hz, physics will almost always update twice, directly after each other each game update. Sometimes only once, sometimes three times. This creates a few issues as the timing of the physics update can vary, sometimes two updates happen as fast as the device is capable of, sometimes it waits for a whole screen refresh.
I smoothed this by faking the timing and smoothing out timing values of six frames. This created a quite good result, physics are more stable at 120Hz and there was very little stutter. But there was some stutter...
If the screen runs at 120Hz, or higher, things improve. The physics gets to run with a more even rate and the fake timing and smoothing done will become more "real" automatically from this. Even so there could be some stutters. In all these cases I am talking about stutter as in something moving a bit erratic with 1-2 pixels during a frame or two. It's hardly noticeable but gives a clue that something should be fixed.
First improvement
I added interpolation to the graphics positioning when reading the physics data. This takes the last drawn physics data, the latest known physics data and draws at a value in between the two. Smoothing out motions such as positional movement and rotation. Downside here is that you get lag, so you want to run physics as fast as possible, otherwise you have a character that runs into and inside a wall, then bumps back out as drawn positions become more in sync with the physics data.
This solution works like magic when the physics are slower than the logic and render updates. However, if the physics are faster, then the result is an improvement, but there remains some stutter, even if it is "smoothed". I tried to find improvements, or if something was wrong with the solution, even got an AI involved to review and give suggestions, but nothing gave a better result than what I already had. This led to taking a whole new approach to how to design the game update loop.
New solution
I did many different solutions, even going as far as to split physics updates, game logic updates and render updates into three completely separate update loops and while this felt very cool and advanced, I could not get an improved end result. The small stutter remained. With all the knowledge, fresh testing and a mind that had all the relevant code up-to-date, I took a breath and decided on a final half-cool idea that might work.
Joining physics and logic, separating render.
I decided to create a new update logic for the physics. So that the physics have a precise fixed timing loop and that the game logic also runs in this loop. This will result in the physics and the game logic always being in sync, and the render runs with the requestAnimationFrame and will always render as fast and in sync as the screen refresh rate allows.
And that was about it. No, of course there are issues here. Number one being that what if the screen refresh rate is faster than 120Hz? Then those users will not benefit from the faster response and smoother drawing; it will draw ALL graphics in the same place twice, not only those synced with physics. Solutions ahead!
Web worker
A web worker is a script that runs in a thread separated from the main thread. Meaning if the main thread that runs the whole app is busy, say with rendering, the web worker script will continue running and not get stuck waiting on the render. Same the other way around, if you do something computationally heavy, run it in a web worker and it will not hold up the render.
By using a web worker I created a simple ticker, a loop that runs and simply sends a message at a fixed interval. I used this to run my 120Hz physics loop, making it possible to run a loop that is faster than the requestAnimationFrame on any device regardless of their screen refresh rate.
Then extended it with functionality so that the game logic can add its update to this loop as well, running after the physics update.
Then finally the render runs its update in a requestAnimationFrame and this, it turns out, seems to always run automagically in sync with the rest of the updates. I was expecting to have to do some checks and adjustment to make sure the render always runs when wanted, but so far in all combinations of Hz and devices I have tested it runs exactly as you want it.
But what about those poor 240Hz screen users?
Glad you asked. I updated the old requestAnimationFrame update for the game logic and render, so that it can be used if wanted. The game will then run the physics update with its web worker and the logic update synced with the screen refresh rate and render.
By no longer running the physics in the same requestAnimationFrame update, the previous stutter issue is gone. Mainly because the physics are no longer doing the big jumps with sometimes doing two updates as fast as possible, sometimes only one, sometimes three. Now it stays as close to a real 120Hz rate as possible.
The final touch
The engine runs a 6 updates settle period at the start, where it does not run any active game worlds, this to avoid the quite large variations in frame time that occurs in these initial updates.
I take this opportunity to check the Hz of the game logic update versus the render update and if it turns out that the render update is running at a higher Hz, the game autosets to use the requestAnimationFrame update and enables interpolation. This makes the game logic and render use the full potential of the screen for the best response and smooth render, while the physics keep going at a steady 120Hz, with the addition of smoothing all graphics synced with physics using interpolation.
Of course these settings are available for the user to change if they experience any issues with the auto-configured settings. And yes, this will be added to ARW as well.
Phew! Back to ARW... Wishlist on Steam
Spelmakare is game development using web technologies.