Learning

Lua + LÖVE: Make a Game · Lesson 05 · 45 min

Move at the speed of time

Read continuous keyboard input and use delta time, normalization, and clamping for reliable movement.

Learn

Continuous input and frame-rate independence.

Build

Eight-direction player movement.

Prove it

Keep diagonal speed fair and stay on-screen.

Movement connects input, math, and the game loop. We want the player to travel at a speed measured in pixels per second, regardless of the computer’s frame rate.

See why dt matters

Suppose you add four pixels every frame:

player.x = player.x + 4

At 30 frames per second that travels 120 pixels in one second. At 120 FPS it travels 480. The faster computer changes the game.

Use a speed and elapsed time instead:

player.x = player.x + player.speed * dt

If speed is 240 pixels per second, the one-second distance stays 240.

Frame-rate laboratory

Simulate one second using either 4 pixels per frame or 240 pixels per second multiplied by dt.

4 pixels each frame240 px
240 pixels/second × dt240 px

Poll held keys

Movement should continue as long as a key is held. Poll the current keyboard state during every update:

if love.keyboard.isDown("right", "d") then
  player.x = player.x + player.speed * dt
end

isDown can accept more than one key and returns true if any is held. Later, love.keypressed will handle one-time actions such as starting a game.

Add speed to the player table:

local player = {
  x = 480,
  y = 360,
  radius = 14,
  speed = 260,
}

Then replace love.update with this first movement version, keeping the signal pulse line:

function love.update(dt)
  signal.pulse = signal.pulse + dt * 4

  if love.keyboard.isDown("left", "a") then
    player.x = player.x - player.speed * dt
  end
  if love.keyboard.isDown("right", "d") then
    player.x = player.x + player.speed * dt
  end
  if love.keyboard.isDown("up", "w") then
    player.y = player.y - player.speed * dt
  end
  if love.keyboard.isDown("down", "s") then
    player.y = player.y + player.speed * dt
  end
end

Run the game and move with WASD or the arrows.

Fix faster diagonal movement

The code above adds full horizontal and vertical movement simultaneously. If both components are 260, diagonal speed is about 368 because of the Pythagorean theorem:

√(260² + 260²) ≈ 368

Build a direction first, then normalize it to length 1:

local dx, dy = 0, 0

if love.keyboard.isDown("left", "a") then dx = dx - 1 end
if love.keyboard.isDown("right", "d") then dx = dx + 1 end
if love.keyboard.isDown("up", "w") then dy = dy - 1 end
if love.keyboard.isDown("down", "s") then dy = dy + 1 end

local length = math.sqrt(dx * dx + dy * dy)
if length > 0 then
  dx = dx / length
  dy = dy / length
end

player.x = player.x + dx * player.speed * dt
player.y = player.y + dy * player.speed * dt

The length > 0 guard matters. Dividing the zero vector by zero would produce invalid values.

Put that block inside love.update, after the signal update. Opposite keys cancel naturally: left contributes -1, right contributes +1, and the result is zero.

Keep the whole player on-screen

Clamping forces a value into a range:

local function clamp(value, minimum, maximum)
  return math.max(minimum, math.min(maximum, value))
end

Use the player’s radius as the margin so its edge—not only its center—stays visible:

player.x = clamp(
  player.x + dx * player.speed * dt,
  player.radius,
  love.graphics.getWidth() - player.radius
)

player.y = clamp(
  player.y + dy * player.speed * dt,
  player.radius,
  love.graphics.getHeight() - player.radius
)

This replaces the two simple position assignments. Try holding two direction keys into each corner.

Continuous state versus discrete events

Use the right kind of input for the action:

Player intent Input style Why
Keep moving love.keyboard.isDown in update true during every held frame
Start game love.keypressed callback one event per press
Type a name love.textinput callback receives entered text
Aim at cursor love.mouse.getPosition in update current position matters

If you start a game by polling Enter, the key may still be held on the next frame and trigger something again. Event callbacks represent the edge: the moment a key becomes down.

Your turn: boost

While either Shift key is held, make the player move 1.6 times faster. Calculate a local currentSpeed; do not permanently change player.speed.

One solution

Place this before the position calculation:

local currentSpeed = player.speed
if love.keyboard.isDown("lshift", "rshift") then
  currentSpeed = currentSpeed * 1.6
end

Then use currentSpeed in both position expressions.

Checkpoint

Your player should move smoothly, travel at the same speed diagonally and straight, accept either control scheme, and stop with its outer edge at every window boundary.