Script cookbook

Working scripts for common gameplay. Paste one into an object’s script (or the Level Script), adjust the names, and press Play.

Sounds: examples that call play_sound("sounds/...") expect a sounds folder next to your .blend. Use any WAV/MP3/OGG you like, or delete those lines.

Coins & score

On each coin: Body Trigger, Visible In Game ticked. Name them Coin, Coin.001… (Blender does this when you duplicate).

function on_update(self, dt)
  self:rotate(0, 0, 90 * dt)
end

function on_trigger_enter(self, other)
  if other.is_player then
    game.score = (game.score or 0) + 1
    ui.set("score", "Coins: " .. game.score)
    play_sound("sounds/coin.wav")
    self:destroy()
  end
end

Collect them all to open the gate

Level Script — count the coins when the level starts:

function on_start()
  game.score = 0
  game.total = #find_all("Coin")
  ui.set("score", "Coins: 0 / " .. game.total)
end

Then in the coin script, after adding to the score:

    ui.set("score", "Coins: " .. game.score .. " / " .. game.total)
    if game.score == game.total then
      ui.message("The gate is open!")
      find("Gate"):call("open")      -- see the Gate script below
    end

Door on a hinge

Add an Empty at the hinge edge, parent the door mesh to it (select door, then Empty, Ctrl+P → Object), set the door to Kinematic, and put this script on the Empty. Pressing E on the door works because interaction reaches parents.

prompt = "Open door"
local open, angle = false, 0

function on_interact(self)
  open = not open
  prompt = open and "Close door" or "Open door"
end

function on_update(self, dt)
  angle = angle + ((open and 100 or 0) - angle) * math.min(1, dt * 5)  -- ease toward 100 or 0 degrees
  self.rotation = vec3(0, 0, angle)
end

Pressure plate & gate

Gate (Kinematic, named Gate) — slides into the floor when something calls open:

local target

function on_start(self) target = self.position end

function open(self)                       -- other scripts call this with find("Gate"):call("open")
  target = target - vec3(0, 0, 3)
  play_sound("sounds/door.wav", 1, self.position)
end

function on_update(self, dt)
  self.position = self.position:lerp(target, math.min(1, dt * 2))
end

Plate — a flat Trigger on the floor. Anything can press it: the player, or a crate you throw onto it.

local used = false
function on_trigger_enter(self, other)
  if not used then
    used = true
    find("Gate"):call("open")
  end
end

Hazard (lava, spikes, falling off the world)

A Trigger covering the dangerous area. For a “fell off the map” catch, make a huge flat trigger well below the level.

function on_trigger_enter(self, other)
  if other.is_player then
    ui.message("Ouch!", 1.5)
    respawn()
  end
end

Countdown timer

Level Script:

local left = 60
local over = false

function on_update(dt)
  if over then return end
  left = left - dt
  ui.set("time", "Time: " .. math.ceil(left))
  if left <= 0 then
    over = true
    ui.message("Time's up!", 3)
    after(3, restart)
  end
end

Spawner

Make a ball, set it to Dynamic, tick Template, and name it Ball. This Level Script drops one somewhere random every two seconds:

local function drop()
  spawn("Ball", vec3(random(-5, 5), random(-5, 5), 10))
  after(2, drop)                         -- schedule the next one
end

function on_start() drop() end

Shooting balls with F

Level Script, using the same Ball template:

function on_update(dt)
  if input.pressed("F") then
    local b = spawn("Ball", camera.position() + camera.forward() * 1.5)
    b.velocity = camera.forward() * 20
    after(10, function() if b.valid then b:destroy() end end)  -- tidy up
  end
end

Enemies with health

On each enemy, add Blender custom properties (Object Properties → Custom Properties): enemy = 1 and health = 3. Enemy script:

local hp
function on_start(self) hp = self.props.health or 3 end

function hurt(self, amount)
  hp = hp - amount
  if hp <= 0 then
    play_sound("sounds/pop.wav", 1, self.position)
    self:destroy()
  end
end

Level Script — press F to zap whatever you’re aiming at:

function on_update(dt)
  if input.pressed("F") then
    local hit = raycast(camera.position(), camera.forward(), 50)
    if hit and hit.entity and hit.entity.props.enemy then
      hit.entity:call("hurt", 1)
    end
  end
end

Chasing guard

Tick AI Agent on the guard. It wanders until you get close, then chases you.

function on_update(self, dt)
  if self:distance_to(player) < 7 then
    self:move_to(player.position)
    if self:distance_to(player) < 1.3 then
      ui.message("Caught!", 1.5)
      respawn()
    end
  elseif self.arrived or not self.moving then
    self:move_to(random_point(self.position, 12))
  end
end
See the navmesh with F3 in the game. Agents only walk on Static surfaces.

Talking sign / NPC

prompt = "Read"
local lines = {
  "Welcome, traveller.",
  "The gate opens when every coin is found.",
  "Watch out for the guard!",
}
local i = 0

function on_interact(self)
  i = i % #lines + 1                     -- 1, 2, 3, 1, 2, ...
  ui.message(lines[i], 3)
  prompt = (i == #lines) and "Read again" or "Next"
end

Light a fire

On a campfire: tick Particles, click the Fire preset and untick Emit On Start. Optionally give it a looping crackle Sound without Play On Start.

prompt = "Light fire"
local lit = false

function on_interact(self)
  lit = not lit
  self.emitting = lit
  if lit then
    self:emit(40)                        -- a burst to get it going
    self:play_sound()
    prompt = "Put out"
  else
    self:stop_sound()
    prompt = "Light fire"
  end
end

Cutscene: show the gate opening

Add an Empty named GateCam where the camera should be. Call this from anywhere (e.g. right after find("Gate"):call("open")):

local function show_gate()
  camera.set(find("GateCam").position, find("Gate").position)
  after(3, function() camera.release() end)
end

Exit to the next level

Each Blender scene becomes a level when you package the game. On a Trigger at the exit of scene Level1:

function on_trigger_enter(self, other)
  if other.is_player then
    save("score", game.score or 0)       -- game.* is wiped on level change
    load_level("Level2.glb")
  end
end

In Level2’s Level Script:

function on_start()
  game.score = load("score", 0)
  ui.set("score", "Coins: " .. game.score)
end
Testing: Play exports only the scene you’re in, to a .bf_cache folder next to your saved .blend. load_level in a test run finds the other level there only if you’ve pressed Play in that scene since last editing it. The packaged game always has every level.

High score that survives quitting

-- call when the player finishes
local function finish()
  local best = load("best", 0)
  if game.score > best then
    save("best", game.score)
    ui.message("New high score: " .. game.score .. "!", 4)
  else
    ui.message("Score " .. game.score .. " (best " .. best .. ")", 4)
  end
end

2D games

A side-scrolling hero (run, jump, animation, a following camera), coins, a camera that stops at the level’s ends and parallax backgrounds are on the 2D games page.

More examples

The Samples & docs download includes samples/test_level.blend with 15 working scripts — coins, a door, a lift, a gate, a ball spawner and an animated tentacle. Open it and click objects to read their scripts.