jake

Game loop advice

Recommended Posts

I've been working on a base repo for pixel art games. The issue I've been running into is stutter when rendering sprites. Specifically I've seen this run buttery smooth on my laptop with a refresh rate of 144hz, but on 60hz monitors it seems to stutter. The game loop looks sensible to me, but I'm not sure what else to try at this point. Any help would be much appreciated.

You can find the branch I'm working on here. The specific file of interest is game.ts. You can also see a live demo of it running on netlify.

 

Share this post


Link to post
Share on other sites

i'm slightly confused as to why you're dividing velocity by time? the result measures as acceleration (unit/time square), you need a distance, which means you should be multiplying velocity with step.

bunny.pos.x += (bunny.vel.x / STEP) * bunny.dir.x;
bunny.pos.y += (bunny.vel.y / STEP) * bunny.dir.y;

 

Share this post


Link to post
Share on other sites

Good point, I corrected this to be:

bunny.pos.x += bunny.vel.x * (STEP / 1000) * bunny.dir.x;
bunny.pos.y += bunny.vel.y * (STEP / 1000) * bunny.dir.y;

Didn't stop the stuttering.

Edited by jake

Share this post


Link to post
Share on other sites

const x = ... seems dodgy to me.
You completely ignore the logic you use in the while loop (e,g, bunny.vel.x * (STEP / 1000) * bunny.dir.x;), and boundary checks...

And what happens when 'dt' comes back as faster than 'STEP', e.g 15 or 16. The entire 'while' is skipped and everything goes haywire. No logic is used whatsoever, lastPos is never set, etc...

You should just use 'dt' instead of STEP (and fractions of it).

Share this post


Link to post
Share on other sites

With a brief look, I don't see anything wrong with the implementation. I would say it all comes down to `ctx.imageSmoothingEnabled = false`, which will cause the pixel positions to be rounded (which is desired for pixel-perfect pixelart game).
At 60 FPS you sometimes get a pixel movement, sometimes don't, and not moving is essentially a frame skip, which at 60 FPS is visible. At 144 FPS, not so much (i.e. the timing is much smaller, so it's closer to reality).
You can test that by playing around with speed setting, making it work well in 1/60 intervals. Although looking at your code, speed of 20 should work well (1 pixel every 3 frames). Try logging rounded position delta every frame and see if there's jitter or not. Repeating 0-0-1 should look good. Something like 0-0-1-0-1-0-0-2 will not.

@Milton No, it's correct. It's simply an implementation of fixed timestep with rendering interpolation. When `dt` comes back smaller than `STEP`, rendering interpolation still makes the movement necessary. Thus allowing for a game with low logic rate (e.g. heavy physics running at 30 Hz) still render nicely on modern desktop with whatever refresh rate the display/gpu manages (~144 Hz, but anything really).
Using `dt` directly would make anything with higher differential order non-deterministic and unstable (e.g. applying acceleration).

Share this post


Link to post
Share on other sites
6 minutes ago, Antriel said:

.@Milton No, it's correct. It's simply an implementation of fixed timestep with rendering interpolation. When `dt` comes back smaller than `STEP`, rendering interpolation still makes the movement necessary. Thus allowing for a game with low logic rate (e.g. heavy physics running at 30 Hz) still render nicely on modern desktop with whatever refresh rate the display/gpu manages (~144 Hz, but anything really).
Using `dt` directly would make anything with higher differential order non-deterministic and unstable (e.g. applying acceleration).

I beg to differ :)

The movement is necessary, but using correct logic.
I'm a big fan though Antriel :)

Share this post


Link to post
Share on other sites

@Milton I actually went with the idea of choosing velocities that respect the minimum FPS value I want to target, in this case 60. The general rule I set for myself is to not allow velocities that result in less than 1 pixel of movement per 2 frames max. For example: velocity of 30 would be the min allowed speed resulting in 0.5 pixels per frame, or 1 pixel per 2 frames, basically 30 FPS. This has resulted in smooth(er) movement. Interpolation never really made a ton of sense in a pixel perfect loop in my opinion - and is a remnant of a brick breaker game I made using canvas primitives (not bitmaps) where it made sense  - so I've rounded positions to whole values during rendering.

The loop itself is fine, it's similar to Monogame where the update logic will try to catch up if dt is too far behind. Which can result in spiral of death but that's not a concern in my case. 

A live demo is here

@Antriel thanks for the breakdown. I think the conclusion I came to with velocity selection makes sense based on what you suggested.

Edited by jake

Share this post


Link to post
Share on other sites

This article is nice: http://buildnewgames.com/real-time-multiplayer/

Quote

Frame rate independence

When a block travels across the screen, it can be a simple line of code. block.position.x += 1; This 1 here, what unit is that measured in? Actually, this is one pixel - but it is moving 1 pixel per frame. Each time the loop runs, we move one pixel. That is - at 30 frames per second, it moves 30 pixels. At 60 frames per second, it moves 60 pixels. This is really bad for games, because hardware and performance can vary from one device or computer to another. Even between different browsers, this problem can be a huge difference. One player reaches the wall, the other barely moves at all.

In order to ensure that the block moves the same distance over the same amount of time, the concept of delta time is used. This value is the milliseconds per frame (mspf), which is measured while updating your game. It is how long one update takes. By taking the time at the start of your loop, and the time at the end of the loop, you can work out how long the update has taken.

At 30 frames per second (1/30) the delta is about 0.033s. One frame, every 33.3 ms. At 60 frames per second (1/60) the delta is about 0.016 or 16.66 ms per frame. So lets say that the ball is moving at 1 pixel per frame. To solve the problem of frame rate dependence, we multiply any change in position by the mspf value, balancing out the time, making the distance always the same.
 

deltatime.png.576638ee8748300502d3c91fb0310a23.png

Our example calculation becomes ball.position.x += (1 * deltatime);. With bigger delta (slower frame rate) the ball moves more pixels - reaching the destination at the same time as at a smaller delta (higher frame rate). This gives us concrete units that will act the same at any render speed. This is critical for animations, movements and pretty much any value that changes over time: they all should be scaled to the mspf.

 

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...

  • Recently Browsing   0 members

    No registered users viewing this page.