All chapters · Chapter 3/9

Make it move

Starting

Now the paddle crosses the screen on its own and reappears on the left. This is what does it:

useUpdate((dt) => {
    paddle.transform.x += PADDLE_SPEED * dt;
    if (paddle.transform.x > WIDTH) paddle.transform.x = -PADDLE_W;
});

useUpdate is the game loop. You hand it a function and the engine calls it once per frame, forever. Everything in your game that moves, decides or reacts will end up coming out of here.

Look at where that call sits: inside the scene body, which (as chapter 1 said) runs exactly once. You aren't moving the paddle there. You're telling the engine that from now on, it should.

dt: the number that changes everything

dt is the seconds elapsed since the previous frame. A small number, around 0.016 on a 60 Hz monitor.

And out of it comes the only rule of this chapter: always multiply by dt.

Look at the difference:

paddle.transform.x += 5;              // 5 pixels PER FRAME
paddle.transform.x += 340 * dt;       // 340 pixels PER SECOND

The first line looks simpler and is a time bomb. On a 60 Hz monitor it gives 300 pixels a second. On a 144 Hz one, 720. Your game runs at double speed on someone else's machine, not because their computer is faster, but because their screen refreshes more often.

The second line says what you mean: 340 pixels per second, on any screen, whatever happens. If a frame takes twice as long, dt is twice as big, and the motion comes out the same.

Why does multiplying fix that?

Speed times time is distance. The same formula as "I drove at 60 mph for 2 hours, so I covered 120 miles".

It's identical here, just with small numbers: you're going 340 pixels per second and 0.016 seconds have passed, so you should move 340 × 0.016 = 5.44 pixels.

If the next frame takes twice as long (0.032 seconds), you get 10.88 pixels. More distance because more time passed, which is exactly what makes the motion look equally fast on both screens.

Mutating directly is fine

paddle.transform.x += PADDLE_SPEED * dt;

No copying the object, nothing returned. Inside useUpdate, changing the position outright is both normal and fast: transform is an object the engine reads every frame, so writing to it is all it takes.

Your turn

Push PADDLE_SPEED to 1200. Drop it to 40.

And try the thing from the top: swap PADDLE_SPEED * dt for PADDLE_SPEED / 60 and see whether you notice. On your screen you probably won't, and that's precisely the problem. The bug never shows up on the machine where the game is written. It shows up on the machine where it's played.