responsiveness of main game loop designs

Started by
38 comments, last by Norman Barrows 7 years, 10 months ago

for maximum responsiveness, it seems you must render after every update, and before each input.

can anyone think of an example that doesn't do this without being less responsive?

you have to render before each input, so the user knows whats going on, so they can respond to the situation.

you have to get input before or during each update, to update the player entity.

and you have to render after each update to display the results of user input (and update in general) in as timely a manner as possible.

so for maximum responsiveness, i don't see how this could be avoided.

can anybody think of anything that would/could be different, but not less responsive?

i can't think of anything that would not be less responsive.

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

Advertisement

You're overthinking this.

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

>> You're overthinking this.

<g> probably a good thing. i'm one of those people who likes to boil things down to their essence. sometimes you discover interesting underlying truths.
like it seems you can't monkey around with the basic render-input-update cycle without negatively impacting responsiveness. never even occurred to me, but it appears to be true.
and for a game that doesn't require dx10 or 11 or 12 (pick any one), higher versions of Dx are just a possible optimization method, not a requirement (in retrospect this is pretty obvious).
and for a game where a single thread can do it all, multi-threading is likewise just a possible optimization method, not a requirement (in retrospect this is pretty obvious too).
and it would seem you should always be developing (testing actually) on the target PC, which will likely be an "above average" game capable PC (depending on game sys reqts and dev time), and always maintain your target FPS at all times on the target machine - IE optimize where needed, when needed, and as needed as you go along. which means nothing is considered to be "done" until its fast enough for release. this avoids 11th hour optimizations.

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

Let me analogize this: You're suddenly asking if it's possible to make a car more efficient by removing the wheels.

You can play around with the loop, but there's no reason to. It's already possible to surpass human limitations for responsiveness. If you're handling input sanely and your frame rate is high enough that the player can see what's happening then the only really good way to screw up responsiveness is to render several frames behind or possibly to just have a crappy control layout. Control issues are usually related to poor game design or implementation issues with things like colliders not matching models or sprites. Networking tricks can also cause trouble if they're implemented incorrectly, but these are all cases where the output is not correctly representing the game state rather than cases where the rendering or input is not being handled fast enough.

At 60 FPS the game is presenting you information every 16ms or so.

Measure your reaction time here:

http://www.humanbenchmark.com/tests/reactiontime

Even if you float a frame (so that the CPU and GPU can work better in tandem) you only lose a little temporal fidelity. Stretching beyond one frame becomes a timing issue, but 0.016 seconds is barely even detectable, especially for fast moving objects.

Moreover, you're talking about a 3-stage process that loops. Your options are limited here:

A input

B update

C render

Naively:

ABC

ACB

BAC

BCA

CAB

CBA

But then when you consider that they're looping:

ABC == BCA == CAB

ACB == CBA == BAC

You've only got two options. Treating rendering as the divider you have input-update or update-input.

If you're polling then default to polling before updating because it makes sense to respond to input when you have it. If you're on an architecture that will delay on polling then you may consider moving it to after updating depending on how much time your GPU is taking compared to how much time the CPU takes.

On the other hand, if your input is handled by something similar to messaging then you don't even have to mess with it as updating includes processing queued inputs.

Regardless, even if you poll in the 'wrong' order you shouldn't be able to detect the difference without instrumentation.

tl;dr - If you want your game to feel responsive focus on precise collision and well designed character reactions instead.

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

While I agree that you are overthinking this, there is also a gross simplification you are making. Even if you follow your intention of limiting things to input then immediately render, that won't make the game feel responsive. The key word is 'feel', it's not about absolute numbers here, it is about perception. Even the most twitchy of games is not able to apply input immediately, it is often several frames later. The difference between feeling responsive and the latency issues some games have is in application. Basically the trick is *not* to act immediately but to buffer just enough such that all input is equally latent. The human mind is a masterful computation device, without knowing it, the player will adapt to slight amounts of latency. If that latency is bouncing all over the place, the player can not adapt, if it is consistent on the other hand, the player will generally not notice.

If things were about the absolute latency, games would never have been able to survive triple buffering, remote streaming etc. Those things seem to work (ok, streaming not so well yet) and the reason is generally that folks put the time to smooth the latency, not try and remove it completely.

My monitor can only display images at 60Hz, but I can run my sim loop at 600Hz. That means I can render once for every 10 updates, and the user is still getting an uber-responsive experience.

If I poll input once per 10 updates, the user probably won't know - it will still feel good... but if I poll input for every update, it will reduce the input latency by approx 0ms to 16ms, making it feel very slightly more responsive.

You actually can poll for input multiple times per frame, or even in a background thread.

However, (a) this then requires that your player physics be directly driven by input so that polling multiple time actually has some effect, and (b) doing all that is ****ing pointless.

Your goal should be to maximize the update rate. That is, just make your game faster. MAke sure you can actually hit 60fps on commodity hardware. Try to hit 144hz on high-end gaming rigs with monitors that can refresh that fast. Make the game run that fast even if the monitor doesn't and let triple buffering sort out the difference.

Just be fast. Don't code for responsiveness. Code for raw speed. Design your main loop to maximize the hardware resources: parallelize CPU and GPU work, use a good job system, and _add_ "lag" in your system where it improves throughput.

A game running at 144hz with a 2 frame render lag (~7 ms per frame, or 14ms latency with lag) is still more responsive than a 60hz game (~16ms latency).

Sean Middleditch – Game Systems Engineer – Join my team!

>>>> can anyone think of an example that doesn't do this without being less responsive?

>> A game running at 144hz with a 2 frame render lag (~7 ms per frame, or 14ms latency with lag) is still more responsive than a 60hz game (~16ms latency).

that sounds like one right there to me.

thanks all!

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

In your other thread you brought up destiny and their simultaneous update/drawing routines and how this will increase latency. This isn't necessarily true and becomes obvious once we draw some timelines.

Let's say we've got three CPU tasks in a frame, with these costs, which must occur in this serial order:
Poll Inputs: 1ms
Update Simulation: 15ms
Draw Scene: 17ms


Single threaded:


        0ms      1ms        16ms     33ms     34ms       49ms     66ms
Core1:  | Poll#1 | Update#1 | Draw#1 | Poll#2 | Update#2 | Draw#2 |

Dual threaded, no buffering / pipelining:


        0ms      1ms        16ms     33ms     34ms       49ms     66ms
Core1:  | Poll#1 | Update#1 | Idle   | Poll#2 | Update#2 | Idle   |
Core2:  | Idle              | Draw#1 | Idle              | Draw#2 |

Dual threaded, pipelined:


        0ms      1ms        16ms     17ms       32ms   33ms
Core1:  | Poll#1 | Update#1 | Poll#2 | Update#2 | Idle | Poll#3...
        0ms                 16ms                       33ms       50ms
Core2:  | Idle              | Draw#1                   | Draw#2   |

Single-threaded, this takes 66ms for two frames.
Putting draw and update onto their own threads, but enforcing mutual exclusion still takes 66ms for two frames.
Running Update and Draw in parallel, by pipelining two frames together, reduces the critical path to 50ms for two frames if you include the pipeline warmup time. Once the pipeline is warmed up, it's actually just 34ms per two frames from that point onwards!

There's still no extra latency for the user -- the path from input polling to finished drawing is 33ms in all three situations.

The only difference is that first two options pump frames at ~30Hz, while the third option pumps frames at ~59Hz - so it's increased framerate while leaving input latency exactly the same as it was before.

Now, extending this. Let's say that your Draw cost is still 17ms, but your Update cost is only 2ms. If we pipeline this as above, then the critical path goes through the Draw tasks, so the best framerate we can achieve is still ~59Hz as above.

So, seeing that our user is only able to perceive the game world at 59Hz... you could argue that polling any faster than this is not required. We can then use the exact same timeline as above, but replace the |Update#N| block with |Update[#N - #N+8)| , and replace Draw#1/Draw#2 with Draw#8/Draw#16 - i.e. we do 8 updates per poll/draw.

If you accept the argument that there's no need to poll faster than the drawing rate, then this will be just as responsive as before, even though we're "dropping" seven out of eight frames :)

>> If you accept the argument that there's no need to poll faster than the drawing rate

assuming you update no faster than you poll or render.

just cause you can only draw the screen once every 8 games turns doesn't mean you should let the computer move every turn and only let the user move every 8 game turns.

you have to preserve the fair play of "its my turn, ok, now its your turn, ok now its my turn again."

"i cant draw fast enough to show you whats going on - so you don't get a turn until i can" - that's just silly.

yet from what you say, it seems folks actually do this. no wonder games are such piles of crap these days.

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

This topic is closed to new replies.

Advertisement