Skip to main content

Creature & Brain

A Creature is any living entity in the world — player character or NPC.
A Brain is the decision-making layer attached to a Creature. The two are always separate: swapping the Brain transfers control without changing the Creature's world state.


Design overview

This mirrors Unreal's Pawn / Controller separation. Key benefits:

  • Possession — give the player control of any Creature at runtime with one call.
  • Cutscene takeover — detach the player brain, run scripted movement, reattach.
  • AI testing — attach a CreatureBrain to the player's character to verify AI.
  • Spectator — possess a camera-only creature with a PlayerBrain.

Spawning creatures

scripts/main.lua
function on_init()
-- Spawn at a marker node in the scene
local player = Engine.world.spawn_creature("player_start")
player:set_brain(Engine.world.make_player_brain())

local guard = Engine.world.spawn_creature("guard_spawn_01")
guard:set_brain(Engine.world.make_creature_brain("scripts://entities/guard.lua"))
end
FunctionDescription
Engine.world.spawn_creature(marker)Spawn at a named marker node; returns Creature
Engine.world.find_creature(name)Look up an already-spawned creature by name; returns Creature|nil
Engine.world.make_player_brain()Create a PlayerBrain ready to attach
Engine.world.make_creature_brain(uri)Create a CreatureBrain backed by a Lua script

Brain swap

Brain swap examples
creature:set_brain(Engine.world.make_player_brain())

-- Give control to an AI script
creature:set_brain(Engine.world.make_creature_brain("scripts://entities/boss.lua"))

-- Remove all control — creature idles
creature:set_brain(nil)

-- Query what is currently attached
local kind = creature:get_brain_type() -- "player" | "ai" | "none"

Reading state

Reading creature state
local x, y, z   = creature:get_position()
local p, yaw, r = creature:get_rotation()
local vx, vy, vz = creature:get_velocity()

local hp = creature:get_health()
local maxHp = creature:get_max_health()
local alive = creature:is_alive()
local ground = creature:is_grounded()
local mode = creature:get_camera_mode() -- "first_person" | "third_person"

Writing state

Writing creature state
creature:set_health(80)
creature:set_move_speed(5.0) -- world units per second
creature:set_camera_mode("third_person")
creature:set_visible(false)

Actions

Creature actions
-- Combat
creature:take_damage(25, attacker) -- attacker is a Creature or nil
creature:heal(10)
creature:kill() -- instant death, fires "death" event

-- Physics
creature:apply_impulse(0, 8, 0) -- jump-like upward boost
creature:teleport(10, 0, -5) -- instant position change, no physics

-- Animation
creature:play_animation("run")

-- Abilities
creature:give_ability("fireball")
creature:use_ability("fireball")
creature:remove_ability("fireball")

Events

Subscribe to creature-local events with creature:on(event, callback). These fire only for this specific creature instance, not all creatures.

Creature events
creature:on("damage", function(amount, source)
Engine.log.info("Hit for " .. amount)
if source then
Engine.log.info("by " .. source.name)
end
end)

creature:on("death", function(killer)
Engine.events.fire("player_died")
end)

creature:on("healed", function(amount) end)
creature:on("grounded", function() end)
creature:on("airborne", function() end)
creature:on("ability_used", function(ability_id) end)

Unsubscribe with creature:off(event).


Camera rig

Every Creature owns a camera rig. It is activated automatically when a PlayerBrain takes control and deactivated when the brain is swapped away. You can also configure it explicitly:

Camera rig configuration
local cam = creature:get_camera()

cam:set_mode("third_person") -- or "first_person"
cam:set_distance(4.0) -- spring arm length (third-person only)
cam:set_height_offset(1.5) -- pivot height above creature origin
cam:set_fov(75.0) -- vertical FOV in degrees

-- Activate / deactivate manually (e.g. for split-screen or cutscene cameras)
cam:activate()
cam:deactivate()

local active = cam:is_active()
local mode = cam:get_mode()

The spring arm automatically slides away from geometry in third-person mode, so the camera never clips through walls.


Minimal player setup

scripts/entities/player.lua
-- scripts/entities/player.lua

local M = {}

function M.init(marker_name)
local p = Engine.world.spawn_creature(marker_name)
p:set_brain(Engine.world.make_player_brain())
p:set_move_speed(5.0)

local cam = p:get_camera()
cam:set_mode("third_person")
cam:set_distance(4.0)
cam:set_fov(75.0)

p:on("death", function()
Engine.events.fire("player_died")
end)

return p
end

return M
scripts/main.lua
-- scripts/main.lua
local player = require("entities.player")

function on_init()
player.init("player_start")
end

Minimal AI creature

The script passed to make_creature_brain is loaded into the same Lua state and called as if it were main.lua, but scoped to that single creature.
The creature is passed as self:

scripts/entities/guard.lua
-- scripts/entities/guard.lua

local patrol_points = { "patrol_a", "patrol_b", "patrol_c" }
local current = 1
local wait = 0.0

function on_update(dt)
wait = wait - dt
if wait > 0 then return end

local target = Engine.scene.find(patrol_points[current])
if target then
local tx, ty, tz = target:get_position()
self:teleport(tx, ty, tz)
self:play_animation("walk")
end

current = current % #patrol_points + 1
wait = 2.0
end