Reference

Scripting API

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

Attachment & lifecycle

How Scripts Run

How a script is attached and run, its lifecycle callbacks and the flat functions on horizon. Lua and Python share one underlying API, so the same calls are available in both — only the way you package a script differs. Every engine function, with its signature, is in the Engine API Reference.

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.

Scripts run in the editor's Play Mode and in the exported game alike. When play mode starts, or when the game loads a scene, every entity that carries an enabled Script component gets one script instance bound to it; entities that arrive later, such as a streamed-in zone, get theirs when they load. 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.

◆

More callbacks: these four are the everyday ones. A script can also receive trigger overlaps, input actions, timers, animation notifies, UI clicks and hovers, multiplayer session events, replicated-variable changes, remote calls and anti-cheat reports. All of them, with their Lua and Python names, are listed under Callbacks in the Engine API Reference.

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). They come in two layers: the 35 flat functions below, which sit directly on horizon, and the groups horizon.math.*, horizon.physics.*, horizon.net.* and so on, generated from the engine's API registry. The groups are listed function by function in the Engine API Reference, together with the rules for passing vectors and entity ids.

Entity arguments are the raw entity id. Invalid ids are tolerated: getters return neutral defaults, setters are no-ops. Arguments in brackets are optional.

FunctionReturnsDescription
log(message)—Writes a line to the Console panel and to HorizonEngine.log. The only way to reach the log row from a script: there is no horizon.log.* group. Reference
getName(entity)stringThe entity's name, "" for an invalid id. Reference
spawn(parent [, name])entityCreates an empty entity named name (default "Entity") under parent and returns its id. parent 0 or invalid = top level. Unlike horizon.entity.spawn, the name is optional here. Reference
destroy(entity)—Removes the entity; an invalid id is ignored. Check the id first: in a script 0 is the scene root, not “nothing”. Reference
getPosition(entity)x, y, zLocal position, i.e. relative to the parent. (0, 0, 0) for an invalid id. There is no horizon.transform.* group, so this is the only script route to the row. Reference
setPosition(entity, x, y, z)—Sets the local position. On an entity with a rigid body or a character controller this is a teleport, so the physics step does not snap it back. Reference
getRotation(entity)x, y, zLocal Euler rotation in degrees. (0, 0, 0) for an invalid id. Reference
setRotation(entity, x, y, z)—Sets the local Euler rotation in degrees; a teleport on physics bodies, like setPosition. Reference
getScale(entity)x, y, zLocal scale. (1, 1, 1) for an invalid id. Reference
setScale(entity, x, y, z)—Sets the local scale. Reference
raycast(ox, oy, oz, dx, dy, dz [, maxDist])hit table / nilCasts a ray from the origin along the direction, up to maxDist (default 1000). Returns a flat table (Lua) / dict (Python) with the keys entity, x, y, z (hit point), nx, ny, nz (surface normal) and distance, or nil / None on a miss or without a physics world. horizon.physics.raycast answers the same question as ten return values and also reports the hit layer. Reference
setVelocity(entity, vx, vy, vz)—Drives the character controller if the entity has one, its rigid body otherwise. No-op without a physics world. Reference
isGrounded(entity)boolWhether the entity's character controller stands on ground. false without a physics world or a controller. Reference
setMaterialParam(entity, name, x [, y, z, w])boolSets the node-graph material parameter name on the entity's material at runtime. One to four numbers, omitted ones are 0, so a scalar parameter takes just x. false when the entity has no material or the parameter does not exist. Known issue: it writes the material asset, so every entity using that material changes with it (see Material). There is no horizon.material.* group. Reference
getMaterialParam(entity, name)x, y, z, wReads a material parameter back as four numbers, from the material asset (an override on the entity's Details panel is not seen); (0, 0, 0, 0) when it cannot be found. Reference
setUIText(entity, text)—Replaces the text of the entity's UI Text component. Reference
getUIText(entity)stringThe text of the UI Text component, "" without one. Reference
setUIColor(entity, r, g, b [, a])—Sets the element's primary colour: image tint, text colour or button normal colour, whichever exists (image first). a defaults to 1. Reference
getUIColor(entity)r, g, b, aThe primary colour in the same order; (1, 1, 1, 1) when there is none. Reference
setUIVisible(entity, visible)—Switches the UI element on or off, together with everything under it. Reference
isUIVisible(entity)boolWhether the UI element is switched on. Reference
setUIPosition(entity, x, y)—Moves the element, in canvas units. Reference
getUIPosition(entity)x, yThe element's position in canvas units. Reference
setUISize(entity, w, h)—Resizes the element, in canvas units. Reference
getUISize(entity)w, hThe element's size in canvas units. Reference
setUIMaterialParam(entity, name, x [, y, z, w])boolsetMaterialParam for the material of the entity's UI Image. false without one or for an unknown parameter. Reference
createWidget(assetPath)widgetInstantiates the widget asset at the content-relative path and returns its id, 0 on failure. The widget starts hidden; showWidget puts it on screen. HorizonCode does the same with the Create Widget node. Reference
destroyWidget(widget)—Removes the widget instance. Reference
showWidget(widget)—Shows the widget on screen. Reference
hideWidget(widget)—Hides it again; the instance and its state stay. Reference
setWidgetZOrder(widget, z)—Draw order among widgets: higher is on top. Reference
isWidgetVisible(widget)boolWhether the widget is currently shown. Reference
callWidgetFunction(widget, name)boolCalls a public function of the widget's graph by name. false for a private or unknown function. Reference
showCursor()—Releases the mouse and shows the pointer, e.g. for a menu. There is no horizon.cursor.* group. Reference
hideCursor()—Hides the pointer and grabs the mouse again for camera control. Reference
◆

Time control: horizon.time.setTimeScale(s) dilates the game clock — 0 pauses, 1 is normal, up to 5. horizon.time.deltaTime() is already scaled, so a script that integrates against it slows down, speeds up and freezes without a single change; horizon.time.unscaledDeltaTime() hands the real frame time to anything that must keep running through a pause. Scripts, physics, cameras, animation and the day-night cycle all follow the scaled clock; the UI keeps the real one, so a pause menu can always unpause the game again.

◆

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.

The other methods a Behavior can define, from on_begin_overlap to on_cheat_detected, are listed under Callbacks.

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.