Skip to content

Moving things

Two kinds of things move in a KKE game: the character, which walks, runs, jumps, vaults and climbs under kke::Locomotion, and physics bodies, which you move by giving them velocity and letting them roll, bounce and collide for real.

The character

The starter game's player is PlayerModule (C++). You tune how it moves with Locomotion's settings, in PlayerModule::init:

auto& move = m_loco->settings();
move.runSpeed = 4.5f;        // m/s (default 3.6)
move.sprintSpeed = 8.0f;     // default 6.2
move.jumpSpeed = 6.5f;       // higher jumps (default 5.2)
move.airAcceleration = 8.0f; // more steering in the air (default 5)
move.coyoteTime = 0.2f;      // jump this long after running off an edge (default 0.12 s)

Every setting and its default is in engine/include/kke/Locomotion.h; Movement explains the ideas behind them (why turning slows you down, why a jump pressed just before landing still counts). Tutorial 1 walks through the player module line by line.

From Lua, the starter game gives you player.position() (the feet), player.facing() and player.teleport(pos). Try player.teleport(Vec(-6, 2.1, -6)) in the Scripts panel console (F1). Those three are the game's own bindings, not the engine's, and a game adds them as its modules start, so use them inside hooks and timers (as every recipe here does), not at the top of a file.

A ball you steer

Moving a body is setting its velocity. This ball speeds up in the direction of the arrow keys, relative to where the camera looks, and hops with Right Ctrl.

A red ball on the ground

roll_ball.lua
-- roll_ball.lua: a ball you steer with the arrow keys, relative to where
-- the camera looks, and hop with Right Ctrl. Movement is forces on a
-- physics body, so it rolls, bumps and falls for real.
-- (docs/cookbook/moving.md)

input.define("ball.forward", "Ball forward", "Up")
input.define("ball.back", "Ball back", "Down")
input.define("ball.left", "Ball left", "Left")
input.define("ball.right", "Ball right", "Right")
input.define("ball.hop", "Ball hop", "Right Ctrl")

local ball = physics.sphere { pos = Vec(0, 1, 2), radius = 0.35, density = 300, bounce = 0.3, color = Vec(0.9, 0.2, 0.4) }
local PUSH = 6           -- how hard the arrows push (m/s per second)

hook.Add("Think", "ball.move", function(dt)
  -- Flatten the camera's forward so "up" never pushes into the ground.
  local f = camera.forward()
  local forward = Vec(f.x, 0, f.z):normalized()
  local right = forward:cross(Vec(0, 1, 0))

  local wish = Vec(0, 0, 0)
  if input.held("ball.forward") then wish = wish + forward end
  if input.held("ball.back") then wish = wish - forward end
  if input.held("ball.right") then wish = wish + right end
  if input.held("ball.left") then wish = wish - right end

  local v = physics.velocity(ball)
  if wish:length() > 0 then
    v = v + wish:normalized() * (PUSH * dt)
  end
  -- Hop only when nearly still vertically (on the ground, more or less).
  if input.pressed("ball.hop") and math.abs(v.y) < 0.2 then
    v = v + Vec(0, 5, 0)
  end
  physics.setVelocity(ball, v)

  -- Fell off the world? Back to the start.
  if physics.position(ball).y < -10 then
    physics.remove(ball)
    ball = physics.sphere { pos = Vec(0, 1, 2), radius = 0.35, density = 300, bounce = 0.3, color = Vec(0.9, 0.2, 0.4) }
  end
end)

Download roll_ball.lua

  • camera.forward() points where the camera looks. Flattening it (y = 0) and normalizing it gives "forward along the ground"; the cross product with up gives "right".
  • Adding to the velocity (instead of setting it) keeps the ball's momentum: it still rolls on when you let go, and bumps change its path.
  • Think gets dt, the seconds since the last frame. Multiplying by it makes the push the same at 30 and at 240 frames per second.

A pet that follows you

The classic "seek and arrive": head toward a target, slow down near it, stop at a comfortable distance.

A green ball beside the player

pet.lua
-- pet.lua: a ball that follows you around like a pet. It steers toward
-- a spot just behind you, slows down as it arrives, and hops when you
-- get far ahead. (docs/cookbook/moving.md)

local pet = physics.sphere { pos = Vec(1.5, 0.5, 6), radius = 0.25, density = 200, color = Vec(0.4, 0.9, 0.5) }
local FOLLOW = 1.8   -- metres it keeps from you
local SPEED = 7      -- top speed, m/s

hook.Add("Think", "pet.follow", function(dt)
  local me = player.position()
  local at = physics.position(pet)
  local toMe = Vec(me.x - at.x, 0, me.z - at.z)    -- flat: steer, don't fly
  local distance = toMe:length()

  local v = physics.velocity(pet)
  if distance > FOLLOW then
    -- "Arrive": full speed far away, slowing to a stop as it gets close.
    local speed = math.min(SPEED, (distance - FOLLOW) * 3)
    local want = toMe:normalized() * speed
    -- Turn the current velocity toward the wanted one, a bit each frame.
    local blend = math.min(1, 8 * dt)
    v = Vec(v.x + (want.x - v.x) * blend, v.y, v.z + (want.z - v.z) * blend)
  end
  -- Left far behind (or you jumped up somewhere): hop.
  if distance > 6 and math.abs(v.y) < 0.1 then v = v + Vec(0, 5, 0) end
  physics.setVelocity(pet, v)

  if at.y < -10 then           -- fell off: back beside you
    physics.remove(pet)
    pet = physics.sphere { pos = me + Vec(1, 1, 0), radius = 0.25, density = 200, color = Vec(0.4, 0.9, 0.5) }
  end
end)

Download pet.lua

Blending the velocity toward the wanted one (blend) instead of setting it outright is what makes the pet curve smoothly instead of snapping to each new direction. Raise the 8 for a snappier pet, lower it for a lazier one.

A patrol

Waypoints in a list, an index for the one it's heading to, and a pause at each: the core of guards, moving platforms and delivery vans.

A red guard walking between waypoints

patrol.lua
-- patrol.lua: a guard that walks between waypoints, waits at each, and
-- turns back. "Go to the next point" is the heart of most enemy and
-- moving-platform scripts. (docs/cookbook/moving.md)

local WAYPOINTS = { Vec(-3, 0, -1), Vec(3, 0, -1), Vec(3, 0, 3), Vec(-3, 0, 3) }
local SPEED = 2.5   -- m/s
local WAIT = 1.0    -- seconds at each point

local guard = physics.box { pos = WAYPOINTS[1] + Vec(0, 0.5, 0), size = Vec(0.6, 1, 0.6), density = 400,
                            friction = 0, color = Vec(0.8, 0.25, 0.25) }
local target = 2      -- index of the waypoint it's heading for
local waiting = 0

hook.Add("Think", "patrol.walk", function(dt)
  local at = physics.position(guard)
  local goal = WAYPOINTS[target]
  local toGoal = Vec(goal.x - at.x, 0, goal.z - at.z)
  local v = physics.velocity(guard)

  if waiting > 0 then
    waiting = waiting - dt
    physics.setVelocity(guard, Vec(0, v.y, 0))
  elseif toGoal:length() < 0.15 then
    -- Arrived: wait, then head for the next one (wrapping round to 1).
    waiting = WAIT
    target = target % #WAYPOINTS + 1
  else
    local step = toGoal:normalized() * SPEED
    physics.setVelocity(guard, Vec(step.x, v.y, step.z))  -- keep falling if it falls
  end
end)

Download patrol.lua

target % #WAYPOINTS + 1 counts 2, 3, 4, 1, 2, ... For a guard that goes back the way it came (1, 2, 3, 4, 3, 2, ...), keep a direction of +1 or -1 and flip it at either end.

Launch pad

The Contact hook fires when two bodies start touching, with both ids, how hard they hit and where. Here, anything landing on the pad is thrown up again.

Orange balls flying up from a cyan pad

launch_pad.lua
-- launch_pad.lua: anything that lands on the pad is fired into the air.
-- The Contact hook says when two bodies touch. (docs/cookbook/moving.md)

local pad = physics.box { pos = Vec(-2, 0.05, 1), size = Vec(1.6, 0.1, 1.6), color = Vec(0.2, 0.9, 0.9), static = true }
local LAUNCH = 11   -- m/s upward

hook.Add("Contact", "launch_pad.touch", function(c)
  -- c.a and c.b are the two bodies; one of them must be the pad.
  local other = nil
  if c.a == pad then other = c.b elseif c.b == pad then other = c.a end
  if not other then return end
  -- Scripts can only push bodies scripts made: pcall turns "not yours"
  -- (the level, say) into a quiet false instead of an error.
  local v = physics.velocity(other)
  pcall(physics.setVelocity, other, Vec(v.x, LAUNCH, v.z))
end)

-- Something to launch: a ball dropped on the pad every two seconds.
timer.Create("launch_pad.feed", 2, 0, function()
  local ball = physics.sphere { pos = Vec(-2 + math.random() * 0.6 - 0.3, 3, 1), radius = 0.2, color = Vec(1, 0.5, 0.1) }
  timer.Simple(8, function() physics.remove(ball) end)
end)

Download launch_pad.lua

A script may only push bodies that scripts made. pcall(f, ...) calls f and catches its error instead of stopping, so the pad quietly ignores the level's own boxes.

Moving a body to an exact place

Sometimes you want a body at an exact spot every frame (a platform on a path, a part of something animated). Give it the velocity that gets it there in one frame:

local function moveTo(id, target, dt)
  physics.setVelocity(id, (target - physics.position(id)) / math.max(dt, 1 / 240))
end

It still collides and pushes things on the way, which a teleport wouldn't. The animation recipes are built on this.

Next: cameras.