Skip to content

Working with the Scene Graph

Most gameplay scripts spend their time manipulating scene objects — the live nodes in the running scene. This guide explains how Items (project assets) relate to SceneObjects (runtime instances), and how to create, find, transform, and organize objects from Lua.

Items vs SceneObjects

Concept Role
Item A persistent project asset (mesh, material, prefab, …) stored in the resource tree. Loaded via Asset Loading.
SceneObject A runtime node in the active scene graph. May be placed in the editor or created from a script. Has transform, tags, layers, physics, and attached behaviours.

Every Behaviour script has an implicit self.node property (Properties) that references the SceneObject the script is attached to.

Accessing the SceneObject module

local SceneObject = require 'engine/sceneobject'
local Transform = require 'engine/math/transform'
local Vector3 = require 'engine/math/vector3'

Finding objects

Use static search methods to locate objects by name, tag, or ID. All find functions support an optional root argument to limit the search to a subtree.

By name — returns the first match, or nil:

local player = SceneObject.findByName("Player")
if player then
    print(player:getName())
end

All by name — returns a table (may be empty):

local enemies = SceneObject.findAllByName("Enemy")
for _, enemy in ipairs(enemies) do
    enemy:addTag("Hostile")
end

By tag:

local pickups = SceneObject.findByTag("Pickup")
for _, pickup in ipairs(pickups) do
    pickup:delete()
end

By ID — useful when you stored an object's ID earlier:

local obj = SceneObject.findById(self.savedId)

Scoped search — search within a parent's hierarchy:

local level = SceneObject.findByName("Level1")
local door = level:findByName("Door")       -- instance call: root defaults to level
local chest = SceneObject.findByName("Chest", level)  -- explicit root
  • Use Property.Node links in the inspector when references are known at design time. See Properties. Use find methods for dynamic lookup.

Creating objects

Creation is asynchronous — typed helpers such as SgSphere.create return a Promise. Use await() during setup or :next() at runtime.

Generic creation via SceneObject.create:

SceneObject.create(SceneObject.Type.Box, {
    name = "Crate",
    transform = Transform.new(Vector3.new(0, 1, 0)),
    width = 1,
    height = 1,
    depth = 1,
}):next(function(crate)
    crate:setActive(true)
end)

Typed helpers are clearer and expose type-specific options. See the Scene Objects API for the full list (SgSphere, SgMesh, SgPrefab, SgTerrain, …).

During init(), blocking setup with await is fine:

function Spawner:init()
    self.crate = await(SgBox.create({
        name = "Crate",
        width = 1, height = 1, depth = 1,
        transform = Transform.new(Vector3.new(0, 0.5, 0)),
    }))
end

Transforms

Read the local or world transform:

local t = self.node:getTransform()
local world = self.node:getWorldTransform()

Modify via withTransform (recommended — reads, mutates, writes back):

self.node:withTransform(function(t)
    t.position.y = t.position.y + 1
end)

Shortcuts for common operations:

self.node:translate(1, 0, 0)
self.node:rotate(0, 90, 0)
self.node:scale(2, 2, 2)
self.node:setPosition(0, 5, 0)

Direction helpers (world space):

local forward = self.node:getForward()
local right = self.node:getRight()
local up = self.node:getUp()

See Math and Transforms for Vector3, Quaternion, and Transform in depth.

Hierarchy and parenting

local parent = self.node:getParent()
local children = self.node:getChildren()

for _, child in ipairs(children) do
    print(child:getDisplayName())
end

-- Reparent at runtime
child:moveToParent(newParent)

Pass a parent SceneObject as the last argument to create functions to spawn directly under that node.

Tags and layers

Tags are string labels for grouping and search. Layers control rendering and physics filtering.

self.node:addTag("Enemy")
if self.node:hasTag("Enemy") then
    -- ...
end

self.node:setLayers({1, 3})

Active state and cleanup

self.node:setActive(false)   -- hide and disable updates on this subtree
self.node:enable()
self.node:disable()

self.node:delete()           -- remove from scene (cannot be undone)

Clone an existing object (optionally offset and reparent):

original:clone(Vector3.new(5, 0, 0)):next(function(copy)
    copy:setActive(true)
end)

Physics on SceneObjects

Physics-enabled objects expose impulse, force, and velocity methods directly on SceneObject. See Physics Scripting for raycasts and collision events; the snippet below shows object-level forces:

local Vector3 = require 'engine/math/vector3'
self.node:applyCentralImpulse(Vector3.new(0, 5, 0))
self.node:setLinearVelocity(Vector3.new(2, 0, 0))

Scene objects expose datatype fields (the same fields you see in the inspector):

self.node:castShadow = true
local radius = self.node.radius

Link fields resolve to loaded Item handles:

local meshItem = self.node.mesh

Swap materials at runtime:

self.node.material = myMaterialItem

Cross-script access

To call methods on another object's behaviour script:

self.target:behaviour("EnemyController"):takeDamage(10)

See Cross-Behaviour Calls for details and naming rules.

See also Properties for linking objects in the inspector.

Snapshots

Save and restore object state (useful for undo-style mechanics or checkpoints):

local snapshot = self.node:takeSnapshot(true)
-- ... later ...
self.node:rollbackSnapshot(snapshot)

Choosing an approach

Goal Approach
Designer-placed reference Property.Node on the behaviour
Runtime lookup findByName / findByTag
Spawn from asset SgPrefab.create or Loader.loadPrefab — see Prefabs
Procedural primitive SgBox.create, SgSphere.create, …
Talk to another script :behaviour("ScriptItemName")
Global game state Variable Sets

Further reading