Back

Activity 2.3: Movement and Delta Time

divider

The Idea

Activity 2.2 ended on the fact that makes movement possible: the screen is wiped and redrawn every frame. So you never move anything. You store a position, change the number, and draw at wherever it now is.

Which gives a clean division of labor, and it is worth holding to strictly:

  • love.update changes numbers. It draws nothing.
  • love.draw reads numbers. It changes nothing.

Break that and you get bugs that only appear at certain frame rates, which are among the least pleasant kinds to find.

What dt is for

dt is delta time — how many seconds passed since the last frame. At 60 frames a second it is about 0.0167.

main.lua
-- wrong: speed depends on the computer
ship.x = ship.x + 5
-- right: 300 pixels per second, on any computer
ship.x = ship.x + 300 * dt

Adding a fixed amount ties your game's speed to the computer it runs on. On a 144 Hz monitor the first line moves the ship almost two and a half times faster than on a 60 Hz one. The player did nothing differently; the game is just a different game.

Multiplying by dt fixes it, and the arithmetic is worth doing once by hand. At 60 frames a second you add 300 * 0.0167 = about 5 pixels, sixty times — 300 pixels. At 30 frames a second you add about 10 pixels, thirty times — 300 pixels again.

So the number becomes a unit you can reason about. speed = 300 means 300 pixels per second, which crosses an 800-pixel window in under three seconds. That is a design decision you can make on paper.

The rule: any number you add every frame gets multiplied by dt. Movement, rotation, fuel drain, cooldowns. Anything you set once does not.

Reading the keyboard

main.lua
if love.keyboard.isDown("left", "a") then
ship.x = ship.x - ship.speed * dt
end

love.keyboard.isDown asks whether a key is held right now, and takes several keys at once — true if any of them is down. That is how you support arrows and WASD in one line.

Four separate if statements, not elseif. With a chain, holding up and right would only move you up. Separate tests let both run, and diagonal movement falls out for free.

This is polling — asking every frame. It suits things that happen continuously while a key is held. For things that happen once per press, like firing, you want love.keypressed, which Activity 2.5 uses.

Staying on screen

main.lua
local half = ship.size / 2
ship.x = math.max(half, math.min(love.graphics.getWidth() - half, ship.x))

That is Activity 1.4's math.max and math.min, nested. Read it inside out: math.min stops it going too far right, math.max stops it going too far left. Together they trap the value in a range, with no if.

Clamp after all the movement, not inside each branch. One check at the end of update handles every way the ship could have left, including diagonals.

The half is there because this ship is stored by its center — which is why the draw call subtracts half the size. Storing the center is worth doing: it makes rotation and collision easier later, and it is what Activity 2.4's sprites want too.

divider

Build

Same skynest folder. You are putting a ship on screen and taking control of it.


Task 1: A ship as data

  • Replace Activity 2.2's panel with one table holding x, y, size and speed.
  • Start it in the middle of the window.
  • Draw it as a square, remembering that x and y are its center, so the rectangle's corner is half a size up and to the left.
main.lua
-- main.lua
local ship = { x = 400, y = 300, size = 40, speed = 300 }
function love.load()
love.graphics.setBackgroundColor(0.05, 0.05, 0.1)
end
function love.draw()
love.graphics.setColor(0.2, 0.8, 1)
love.graphics.rectangle("fill", ship.x - ship.size / 2, ship.y - ship.size / 2, ship.size, ship.size)
end

Task 2: Drive it

  • Add love.update(dt) with four separate if statements.
  • Accept arrows and WASD, in one test each.
  • Multiply by dt every time. Then remove the * dt from one direction and compare how it feels — it will be far too fast, because you are adding 300 pixels sixty times a second.
  • Hold two keys at once and confirm you move diagonally.
main.lua
-- main.lua
local ship = { x = 400, y = 300, size = 40, speed = 300 }
function love.load()
love.graphics.setBackgroundColor(0.05, 0.05, 0.1)
end
function love.update(dt)
if love.keyboard.isDown("left", "a") then
ship.x = ship.x - ship.speed * dt
end
if love.keyboard.isDown("right", "d") then
ship.x = ship.x + ship.speed * dt
end
if love.keyboard.isDown("up", "w") then
ship.y = ship.y - ship.speed * dt
end
if love.keyboard.isDown("down", "s") then
ship.y = ship.y + ship.speed * dt
end
end
function love.draw()
love.graphics.setColor(0.2, 0.8, 1)
love.graphics.rectangle("fill", ship.x - ship.size / 2, ship.y - ship.size / 2, ship.size, ship.size)
end

Task 3: Keep it on screen

  • Before Task 3, drive off the edge and watch the ship vanish entirely. It is still there, at an x of 2000.
  • Clamp both x and y at the end of update, using the window size rather than 800 and 600.
  • Account for the ship's half-size so it stops flush with the edge rather than half-buried in it.
main.lua
-- main.lua
local ship = { x = 400, y = 300, size = 40, speed = 300 }
function love.load()
love.graphics.setBackgroundColor(0.05, 0.05, 0.1)
end
function love.update(dt)
if love.keyboard.isDown("left", "a") then
ship.x = ship.x - ship.speed * dt
end
if love.keyboard.isDown("right", "d") then
ship.x = ship.x + ship.speed * dt
end
if love.keyboard.isDown("up", "w") then
ship.y = ship.y - ship.speed * dt
end
if love.keyboard.isDown("down", "s") then
ship.y = ship.y + ship.speed * dt
end
local half = ship.size / 2
ship.x = math.max(half, math.min(love.graphics.getWidth() - half, ship.x))
ship.y = math.max(half, math.min(love.graphics.getHeight() - half, ship.y))
end
function love.draw()
love.graphics.setColor(0.2, 0.8, 1)
love.graphics.rectangle("fill", ship.x - ship.size / 2, ship.y - ship.size / 2, ship.size, ship.size)
end

Challenge (Optional)

  • Hold up and right together and watch the ship move faster diagonally than it does straight. That is real, and it is because you added a full step in each direction. Fixing it properly needs the length of the movement vector — the bonus page on operators has exactly the tool.
  • Add a boost: while shift is held, multiply the speed. One extra variable, no new concepts.
  • Give the ship a velocity pair instead of moving x directly — keys change vx and vy, and one line at the end applies them. Then multiply the velocity by 0.9 each frame and you have drift. This is the model Activity 2.5's bullets use.
divider

Check Your Work

A cyan square pinned in the bottom-right corner of the window

Right and down held until nothing more happened. The square is still on screen because of the clamp, not because the keys ran out.

  • A blue square in the middle of a near-black window.
  • Arrows or WASD move it smoothly in eight directions.
  • It stops at all four edges and cannot leave.
  • Crossing the window takes a little under three seconds — which is what a speed of 300 in an 800-wide window means.

If it moves in jumps rather than smoothly, your movement is in love.draw. If it is far too fast, a * dt is missing. If it will not go diagonally, you used elseif.