Reference

Scripting API

Every horizon.* function, in Lua and Python — attachment, lifecycle, properties and worked examples.

Attachment & lifecycle

How Scripts Run

A complete reference for the scripting surface: how a script is attached and run, the lifecycle callbacks, and every function the horizon API exposes. Lua and Python share one underlying API, so the same calls are available in both — only the way you package a script differs.

You attach a script to an entity with the Script component, which references a script asset by UUID plus a logical module name. Each script asset is either Lua or Python — the language is stored on the asset and is inferred from the file extension (.py → Python, everything else → Lua). Both languages are hosted side by side; an instance is routed to the right runtime automatically.

The lifecycle is driven by the editor's Play Mode. When you enter play mode, every entity that carries an enabled Script component gets one script instance bound to it. Each instance is bound to its entity: the raw entity id is exposed as self.entityId in Lua and self.entity_id in Python. That id is the first argument to nearly every horizon function.

Phase When Callback
Start Once, when play mode begins (after property overrides are injected). onStart(self) / on_start(self)
Update Every play-mode frame. dt is the elapsed time in seconds. onUpdate(self, dt) / on_update(self, dt)
Collision enter When another physics body starts touching this entity. otherId is the other entity's id. onCollisionEnter(self, otherId) / on_collision_enter(self, otherId)
Collision exit When another body stops touching this entity. onCollisionExit(self, otherId) / on_collision_exit(self, otherId)

Every callback is optional — if a script doesn't define one, it's simply skipped. Collision callbacks only fire for entities that also have physics colliders.

Current scope: scripts are executed by the editor's Play Mode. The standalone runtime that ships with an exported game does not yet drive script lifecycles, so gameplay scripts run inside the editor for now.

Two languages, one API

Lua vs. Python

A Lua script is a module: create a table, add lifecycle functions to it, and return it. Every callback takes self as its first parameter (the per-instance table). A Python script defines a single subclass of horizon.Behavior; the callbacks are ordinary methods, and each instance is one object of that class.

module shape
-- Lua: return a table of callbacks
local M = {}
function M.onStart(self) end
function M.onUpdate(self, dt) end
return M

# Python: subclass horizon.Behavior
import horizon
class MyScript(horizon.Behavior):
    def on_start(self): pass
    def on_update(self, dt): pass

Naming: the lifecycle uses each language's convention — onUpdate in Lua, on_update in Python. The horizon.* functions, however, are camelCase in both languages — you call horizon.setPosition(...) from Python too, not set_position.

Per-instance data

Exposed Properties

You can expose typed fields that appear as editable controls in the Details panel, set per instance. In Lua, add a properties table to your module; in Python, declare typed class attributes (any non-underscore attribute). Supported types are number (int or float), bool and string. The value you set in the editor is injected into the instance before onStart runs, so read it there or in onUpdate.

exposing properties
-- Lua
local M = {}
M.properties = { speed = 4.0, jumpHeight = 2.0, canFly = false }
function M.onUpdate(self, dt)
    -- self.speed is the per-instance value from the editor
end
return M

# Python: typed class attributes become editable properties
class Player(horizon.Behavior):
    speed = 4.0
    jump_height = 2.0
    can_fly = False

Function reference

The horizon API

The same functions are available in Lua (as the global horizon table) and Python (via import horizon). Entity arguments are the raw entity id. Invalid ids are tolerated: getters return neutral defaults, setters are no-ops.

Function Returns Description
log(message) Write a string to the engine log.
getName(id) string The entity's name.
getPosition(id) x, y, z The entity's position. Default (0, 0, 0).
setPosition(id, x, y, z) Move the entity's transform.
getRotation(id) x, y, z Euler angles in degrees. Default (0, 0, 0).
setRotation(id, x, y, z) Set Euler rotation in degrees.
getScale(id) x, y, z Scale. Default (1, 1, 1).
setScale(id, x, y, z) Set scale.
spawn(parentId, name) id Create a named entity parented under parentId, returns its id. name is optional (defaults to "Entity").
destroy(id) Remove an entity.
raycast(ox, oy, oz, dx, dy, dz [, maxDist]) table / nil Physics ray from an origin along a direction. Returns a hit table (see below) or nil/None on a miss. maxDist defaults to 1000.
setVelocity(id, vx, vy, vz) Set a character controller's velocity.
isGrounded(id) bool Whether a character controller stands on ground.

Physics functions (raycast, setVelocity, isGrounded) need an active physics world. Without one, raycast returns a miss, isGrounded returns false and setVelocity is a no-op.

The raycast hit table

A hit is a flat table (Lua) / dict (Python) — the point and normal are spread across individual keys, not nested:

Key Meaning
entity Id of the entity that was hit.
x, y, z The hit point in world space.
nx, ny, nz The surface normal at the hit point.
distance Distance from the ray origin to the hit.

Python subclass

horizon.Behavior (Python)

Python scripts must define exactly one subclass of horizon.Behavior. The base class provides the bound entity_id attribute; every lifecycle method is optional and overridden as needed:

Member Description
entity_id Attribute: the id of the entity this instance is bound to.
on_start(self) Called once when play mode begins.
on_update(self, dt) Called each frame with the delta time in seconds.
on_collision_enter(self, other_id) Called when another body starts touching this entity.
on_collision_exit(self, other_id) Called when another body stops touching this entity.

Worked examples

Complete Examples

A minimal starter in each language — these mirror the skeletons the editor generates when you create a new script:

starter.lua
local M = {}

function M.onStart(self)
end

function M.onUpdate(self, dt)
end

return M
starter.py
import horizon

class NewScript(horizon.Behavior):
    def on_start(self):
        pass

    def on_update(self, dt):
        pass

And a working example in each language — a mover that logs its name, spawns a child, and drives its own transform every frame:

patroller.lua
local M = {}

M.properties = { speed = 2.0 }

function M.onStart(self)
    self.t = 0
    horizon.log("Patroller start: " .. horizon.getName(self.entityId))
    -- spawn a marker parented to this entity
    self.marker = horizon.spawn(self.entityId, "Marker")
end

function M.onUpdate(self, dt)
    self.t = self.t + dt
    local x = math.sin(self.t) * self.speed
    horizon.setPosition(self.entityId, x, 0, 0)
end

function M.onCollisionEnter(self, otherId)
    horizon.log("Hit " .. horizon.getName(otherId))
end

return M
patroller.py
import horizon
import math

class Patroller(horizon.Behavior):
    speed = 2.0

    def on_start(self):
        self.t = 0.0
        horizon.log("Patroller start: " + horizon.getName(self.entity_id))
        # spawn a marker parented to this entity
        self.marker = horizon.spawn(self.entity_id, "Marker")

    def on_update(self, dt):
        self.t += dt
        x = math.sin(self.t) * self.speed
        horizon.setPosition(self.entity_id, x, 0, 0)

    def on_collision_enter(self, other_id):
        horizon.log("Hit " + horizon.getName(other_id))

Hot reload: while play mode is running, saving a script patches its callbacks into the live instances — your data fields (self.t, self.marker, …) are preserved across the reload.