All 582 engine functions in 42 groups, each with its Lua,
Python, HorizonCode and C++ name, plus the 35 flat
horizon.* functions and every callback a script receives.
Read this first
Calling Conventions
Every function on this page comes from one table inside the engine,
the API registry. A HorizonCode graph, a Lua script, a Python script
and C++ game code all call the same row, so a function behaves the
same in all four. What differs is how you spell the call and how
arguments travel. These rules apply to every row below.
One function, four names
Frontend
How physics.addImpulse is called
HorizonCode
the node Add Impulse (search the node menu by that name)
he::physics::addImpulse(entity, {0, 5, 0}), where a service helper exists
The C++ name listed with every row is the engine's own
function behind it. A native GameLogic module does not
link against the engine and cannot call it; it reaches a smaller set
(save, entity lookup, physics, input, content, networking and
anti-cheat) through the he:: service helpers, see
Native C++.
Function names are camelCase in Python too:
horizon.physics.addImpulse, not add_impulse.
Four groups are not reachable as
horizon.<group>.* from Lua and Python:
transform, material, cursor and
log. Scripts use the flat functions
for them instead (horizon.setPosition,
horizon.setMaterialParam, horizon.showCursor,
horizon.log). Rows without a flat twin, such as
transform.getWorldPosition, are HorizonCode and C++ only.
Each group below says which case it is.
Arguments and results
Arguments are passed flat, in the order the
signature lists them. A vector is spread into its numbers, it is
not a table or a tuple:
Type in the signature
Lua
Python
float
one number
one float (an int is accepted)
int, entity, ref, enum
one integer
one int
bool
true / false
True / False
string
one string
one str
vec2
2 numbers: x, y
2 numbers
vec3
3 numbers: x, y, z
3 numbers
color, vec4
4 numbers: r, g, b, a
4 numbers
struct
one table with __type
one dict with "__type"
Results spread the same way. One value comes back as that value,
several as Lua multiple return values or a Python tuple, none as
nil / None.
physics.raycast takes two vec3 and a
float (seven numbers) and returns a
bool, an entity, two vec3, a
float and an int (ten values):
ground.lua
local x, y, z = horizon.getPosition(self.entityId)
local hit, other, px, py, pz, nx, ny, nz, dist, layer =
horizon.physics.raycast(x, y, z, 0, -1, 0, 50)
if hit then
horizon.log("ground is " .. dist .. " below")
end
ground.py
x, y, z = horizon.getPosition(self.entity_id)
hit, other, px, py, pz, nx, ny, nz, dist, layer = \
horizon.physics.raycast(x, y, z, 0, -1, 0, 50)
if hit:
horizon.log(f"ground is {dist} below")
Too few arguments is an error: Lua raises
bad argument #n … (number expected, got no value), Python
raises IndexError. The one exception is a missing
bool in Lua, which silently reads as
false. Extra arguments are ignored in both languages.
Optional arguments exist only where a signature shows them in
brackets, which is only on the flat functions.
Entity ids and “Self”
An entity is passed as its integer id: self.entityId in
Lua, self.entity_id in Python, or whatever a lookup like
horizon.entity.findByName returned. Invalid ids are
tolerated: getters return a neutral default, setters do nothing.
▲
Always pass your own id. Parameters marked
self in HorizonCode mean “the calling object” when a graph
leaves the pin unconnected. A Lua or Python script is not such an
object, so there it does not apply: 0 is passed on
unchanged, the engine logs “Entity is empty and there is no
calling object”, and the call acts on entity 0, the scene root.
Write horizon.entity.getName(self.entityId), never
horizon.entity.getName(0).
List results (not yet usable from scripts)
▲
A few rows return a list, such as
physics.overlapSphere, physics.raycastAll,
fs.list, save.list or
scene.available, and process.run takes one.
HorizonCode and C++ handle these fine. The Lua and Python bridge does
not yet translate lists: such a call currently returns a single
0 or "" instead of the list, and the list
argument of process.run only accepts a string, which it
then drops. These rows are
marked list: HorizonCode/C++ only below. Until the bridge
is fixed, use them from a HorizonCode graph.
Events in the descriptions
The descriptions come from the editor and name HorizonCode events
such as On Timer or On Http Response. A script
receives only the events listed under Callbacks;
those are linked where they appear. The others (On Http
Response, On File Changed, On Menu Item,
On Tray Item, On Row Bind, On Window Closed,
On Dismissed, On Selection Changed, On Right
Clicked) have no Lua or Python callback today. Where a query
exists, poll it instead, for example horizon.http.done(ticket)
each frame after horizon.http.get. Otherwise handle that
part in a HorizonCode graph.
Actions and queries
Every row is either an action (it changes something:
moves, spawns, plays, writes) or a query (it only
reads and returns). In HorizonCode an action is a node with white
execution pins that runs in sequence, a query is a data node without
them. In a script both are ordinary calls, but the tag tells you
whether a call is safe to repeat freely.
Project permissions
A few rows reach outside the game and only run when the project
allows it, under Project Settings ▸ Permissions. Without the
permission the call does nothing and returns its “failed” value
(false, 0, an empty result), and the log says
which permission is missing. The settings travel into the exported
app, and they bind play mode in the editor the same way.
Permission
Rows
Files outside the project
fs.* with an absolute path that the user did not pick in a file dialog, print.toPdf, db.open
Dialogs and the clipboard need no permission. A path the user picked
in a file dialog stands in for Files outside the project, but
only for the fs.* rows: print.toPdf and
db.open need the permission for every path, a relative one
inside the project included (build 7d49d44f). The log line about a
missing permission is written for the first refused call only; further
refusals of any row within the next ten seconds are not logged.
Remote calls take any arguments
horizon.net.callServer(entity, name, …),
horizon.net.callClient(player, entity, name, …) and
horizon.net.callAllClients(entity, name, …) accept any
number of extra arguments after the function name and pass them on.
Their types are taken from the values themselves: a boolean, an
integer, any other number, a string, or a list/table of those. This
is the one place where the registry signature (which has no slot for
them) does not describe the script call. The receiving side is a
method of that name on the target entity's script, see
Callbacks.
Your own enums and structs
Project Types in Scripts
Enums and structs you create as assets in the Content Browser are
available to scripts under horizon.enums and
horizon.structs, built when the script runtime starts.
What
Lua
Python
Enum entry (an integer)
horizon.enums.Weapon.Sword
horizon.enums.Weapon.Sword
New struct with its default values
horizon.structs.Loadout() → table
horizon.structs.Loadout() → dict
A struct value is a plain table / dict: one key per field plus
__type, the path of the struct asset. Keep
__type when you hand the value back, it is how the engine
knows which struct it is. Missing fields keep their defaults, unknown
keys are ignored. Inside a struct, fields are single values rather than
spread: a vec2 is {x, y} / [x, y],
a colour {r, g, b, a}, a transform
{pos = {…}, rot = {…}, scl = {…}}, a list a sequence, and a
nested struct a nested table. A map field is a Python dict
(it keeps its order); in Lua it is a table plus a __keys
list that holds the order. Structs go in and out of
save.setStruct / save.getStruct this way.
Lua and Python only
Flat horizon.* Functions
Besides the groups, horizon carries 35 functions directly:
horizon.setPosition(...) rather than
horizon.transform.setPosition(...). They predate the
registry, exist only in Lua and Python, and keep a signature of their
own: some arguments are optional (in brackets), and
raycast returns a table instead of separate values. Where
a registry row does the same job it is linked; for
transform, material, cursor and
log these functions are the only way in from a script.
Log and entities
Function
Returns
Description
horizon.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. Same job as log.
horizon.getName(entity)
string
The entity's name, "" for an invalid id. Same job as entity.getName.
horizon.spawn(parent [, name])
entity
Creates 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. Same job as entity.spawn.
horizon.destroy(entity)
—
Removes the entity; an invalid id is ignored. Check the id first: in a script 0 is the scene root, not “nothing”. Same job as entity.destroy.
Transform
Function
Returns
Description
horizon.getPosition(entity)
x, y, z
Local 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. Same job as transform.getPosition.
horizon.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. Same job as transform.setPosition.
horizon.getRotation(entity)
x, y, z
Local Euler rotation in degrees. (0, 0, 0) for an invalid id. Same job as transform.getRotation.
horizon.setRotation(entity, x, y, z)
—
Sets the local Euler rotation in degrees; a teleport on physics bodies, like setPosition. Same job as transform.setRotation.
horizon.getScale(entity)
x, y, z
Local scale. (1, 1, 1) for an invalid id. Same job as transform.getScale.
horizon.raycast(ox, oy, oz, dx, dy, dz [, maxDist])
hit table / nil
Casts 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. Same job as physics.raycast.
horizon.setVelocity(entity, vx, vy, vz)
—
Drives the character controller if the entity has one, its rigid body otherwise. No-op without a physics world. Same job as physics.setVelocity.
horizon.isGrounded(entity)
bool
Whether the entity's character controller stands on ground. false without a physics world or a controller. Same job as physics.isGrounded.
Material
Function
Returns
Description
horizon.setMaterialParam(entity, name, x [, y, z, w])
bool
Sets 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. Same job as material.setParam.
horizon.getMaterialParam(entity, name)
x, y, z, w
Reads 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. Same job as material.getParam.
In-game UI elements
These act on entities that carry UI components (a UI canvas in the scene). Widgets built in the Widget Designer are a different thing, see the next table.
Function
Returns
Description
horizon.setUIText(entity, text)
—
Replaces the text of the entity's UI Text component. Same job as ui.setText.
horizon.getUIText(entity)
string
The text of the UI Text component, "" without one. Same job as ui.getText.
horizon.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. Same job as ui.setColor.
horizon.getUIColor(entity)
r, g, b, a
The primary colour in the same order; (1, 1, 1, 1) when there is none. Same job as ui.getColor.
horizon.setUIVisible(entity, visible)
—
Switches the UI element on or off, together with everything under it. Same job as ui.setVisible.
horizon.isUIVisible(entity)
bool
Whether the UI element is switched on. Same job as ui.getVisible.
horizon.setUIPosition(entity, x, y)
—
Moves the element, in canvas units. Same job as ui.setPosition.
horizon.getUIPosition(entity)
x, y
The element's position in canvas units. Same job as ui.getPosition.
horizon.setUISize(entity, w, h)
—
Resizes the element, in canvas units. Same job as ui.setSize.
horizon.getUISize(entity)
w, h
The element's size in canvas units. Same job as ui.getSize.
horizon.setUIMaterialParam(entity, name, x [, y, z, w])
bool
setMaterialParam for the material of the entity's UI Image. false without one or for an unknown parameter. Same job as ui.setMaterialParam.
Widgets
Widgets are instances of Widget Designer assets. They live outside the entity world and are addressed by the widget id createWidget returns, not by an entity id. The rest of the widget API (lists, animation, popups, children) is horizon.widget.*.
Function
Returns
Description
horizon.createWidget(assetPath)
widget
Instantiates 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.
horizon.destroyWidget(widget)
—
Removes the widget instance.
horizon.showWidget(widget)
—
Shows the widget on screen.
horizon.hideWidget(widget)
—
Hides it again; the instance and its state stay.
horizon.setWidgetZOrder(widget, z)
—
Draw order among widgets: higher is on top. Same job as widget.setZOrder.
horizon.isWidgetVisible(widget)
bool
Whether the widget is currently shown. Same job as widget.isVisible.
horizon.callWidgetFunction(widget, name)
bool
Calls a public function of the widget's graph by name. false for a private or unknown function. Same job as widget.callFunction.
Cursor
Function
Returns
Description
horizon.showCursor()
—
Releases the mouse and shows the pointer, e.g. for a menu. There is no horizon.cursor.* group. Same job as cursor.setVisible.
horizon.hideCursor()
—
Hides the pointer and grabs the mouse again for camera control. Same job as cursor.setVisible.
What the engine calls on your script
Callbacks
A Lua script returns a table of functions, a Python script defines one
subclass of horizon.Behavior. The engine calls the
functions below on it by name; every one is optional, and a missing one
is simply skipped. Lua names are camelCase, Python names snake_case.
Receives says who gets the call: this entity
means only the script on the entity concerned, every script
means every Lua and Python instance in the session, so check the
arguments before reacting. Results of many calls on this page arrive
here: timers, input actions, network events, anti-cheat reports.
Lifecycle
Lua / Python
Receives
When
onStart(self) on_start(self)
this entity
Once, when the script starts: at the start of play mode, in a packaged game when its scene loads, and for scripts on entities that arrive later (an additive zone). Exposed properties are already set.
onUpdate(self, dt) on_update(self, dt)
this entity
Every frame. dt is the scaled frame time in seconds (0 while the game is paused through horizon.time.setTimeScale(0)).
Physics contacts
Both entities of a contact get the event, each with the other's id. A contact where either side no longer exists is dropped entirely. A trigger contact fires both pairs: onCollisionEnter and onBeginOverlap, then onCollisionExit and onEndOverlap.
Actions are the ones defined in the project's input mapping. These go to every script instance, not only to the player's entity; pick the action by name. For polling instead of events use horizon.input.actionDown and friends.
Every frame, for each 2D axis action. Note the lower-case d in the Python name.
Timers
Lua / Python
Receives
When
onTimer(self, handle) on_timer(self, handle)
every script
A timer started with horizon.timer.after or horizon.timer.every went off. handle is the value those calls returned; compare it to tell your timers apart, because every instance receives every timer. Timers run on real time, a paused game does not stretch them.
Sent to scripts on entities with an interactive UI element (a button, or any element carrying a script) under the mouse.
Lua / Python
Receives
When
onClick(self) on_click(self)
this entity
The element was clicked: pressed and released over it.
onHoverEnter(self) on_hover_enter(self)
this entity
The pointer moved onto the element.
onHoverExit(self) on_hover_exit(self)
this entity
The pointer left it.
Multiplayer session
Every script instance on the machine concerned receives these. Each event happens on one side only: the host hears players come and go, a client hears its own connection. Measured with host and client in Lua and Python.
Lua / Python
Receives
When
onConnected(self) on_connected(self)
every script
Client only: the connection is up and the host's world has arrived (net.status() is 3). A client gets no onSessionStarted.
Client only: the host is gone or removed us; the status is now 4. reason: 0 left, 1 connection lost, 2 kicked, 3 refused, 4 other engine version, 5 other project. A kick with anti-cheat off arrives as 1. Not sent for a leave() of our own, and not followed by onSessionEnded.
Host only: a player joined and has the world. player is their player number (2, 3, …), the one net.playerName takes, not an anti-cheat connection number.
Host only: a player left, was kicked or timed out. net.playerName(player) is already empty.
onSessionStarted(self) on_session_started(self)
every script
Host only: net.host opened a session.
onSessionEnded(self) on_session_ended(self)
every script
net.leave() ended the session on this machine, on the host also when hosting again. Not sent to a client that was removed or lost the host (that is onDisconnected).
Replication and remote calls
Lua / Python
Receives
When
onRep_<var>(self, old) on_rep_<var>(self, old)
this entity
A replicated variable (declared with horizon.net.declareVarInt and its siblings) arrived with a new value on this client. Never called on the host, which set the value itself. <var> is the variable's name verbatim (health gives onRep_health / on_rep_health); old is the previous value: in Python a list [x, y, z] for a Vector, in Lua the number 0 for a Vector (known issue). Read the new one with horizon.net.getVarInt and its siblings. Only on the entity whose variable changed, and only for variables declared with notify. A client that wrote the variable itself gets it again when the host's value comes back.
<name>(self, …) <name>(self, …)
this entity
A remote call from horizon.net.callServer, callClient or callAllClients runs the method of exactly that name on the target entity's script, with the caller's extra arguments. No renaming between languages: a Lua takeDamage is reached by a Python caller as takeDamage. On the host the method runs only for a call from a client: offline and on the host itself these rows reach a HorizonCode class only, see Remote calls.
The host's anti-cheat filed a report. Read it with horizon.anticheat.reportLevel, reportRule, reportPlayer … and override the host's reaction with horizon.anticheat.respond in the same frame. On a client it fires once, when the host is removing it (reportPlayer 0). Never fires in build 7d49d44f, see Anti-Cheat.
Pure functions over numbers: none of them changes anything, so they
can be called anywhere and as often as needed. In a script the
language's own math library does most of this too, and
with more precision. horizon.math is for the results a
HorizonCode graph would get, and for what the languages lack:
lerp, clamp, the vector helpers and 32-bit
bit operations.
Angles.sin, cos,
tan and atan2 work in radians, while the
engine's rotations are in degrees.
radians and degrees convert between them;
horizon.math.sin(90) is 0.894, not 1.
Precision. Every float crosses into
the engine as a 32-bit number, about seven significant digits:
normalize3(3, 0, 4) returns
0.6000000238…, 0, 0.8000000119…. Compare results with a
tolerance, not with ==.
Vectors are spread like every other argument:
length(x, y), distance3(ax, ay, az, bx, by, bz).
normalize3 and cross return three numbers.
Bit operations (bitAnd to
shiftRight) take and return 32-bit signed integers.
Lua accepts an integer or a float with no fraction (6.0);
6.5 raises number has no integer representation.
Python accepts only an int; any float raises
TypeError. Results wrap at 32 bits:
shiftLeft(1, 31) is −2147483648.
Example: math.dot3
view_cone.lua
local M = {}
-- True when target lies within coneDeg degrees around the direction
-- (fx, fy, fz) this entity faces.
function M.canSee(self, target, fx, fy, fz, coneDeg)
local x, y, z = horizon.getPosition(self.entityId)
local tx, ty, tz = horizon.getPosition(target)
local dx, dy, dz = horizon.math.normalize3(tx - x, ty - y, tz - z)
local limit = horizon.math.cos(horizon.math.radians(coneDeg / 2))
return horizon.math.dot3(dx, dy, dz, fx, fy, fz) >= limit
end
return M
view_cone.py
import horizon
class ViewCone(horizon.Behavior):
def can_see(self, target, fx, fy, fz, cone_deg):
"""True when target lies within cone_deg degrees around (fx, fy, fz)."""
x, y, z = horizon.getPosition(self.entity_id)
tx, ty, tz = horizon.getPosition(target)
dx, dy, dz = horizon.math.normalize3(tx - x, ty - y, tz - z)
limit = horizon.math.cos(horizon.math.radians(cone_deg / 2))
return horizon.math.dot3(dx, dy, dz, fx, fy, fz) >= limit
Function
Returns
Description
sin(x: float)
result: float
The sine of an angle given in RADIANS. Feed degrees through To Radians first — this is the one that catches everyone. Lua/Python horizon.math.sin · HorizonCode Sine · C++ HE::api::math::sin · query
cos(x: float)
result: float
The cosine of an angle in radians. Paired with Sine it turns an angle into a direction: cos for X, sin for Z. Lua/Python horizon.math.cos · HorizonCode Cosine · C++ HE::api::math::cos · query
tan(x: float)
result: float
The tangent of an angle in radians. Runs off to infinity near 90 degrees, so keep the input away from it. Lua/Python horizon.math.tan · HorizonCode Tangent · C++ HE::api::math::tan · query
sqrt(x: float)
result: float
The square root. A negative input has none and yields NaN, which then spreads silently through everything downstream — clamp at 0 first if the value can go negative. Lua/Python horizon.math.sqrt · HorizonCode Square Root · C++ HE::api::math::sqrt · query
abs(x: float)
result: float
Drops the sign: -3 and 3 both come out as 3. How a distance is made out of a difference. Lua/Python horizon.math.abs · HorizonCode Absolute · C++ HE::api::math::abs · query
floor(x: float)
result: float
Rounds DOWN to a whole number: 2.9 becomes 2, and -2.1 becomes -3. Lua/Python horizon.math.floor · HorizonCode Floor · C++ HE::api::math::floor · query
ceil(x: float)
result: float
Rounds UP to a whole number: 2.1 becomes 3. Lua/Python horizon.math.ceil · HorizonCode Ceil · C++ HE::api::math::ceil · query
round(x: float)
result: float
Rounds to the nearest whole number, halves away from zero: 2.5 becomes 3, -2.5 becomes -3. Lua/Python horizon.math.round · HorizonCode Round · C++ HE::api::math::round · query
sign(x: float)
result: float
-1 for a negative number, 1 for a positive one, 0 for zero. Which way, without how far. Lua/Python horizon.math.sign · HorizonCode Sign · C++ HE::api::math::sign · query
radians(x: float)
result: float
Converts degrees to radians. The engine's rotations are in degrees and the trigonometry is in radians, so this sits between them. Lua/Python horizon.math.radians · HorizonCode Degrees to Radians · C++ HE::api::math::radians · query
degrees(x: float)
result: float
Converts radians back to degrees — the direction to use before writing an angle into a transform. Lua/Python horizon.math.degrees · HorizonCode Radians to Degrees · C++ HE::api::math::degrees · query
pow(base: float, exp: float)
result: float
Raises the base to the exponent. Lua/Python horizon.math.pow · HorizonCode Power · C++ HE::api::math::pow · query
mod(a: float, b: float)
result: float
The remainder after division, for doing something every Nth item. It keeps the sign of A: -1 mod 360 is -1, not 359, so wrapping an angle into 0..360 takes a second Modulo of the result plus 360. A divisor of 0 is a runtime error: it is logged and the result is 0 rather than a NaN. Lua/Python horizon.math.mod · HorizonCode Modulo · C++ HE::api::math::mod · query
bitAnd(a: int, b: int)
result: int
Keeps only the bits set in BOTH integers. Testing a flag: AND the flags with the one you ask about, and the result is non-zero when it is set. Not the bool And — this works on the bits of an Int. Lua/Python horizon.math.bitAnd · HorizonCode Bitwise AND · C++ HE::api::math::bitAnd · query
bitOr(a: int, b: int)
result: int
Sets every bit set in EITHER integer. Adding a flag to a set of flags. Lua/Python horizon.math.bitOr · HorizonCode Bitwise OR · C++ HE::api::math::bitOr · query
bitXor(a: int, b: int)
result: int
Sets the bits that differ between the two integers. Toggling a flag: XOR with it once turns it on, XOR again turns it off. Lua/Python horizon.math.bitXor · HorizonCode Bitwise XOR · C++ HE::api::math::bitXor · query
bitNot(x: int)
result: int
Flips every bit of the integer (two's complement, so 0 becomes -1). Clearing a flag: AND with the NOT of it. Lua/Python horizon.math.bitNot · HorizonCode Bitwise NOT · C++ HE::api::math::bitNot · query
shiftLeft(x: int, count: int)
result: int
Moves the bits of X toward the high end by Count places, filling with zeros: each place doubles the value (1 shifted by 3 is 8). Packing two small numbers into one Int. A negative count shifts right instead; a count of 32 or more shifts everything out and gives 0. Lua/Python horizon.math.shiftLeft · HorizonCode Shift Left · C++ HE::api::math::shiftLeft · query
shiftRight(x: int, count: int)
result: int
Moves the bits of X toward the low end by Count places: each place halves the value, rounding toward minus infinity. The sign is kept (-8 shifted by 1 is -4), so a negative value never turns positive. A negative count shifts left instead; a count of 32 or more leaves 0 (or -1 for a negative value). Lua/Python horizon.math.shiftRight · HorizonCode Shift Right · C++ HE::api::math::shiftRight · query
atan2(y: float, x: float)
result: float
The angle of the vector (X, Y) in radians, over the full circle. This is how a direction becomes a heading, and it gets the quadrant right where plain arctangent cannot. Lua/Python horizon.math.atan2 · HorizonCode Atan2 · C++ HE::api::math::atan2 · query
min(a: float, b: float)
result: float
The smaller of the two values. Lua/Python horizon.math.min · HorizonCode Min · C++ HE::api::math::min · query
max(a: float, b: float)
result: float
The larger of the two values. Max with 0 is the usual way to stop a value going negative. Lua/Python horizon.math.max · HorizonCode Max · C++ HE::api::math::max · query
clamp(x: float, lo: float, hi: float)
result: float
Keeps a value inside Lo..Hi. Below Lo it becomes Lo, above Hi it becomes Hi. Lua/Python horizon.math.clamp · HorizonCode Clamp · C++ HE::api::math::clamp · query
lerp(a: float, b: float, t: float)
result: float
Blends from A to B by T: 0 gives A, 1 gives B, 0.5 the middle. T outside 0..1 is not clamped — it keeps going past either end. Lua/Python horizon.math.lerp · HorizonCode Lerp · C++ HE::api::math::lerp · query
length(v: vec2)
result: float
The length of a 2D vector. Lua/Python horizon.math.length · HorizonCode Length (Vec2) · C++ HE::api::math::length · query
distance(a: vec2, b: vec2)
result: float
The distance between two 2D points. Lua/Python horizon.math.distance · HorizonCode Distance (Vec2) · C++ HE::api::math::distance · query
length3(v: vec3)
result: float
The length of a 3D vector — how far it reaches, regardless of direction. Lua/Python horizon.math.length3 · HorizonCode Length (Vec3) · C++ HE::api::math::length3 · query
distance3(a: vec3, b: vec3)
result: float
The distance between two 3D points in metres. Lua/Python horizon.math.distance3 · HorizonCode Distance (Vec3) · C++ HE::api::math::distance3 · query
normalize3(v: vec3)
result: vec3
The same direction at length 1. A zero vector has no direction and stays zero rather than becoming a NaN. Lua/Python horizon.math.normalize3 · HorizonCode Normalize (Vec3) · C++ HE::api::math::normalize3 · query
dot3(a: vec3, b: vec3)
result: float
The dot product: how much two directions agree. 1 means the same way, 0 perpendicular, -1 opposite. With normalised vectors it is the cosine of the angle between them, which is how a field-of-view check is written. Lua/Python horizon.math.dot3 · HorizonCode Dot Product · C++ HE::api::math::dot3 · query
cross(a: vec3, b: vec3)
result: vec3
A vector perpendicular to both inputs. Forward crossed with up gives right, which is how a local axis is built from a facing. Lua/Python horizon.math.cross · HorizonCode Cross Product · C++ HE::api::math::cross · query
Text functions with the same results a HorizonCode graph gets. Three
rules decide most surprises:
Positions count from 0, also in Lua, whose own
string.sub and string.find count from 1.
find returns −1 when the piece is not there.
Lengths and positions are bytes. Text is UTF-8:
letters, digits and punctuation from plain ASCII take one byte, an
accented or non-Latin letter two to four. length("ä") is
2, and a substring can cut such a letter in half.
toUpper and toLower change only A to Z.
Comparisons are exact.equals,
contains, find, startsWith and
endsWith are case-sensitive; lower both sides first when
case should not matter.
An empty piece is found everywhere: contains(s, "") is
true and find(s, "") is 0, while
replace(s, "", x) returns s unchanged. A
negative start in substring uses up part of
count, so substring("hello", -2, 4) is
"he". toNumber reads the number at the start of
the text and ignores the rest: "3.5abc" gives 3.5,
" 42 " 42, "1e3" 1000 and "0x10"
16. The result is a 32-bit float, exact for whole numbers only up to
16,777,216.
Example: string.find
settings_line.lua
local M = {}
-- "volume = 75" -> "volume", 75. Positions count from 0.
local function parse(line)
local eq = horizon.string.find(line, "=")
if eq < 0 then return nil end
local key = horizon.string.trim(horizon.string.substring(line, 0, eq))
local rest = horizon.string.substring(line, eq + 1, horizon.string.length(line))
return key, horizon.string.toNumber(rest)
end
function M.onStart(self)
local key, value = parse("volume = 75")
if key then horizon.log(key .. " is " .. value) end
end
return M
settings_line.py
import horizon
def parse(line):
"""'volume = 75' -> ('volume', 75.0). Positions count from 0."""
eq = horizon.string.find(line, "=")
if eq < 0:
return None
key = horizon.string.trim(horizon.string.substring(line, 0, eq))
rest = horizon.string.substring(line, eq + 1, horizon.string.length(line))
return key, horizon.string.toNumber(rest)
class Settings(horizon.Behavior):
def on_start(self):
parsed = parse("volume = 75")
if parsed:
key, value = parsed
horizon.log(f"{key} is {value}")
Function
Returns
Description
length(s: string)
length: int
How long the text is in bytes. Plain letters and digits are one byte each, but an accented or non-Latin letter takes two to four ("ä" is 2). Lua/Python horizon.string.length · HorizonCode String Length · C++ HE::api::str::length · query
equals(a: string, b: string)
result: bool
Are the two texts the same? Exact and case-sensitive — put To Lower on both sides when case should not matter. This is what to branch on after On Menu Item or On Tray Item: the Equals node next door compares NUMBERS, and two texts arriving there both count as 0, so every id would match every other one. Lua/Python horizon.string.equals · HorizonCode String Equals · C++ HE::api::str::equals · query
substring(s: string, start: int, count: int)
result: string
Count bytes starting at Start (counting from 0). A range past the end is clamped rather than an error. Lua/Python horizon.string.substring · HorizonCode Substring · C++ HE::api::str::substring · query
contains(s: string, needle: string)
contains: bool
Does the text contain this piece? Case-sensitive. Lua/Python horizon.string.contains · HorizonCode String Contains · C++ HE::api::str::contains · query
find(s: string, needle: string)
index: int
Where the piece first occurs, counting from 0, or -1 when it does not. In Lua, add 1 before handing the result to string.sub. Lua/Python horizon.string.find · HorizonCode String Find · C++ HE::api::str::find · query
replace(s: string, from: string, to: string)
result: string
Replaces every occurrence of From with To. Lua/Python horizon.string.replace · HorizonCode String Replace · C++ HE::api::str::replace · query
toUpper(s: string)
result: string
The text in upper case. Only A to Z change; accented letters stay as they are. Lua/Python horizon.string.toUpper · HorizonCode To Upper · C++ HE::api::str::toUpper · query
toLower(s: string)
result: string
The text in lower case. Only A to Z change; accented letters stay as they are. Lua/Python horizon.string.toLower · HorizonCode To Lower · C++ HE::api::str::toLower · query
trim(s: string)
result: string
Removes whitespace from both ends — what a text input needs before it is compared to anything. Lua/Python horizon.string.trim · HorizonCode Trim · C++ HE::api::str::trim · query
startsWith(s: string, prefix: string)
result: bool
Does the text begin with this piece? Lua/Python horizon.string.startsWith · HorizonCode Starts With · C++ HE::api::str::startsWith · query
endsWith(s: string, suffix: string)
result: bool
Does the text end with this piece? Lua/Python horizon.string.endsWith · HorizonCode Ends With · C++ HE::api::str::endsWith · query
toNumber(s: string)
number: float
Parses the text as a number. Text that is not one yields 0, so this cannot tell "0" from nonsense — validate first if that matters. Lua/Python horizon.string.toNumber · HorizonCode To Number · C++ HE::api::str::toNumber · query
One random generator for the whole game, shared by every Lua and
Python script and every HorizonCode graph. It starts from the same
built-in seed at every launch, so a game that never calls
seed draws the same numbers every time it
starts. Seed it once at start: from the clock when every run
should differ, from a constant when a bug should happen again.
Because it is shared, a seed restarts the sequence for
everyone, and another script drawing in between changes the numbers
you get. A repeated run matches only if the same scripts draw in the
same order. Lua's math.random and Python's
random module are separate generators and
seed does not touch them.
All five are actions, because every call draws a new number (see
Actions and queries). range can
return min but not max;
rangeInt includes both ends. Either one swaps the bounds
when max is smaller than min.
chance treats anything below 0 as 0 and anything above 1
as 1.
Example: random.seed
dice.lua
local M = {}
function M.onStart(self)
-- Without this, every launch rolls the same numbers. Seed in one
-- script only: every seed restarts the sequence for all of them.
horizon.random.seed(os.time())
end
function M.roll(self)
local eyes = horizon.random.rangeInt(1, 6) -- 1 to 6, both included
local critical = horizon.random.chance(0.05) -- one in twenty
return eyes, critical
end
return M
dice.py
import time
import horizon
class Dice(horizon.Behavior):
def on_start(self):
# Without this, every launch rolls the same numbers. Seed in one
# script only: every seed restarts the sequence for all of them.
horizon.random.seed(int(time.time()))
def roll(self):
eyes = horizon.random.rangeInt(1, 6) # 1 to 6, both included
critical = horizon.random.chance(0.05) # one in twenty
return eyes, critical
Function
Returns
Description
seed(seed: int)
—
Restarts the random sequence from this seed, so a run can be repeated exactly. Without it the sequence starts from the same built-in seed at every launch, so a game that should differ each time seeds once at start, from the clock. Lua/Python horizon.random.seed · HorizonCode Seed Random · C++ HE::api::random::seed · action
value()
value: float
A random number from 0 up to (but not including) 1. An exec node, not a pure one, precisely because it changes every call: a pure node would be re-evaluated per reader and hand two of them different numbers. Lua/Python horizon.random.value · HorizonCode Random Value · C++ HE::api::random::value · action
range(min: float, max: float)
value: float
A random number between Min and Max. Can return min, never max. Lua/Python horizon.random.range · HorizonCode Random Range · C++ HE::api::random::range · action
rangeInt(min: int, max: int)
value: int
A random whole number from Min to Max, both ends included. Lua/Python horizon.random.rangeInt · HorizonCode Random Range (Int) · C++ HE::api::random::rangeInt · action
chance(p: float)
value: bool
True with probability P (0 never, 1 always). The coin flip, spelled out. Lua/Python horizon.random.chance · HorizonCode Random Chance · C++ HE::api::random::chance · action
The game clock. It advances once per frame, and every query gives the
same answer for the rest of that frame. In the editor it runs only
during Play and starts from 0 each time. For the time of day use
Date and Time; to be called back later, use a
Timer.
deltaTime and elapsed are scaled:
they follow the time scale and stand still while the game is paused
or in a hit stop. unscaledDeltaTime and
unscaledElapsed follow real time, which is what a pause
menu or a UI animation needs.
Three separate controls stop or slow the clock, and
effectiveScale is their result:
Time scale (setTimeScale), limited to
0…5. A larger value becomes 5, a negative one 0.
Pause (pause / resume).
It also silences input actions, except the ones with Fires
while the game is paused ticked in the Input asset. Tick it on
the key that opens the pause menu, or that key cannot close it
again. All scripts and
graphs share one pause, so a resume anywhere lifts a
pause from anywhere. The pause the window sets when
it loses focus is separate; resume cannot lift it.
Hit stop (hitStop): a freeze for a
stretch of real time. Input keeps arriving, and
isPaused stays false.
Pause and hit stop leave the time scale alone, so the game resumes at
the speed it had. When a game loads another scene, the scale goes back
to 1 and the script pause and any hit stop are lifted;
elapsed and frameCount keep counting.
Example: time.pause
pause_menu.lua
local M = {}
-- "Pause" is an input action with "Fires while the game is paused"
-- ticked; without it the second press would never arrive.
function M.onInputPressed(self, name)
if name ~= "Pause" then return end
if horizon.time.isPaused() then
horizon.time.resume()
else
horizon.time.pause()
end
end
function M.onCollisionEnter(self, other)
horizon.time.hitStop(0.1) -- a landed hit freezes for a tenth of a second
end
return M
pause_menu.py
import horizon
class PauseMenu(horizon.Behavior):
# "Pause" is an input action with "Fires while the game is paused"
# ticked; without it the second press would never arrive.
def on_input_pressed(self, name):
if name != "Pause":
return
if horizon.time.isPaused():
horizon.time.resume()
else:
horizon.time.pause()
def on_collision_enter(self, other):
horizon.time.hitStop(0.1) # a landed hit freezes for a tenth of a second
Function
Returns
Description
deltaTime()
dt: float
Seconds since the previous frame, scaled by the time scale. Multiply anything per-second by it and the motion stays the same on a fast machine and a slow one. Lua/Python horizon.time.deltaTime · HorizonCode Delta Time · C++ HE::api::time::deltaTime · query
elapsed()
seconds: float
Seconds since the game started, scaled. Lua/Python horizon.time.elapsed · HorizonCode Elapsed Time · C++ HE::api::time::elapsed · query
frameCount()
frame: int
How many frames have been drawn. For "every Nth frame" work; frames are not a unit of time, so do not pace anything with it. Lua/Python horizon.time.frameCount · HorizonCode Frame Count · C++ HE::api::time::frameCount · query
setTimeScale(scale: float)
—
Slows down or speeds up game time: 0.5 is half speed, 0 is slow motion down to a standstill, 1 is normal. This is the scale you asked for; a Pause Game or a Hit Stop overrides it without erasing it. Real-time Delay nodes and Unscaled Delta Time are deliberately unaffected. Limited to 0…5. Lua/Python horizon.time.setTimeScale · HorizonCode Set Time Scale · C++ HE::api::time::setTimeScale · action
timeScale()
scale: float
The time scale that was asked for — what Set Time Scale last wrote, even while a pause or a hit stop is holding the clock at zero. Effective Time Scale is the one actually in effect. Lua/Python horizon.time.timeScale · HorizonCode Get Time Scale · C++ HE::api::time::timeScale · query
unscaledDeltaTime()
dt: float
The same, but ignoring Set Time Scale — so a UI animation keeps running at normal speed while the game is in slow motion or paused. Lua/Python horizon.time.unscaledDeltaTime · HorizonCode Unscaled Delta Time · C++ HE::api::time::unscaledDeltaTime · query
unscaledElapsed()
seconds: float
Real seconds since the game started — it keeps counting through slow motion, a pause and a hit stop, which is what a pause menu needs to time anything at all. Lua/Python horizon.time.unscaledElapsed · HorizonCode Unscaled Elapsed Time · C++ HE::api::time::unscaledElapsed · query
pause()
—
Pauses the game: Delta Time goes to zero and the player's input stops being delivered, which is what makes a pause menu a pause menu. The time scale is left alone, so resuming returns to the slow motion it came from. Input actions stop arriving unless they have Fires while the game is paused ticked. Lua/Python horizon.time.pause · HorizonCode Pause Game · C++ HE::api::time::pause · action
resume()
—
Lifts the pause this script asked for. It cannot lift the one the window put in place when the game lost focus — those are separate reasons, and the game runs again only once none of them is left. Lua/Python horizon.time.resume · HorizonCode Resume Game · C++ HE::api::time::resume · action
isPaused()
paused: bool
True while the game is paused. Note it stays FALSE during a hit stop: a freeze stops the world, it does not stop the player being heard. Lua/Python horizon.time.isPaused · HorizonCode Is Paused · C++ HE::api::time::isPaused · query
hitStop(seconds: float)
—
Freezes the game for a moment of real time — 0.08 to 0.12 seconds is the usual weight for a landed hit. It returns to exactly the speed it came from, slow motion included, and input is never swallowed. Triggering it again takes the longer of the two windows rather than adding them up. 0 or less does nothing, and does not end a hit stop that is running. Lua/Python horizon.time.hitStop · HorizonCode Hit Stop · C++ HE::api::time::hitStop · action
isFrozen()
frozen: bool
True while a Hit Stop is still running. For the effect that spawned it, not for gameplay logic. Lua/Python horizon.time.isFrozen · HorizonCode Is Frozen · C++ HE::api::time::isFrozen · query
effectiveScale()
scale: float
What actually multiplies Delta Time this frame: the time scale, or zero while a pause or a hit stop is on. The number to put on a debug HUD. Lua/Python horizon.time.effectiveScale · HorizonCode Effective Time Scale · C++ HE::api::time::effectiveScale · query
A timer calls back instead of waiting. after and
every return a handle at once and the script carries on;
when the time is up the engine calls onTimer(self, handle)
(Python on_timer, see Callbacks) on
every script instance, and fires the GameInstance's
On Timer event. Keep the handle and compare it, because the
other scripts' timers arrive too.
Timers run on real time: pause, slow motion and hit
stop do not stretch them. To wait in game time, count
horizon.time.deltaTime() in onUpdate.
Handles count up from 1. 0 means the timer did not
start: the interval was shorter than 0.001 seconds or not a
number, or 64 timers were already running.
A timer belongs to the session, not to the script or entity that
started it. Destroying the entity or loading another scene does not
stop it; stopping Play or quitting the game does.
cancelAll stops every timer, other scripts' and the
GameInstance's included. To stop only your own, use
cancel(handle).
Example: timer.every
reminders.lua
local M = {}
function M.onStart(self)
self.autosave = horizon.timer.every(30)
self.hint = horizon.timer.after(5)
end
function M.onTimer(self, handle)
if handle == self.autosave then
horizon.log("autosave")
elseif handle == self.hint then
horizon.log("Press E to open the door")
end
-- any other handle belongs to another script
end
return M
reminders.py
import horizon
class Reminders(horizon.Behavior):
def on_start(self):
self.autosave = horizon.timer.every(30)
self.hint = horizon.timer.after(5)
def on_timer(self, handle):
if handle == self.autosave:
horizon.log("autosave")
elif handle == self.hint:
horizon.log("Press E to open the door")
# any other handle belongs to another script
Function
Returns
Description
after(seconds: float)
handle: int
Fires On Timer once, after the seconds you give it, and hands back a handle so you can tell it apart from other timers (and cancel it before it goes off). Unlike Delay it does not park the graph: everything carries on and the event arrives later. 0 means it did not start. 0 also when 64 timers are already running. Lua/Python horizon.timer.after · HorizonCode Timer After · C++ HE::api::timer::after · action
every(seconds: float)
handle: int
The same, but it keeps firing until you cancel it — a clock, an autosave, a poll. If the application was busy or away it fires ONCE when it comes back, not once for every tick it missed. 0 when the interval is under 0.001 seconds or 64 timers are already running. Lua/Python horizon.timer.every · HorizonCode Timer Every · C++ HE::api::timer::every · action
cancel(handle: int)
ok: bool
Stops a timer, using the handle it gave you. Ok is false when there was no such timer running — it already fired, or it was cancelled before. Lua/Python horizon.timer.cancel · HorizonCode Cancel Timer · C++ HE::api::timer::cancel · action
active(handle: int)
active: bool
Is this timer still running? False for a one-shot that has already gone off, for a cancelled one, and for a handle that never existed. Lua/Python horizon.timer.active · HorizonCode Timer Is Running · C++ HE::api::timer::active · query
cancelAll()
—
Stops every timer this application started. They also all stop when it closes. That includes the timers of every other script and of the GameInstance. Lua/Python horizon.timer.cancelAll · HorizonCode Cancel All Timers · C++ HE::api::timer::cancelAll · action
The wall clock: calendar date and time of day in the time zone of the
computer the game runs on. Unlike Time it keeps
going through pauses and has nothing to do with frames. A point in time
is a number of seconds since 1 January 1970 (the Unix epoch), which is
what now returns and every other function takes.
format uses strftime patterns:
%Y-%m-%d, %H:%M, %A for the
weekday name. An empty pattern, or a result longer than 511 bytes,
gives an empty string. weekday counts from 0 for Sunday.
▲
Known issue: accurate only to about a minute. The
seconds pass through the engine as a 32-bit float, which today can
only hold multiples of 128. now is therefore up to
64 seconds off, and so are second, minute
and format with %S or %M, also
when you pass in an exact time. The hour, day, month, year and
weekday are only wrong in the minute around a change. Until this is
fixed, take a time that has to be exact from the language:
os.time() and os.date() in Lua,
time.time() and datetime in Python.
Example: datetime.now
clock_label.lua
local M = {}
local DAYS = { [0] = "Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday" }
function M.onStart(self)
local now = horizon.datetime.now()
horizon.log("Today is " .. DAYS[horizon.datetime.weekday(now)] .. ", "
.. horizon.datetime.format(now, "%d.%m.%Y"))
-- To the second: from Lua itself until the precision issue is fixed.
horizon.log("It is " .. os.date("%H:%M:%S"))
end
return M
clock_label.py
import time
import horizon
DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"]
class ClockLabel(horizon.Behavior):
def on_start(self):
now = horizon.datetime.now()
day = DAYS[horizon.datetime.weekday(now)]
horizon.log(f"Today is {day}, {horizon.datetime.format(now, '%d.%m.%Y')}")
# To the second: from Python itself until the precision issue is fixed.
horizon.log("It is " + time.strftime("%H:%M:%S"))
Function
Returns
Description
now()
epochSeconds: float
The current wall-clock time as seconds since 1970. This is the clock the operating system shows, not the game clock: pausing does not stop it. Accurate only to about a minute, see the known issue. Lua/Python horizon.datetime.now · HorizonCode Now · C++ HE::api::datetime::now · query
format(epochSeconds: float, format: string)
text: string
Turns a time into text using a strftime pattern, in local time. "%Y-%m-%d %H:%M" gives you 2026-08-27 14:32. %M and %S are accurate only to about a minute, see the known issue. Lua/Python horizon.datetime.format · HorizonCode Format Time · C++ HE::api::datetime::format · query
year(epochSeconds: float)
value: int
The year of that time, in local time, as a full number like 2026. Lua/Python horizon.datetime.year · HorizonCode Year · C++ HE::api::datetime::year · query
month(epochSeconds: float)
value: int
The month of that time in local time, 1 for January through 12. Lua/Python horizon.datetime.month · HorizonCode Month · C++ HE::api::datetime::month · query
day(epochSeconds: float)
value: int
The day of the month of that time, in local time, from 1 to 31. Lua/Python horizon.datetime.day · HorizonCode Day · C++ HE::api::datetime::day · query
hour(epochSeconds: float)
value: int
The hour of that time in local time, from 0 to 23. Lua/Python horizon.datetime.hour · HorizonCode Hour · C++ HE::api::datetime::hour · query
minute(epochSeconds: float)
value: int
The minute of that time in local time, from 0 to 59. Accurate only to about a minute, see the known issue. Lua/Python horizon.datetime.minute · HorizonCode Minute · C++ HE::api::datetime::minute · query
second(epochSeconds: float)
value: int
The second of that time in local time, 0 to 60 (leap seconds). Not usable until the known issue is fixed. Lua/Python horizon.datetime.second · HorizonCode Second · C++ HE::api::datetime::second · query
weekday(epochSeconds: float)
value: int
The day of the week of that time in local time, with 0 for Sunday through 6 for Saturday. Lua/Python horizon.datetime.weekday · HorizonCode Weekday · C++ HE::api::datetime::weekday · query
Reading and writing JSON text without a parser of your own: a config
file from File System, a reply from
HTTP. Every call takes the whole document as a
string. The setters do not change it in place; they
return the new document, so keep the result:
doc = horizon.json.setNumber(doc, "volume", 0.8).
A path is a chain of keys separated by dots, with [i]
for an array element counted from 0: user.name,
items[2].id. The empty path is the document itself, so
count(text, "") counts a top-level array. A key that
itself contains a dot cannot be reached.
Getters never fail loudly. A missing path, a value of another type
and text that is not JSON all return the fallback.
has is true for a key whose value is
null.
Setters create missing objects along the path, and an empty string
starts a new document. They cannot add array elements: a path
through a missing index, or through a value that is not an object,
returns the document unchanged, as does text that is not JSON.
The returned text is compact and its keys come out in
alphabetical order, whatever order they had.
Numbers pass through a 32-bit float. getNumber is exact
for whole numbers up to 16 777 216 and keeps about seven digits
otherwise; setNumber writes 3 as
3.0 and 0.1 as
0.10000000149011612. Read ids and other large numbers
that the source wrote as strings with getString.
Example: json.getString
settings_file.lua
local M = {}
local FILE = "settings.json"
-- settings.json in the game's data folder, e.g.
-- {"video": {"width": 1920}, "profiles": [{"name": "Ada"}, {"name": "Lin"}]}
function M.onStart(self)
local doc = horizon.fs.readText(FILE) -- "" when the file is missing
local width = horizon.json.getNumber(doc, "video.width", 1280)
for i = 0, horizon.json.count(doc, "profiles") - 1 do
horizon.log(horizon.json.getString(doc, "profiles[" .. i .. "].name", "?"))
end
-- Setters return the new text; store it and write it back yourself.
doc = horizon.json.setNumber(doc, "video.width", width)
doc = horizon.json.setBool(doc, "video.seen", true)
horizon.fs.writeText(FILE, doc)
end
return M
settings_file.py
import horizon
FILE = "settings.json"
class SettingsFile(horizon.Behavior):
# settings.json in the game's data folder, e.g.
# {"video": {"width": 1920}, "profiles": [{"name": "Ada"}, {"name": "Lin"}]}
def on_start(self):
doc = horizon.fs.readText(FILE) # "" when the file is missing
width = horizon.json.getNumber(doc, "video.width", 1280)
for i in range(horizon.json.count(doc, "profiles")):
horizon.log(horizon.json.getString(doc, f"profiles[{i}].name", "?"))
# Setters return the new text; store it and write it back yourself.
doc = horizon.json.setNumber(doc, "video.width", width)
doc = horizon.json.setBool(doc, "video.seen", True)
horizon.fs.writeText(FILE, doc)
Reads a text value out of JSON. The path is dotted, with [i] for array elements: "user.name", "items[2].id". Missing, wrong type or unparsable text all give you the fallback. Lua/Python horizon.json.getString · HorizonCode JSON Get String · C++ HE::api::json::getString · query
Reads a number out of JSON at a dotted path. Missing, wrong type or unparsable text all give you the fallback rather than an error. Exact for whole numbers up to 16 777 216. Lua/Python horizon.json.getNumber · HorizonCode JSON Get Number · C++ HE::api::json::getNumber · query
Reads a true/false value out of JSON at a dotted path. Falls back to the given value when the path is missing or holds something else. Lua/Python horizon.json.getBool · HorizonCode JSON Get Bool · C++ HE::api::json::getBool · query
has(text: string, path: string)
present: bool
True when the dotted path exists in the JSON text. Says nothing about what type the value has, only that something is there. true also for a key whose value is null. Lua/Python horizon.json.has · HorizonCode JSON Has · C++ HE::api::json::has · query
count(text: string, path: string)
count: int
How many elements the array at this path has, or zero when the path is not an array. Use it to walk items[0], items[1] without guessing the end. Lua/Python horizon.json.count · HorizonCode JSON Array Count · C++ HE::api::json::count · query
Writes a text value into JSON and returns the whole document as new text. Missing objects along the path are created; an empty input starts a new document. Lua/Python horizon.json.setString · HorizonCode JSON Set String · C++ HE::api::json::setString · query
Writes a number into JSON at a dotted path and returns the whole document as new text. Missing objects along the path are created. Writes whole numbers as 3.0 and keeps about seven digits. Lua/Python horizon.json.setNumber · HorizonCode JSON Set Number · C++ HE::api::json::setNumber · query
setBool(text: string, path: string, value: bool)
result: string
Writes a true/false value into JSON at a dotted path and returns the whole document as new text. Missing objects along the path are created. Lua/Python horizon.json.setBool · HorizonCode JSON Set Bool · C++ HE::api::json::setBool · query
Lines, spheres and boxes drawn into the 3D view, to see what a script
is doing: where a ray went, what an overlap covered. A shape stays for
seconds; 0 draws it for one frame, so a shape that follows
something moving is drawn again every frame from
onUpdate. seconds counts down in real time,
so a paused game does not keep a shape alive longer; in the editor
the countdown runs only during Play. The color is red, green and blue from 0 to 1; its alpha is ignored.
clear removes every shape, including the ones other
scripts drew. Debug shapes appear in a packaged game too, so take them
out, or put them behind a flag of your own, before you ship.
▲
Known issue: four numbers per position. The
positions of line, sphere and
box are declared as colors, so Lua and Python read
four numbers for each one. Pass a fourth number
(it is ignored) after every x, y, z:
line takes 13 arguments, not 10. With too few, Lua
reports bad argument #11 and Python raises
IndexError. The signatures below show the
extra number as _.
Example: debug.line
ground_probe.lua
local M = {}
function M.onUpdate(self, dt)
local x, y, z = horizon.getPosition(self.entityId)
-- Four numbers per position and color, the fourth is ignored.
-- 0 seconds: drawn this frame only, so the shapes follow the entity.
horizon.debug.line(x, y, z, 0, x, y - 2, z, 0, 1, 0, 0, 1, 0)
horizon.debug.sphere(x, y - 2, z, 0, 0.25, 0, 1, 0, 1, 0)
end
return M
ground_probe.py
import horizon
class GroundProbe(horizon.Behavior):
def on_update(self, dt):
x, y, z = horizon.getPosition(self.entity_id)
# Four numbers per position and color, the fourth is ignored.
# 0 seconds: drawn this frame only, so the shapes follow the entity.
horizon.debug.line(x, y, z, 0, x, y - 2, z, 0, 1, 0, 0, 1, 0)
horizon.debug.sphere(x, y - 2, z, 0, 0.25, 0, 1, 0, 1, 0)
Draws a line in the world for Seconds (0 = this frame only). A packaged game draws it too, so take debug drawing out before you ship. Lua/Python horizon.debug.line · HorizonCode Draw Debug Line · C++ HE::api::debug::line · action
sphere(x, y, z, _, radius, r, g, b, _, seconds)
—
Draws a wireframe sphere in the world for Seconds. Useful for showing what an overlap or sphere cast actually covered. Lua/Python horizon.debug.sphere · HorizonCode Draw Debug Sphere · C++ HE::api::debug::sphere · action
Draws a wireframe box between two world corners for Seconds. Lua/Python horizon.debug.box · HorizonCode Draw Debug Box · C++ HE::api::debug::box · action
clear()
—
Removes every debug shape drawn so far, including ones whose time has not run out. Lua/Python horizon.debug.clear · HorizonCode Clear Debug Draw · C++ HE::api::debug::clear · action
Core and data · 1 function
Log
◆
In Lua and Python this row is the flat function horizon.log(message).
horizon.log writes one line at Info level to the
editor's Console and to HorizonEngine.log, in the editor and
in a packaged game alike. In the editor the line is prefixed with the
project's script language, for example [Lua]. The message
must be text. Lua turns a number into text on its own, and anything
else (nil, a table, a boolean) is an error. Python accepts
only a str, so pass numbers through an f-string or
str(). The language's own print writes to
standard output only, which the Console does not show.
Function
Returns
Description
log(message: string)
—
Writes a line to the editor's Console panel and to HorizonEngine.log. The cheapest way to find out whether an exec chain ran at all. Lua also accepts a number; Python only a str. Lua/Python horizon.log (flat) · HorizonCode Log · C++ HE::api::log · action
Creating, finding and removing entities. An entity is an integer id
(see Entity ids). When a destroyed entity's
slot is reused, the newcomer gets a different, larger id, so a kept
id never points at a stranger; it just stops existing. Guard it with
exists. exists(0) is
false: 0 is what findByName returns when it
finds nothing, even though it is also the scene root.
destroy takes the entity's children with it.
spawnClass and spawnClassRotated need the
running game or play mode; they return 0 and log the reason when the
class path is unknown or not an Entity class. The path is the class
asset's content path, as in the Content Browser.
self, selfObject and the Ref
rows (instance, owned,
destroyObject) belong to HorizonCode objects. A Lua or
Python script is not one, so self() and
selfObject() always return 0 there; use
self.entityId / self.entity_id.
Visibility only affects drawing. An entity with nothing to draw
always reports getVisible = true, also
right after setVisible(e, false).
▲
Known issue: spawn creates no transform.
The new entity has a name and a parent, but no Transform component,
so setPosition, setRotation and
setScale on it do nothing and getPosition
returns 0, 0, 0 (measured on build 7d49d44f, also for
the flat horizon.spawn). Until this is fixed, spawn a
class with spawnClass, or give an entity its position
by placing it in the scene.
saveState, hasSavedState and
applySavedState store an entity's pose and visibility in
the active save, keyed by the entity's stable
UUID, so the same entity finds its state again after the scene is
loaded anew. Three things must hold, and each missing one logs its own
warning and returns false: the game is running (play
mode or the packaged game), a save is active
(save.create / save.load), and the entity
has an enabled Save State component, whose checkboxes choose
what is stored. Nothing is on disk until save.write.
Example: entity.exists
boss_gate.lua
local M = {}
-- Opens the gate once the boss is gone. Look the ids up once, not every
-- frame: findByName walks every named entity.
function M.onStart(self)
self.boss = horizon.entity.findByName("Boss")
self.gate = horizon.entity.findByName("Gate")
end
function M.onUpdate(self, dt)
if self.gate ~= 0 and not horizon.entity.exists(self.boss) then
horizon.entity.destroy(self.gate) -- takes its children along
self.gate = 0
end
end
return M
boss_gate.py
import horizon
class BossGate(horizon.Behavior):
# Opens the gate once the boss is gone. Look the ids up once, not every
# frame: findByName walks every named entity.
def on_start(self):
self.boss = horizon.entity.findByName("Boss")
self.gate = horizon.entity.findByName("Gate")
def on_update(self, dt):
if self.gate != 0 and not horizon.entity.exists(self.boss):
horizon.entity.destroy(self.gate) # takes its children along
self.gate = 0
Function
Returns
Description
getName(entity: int)
name: string
The entity's name as the World Outliner shows it. Empty for an entity that does not exist. Lua/Python horizon.entity.getName, or flat horizon.getName · HorizonCode Get Name · C++ HE::api::entity::getName · query · entity is Self in HorizonCode
spawn(parent: int, name: string)
entity: int
Creates an empty entity at run time and returns it. It has a transform and nothing else — add what it needs with the other calls, or use Spawn Class, which brings the mesh, the collider, the body and the logic an author gave the class. An empty name becomes "Entity"; parent 0 puts it at the top level. Known issue: the entity gets no transform, so position calls on it do nothing (see the group notes). Lua/Python horizon.entity.spawn, or flat horizon.spawn · HorizonCode Spawn Entity · C++ HE::api::entity::spawn · action
destroy(entity: int)
—
Removes the entity and everything parented under it. Any id still held elsewhere becomes invalid; check with Exists before using one you kept. Lua/Python horizon.entity.destroy, or flat horizon.destroy · HorizonCode Destroy Entity · C++ HE::api::entity::destroy · action
Creates an instance of a HorizonCode class at this position and returns its entity. Unlike Spawn Entity it arrives furnished: the components the class carries, its physics body and its graph are all in place before Construct and Begin Play run, so its first frame already knows where it stands. Rotation stays as the class authored it. Returns 0 and logs why if the class is unknown or is not an Entity class. Lua/Python horizon.entity.spawnClass · HorizonCode Spawn Class · C++ HE::api::entity::spawnClass · action
Spawn Class with the rotation stated as well, in Euler degrees. The two are separate calls because "leave the rotation the class authored" and "face zero degrees" are different requests, and a defaulted 0,0,0 could not tell them apart. Lua/Python horizon.entity.spawnClassRotated · HorizonCode Spawn Class Rotated · C++ HE::api::entity::spawnClassRotated · action
destroyObject(object: ref)
—
Destroys a spawned object by its reference: the class instance, the entity under it and that entity's physics bodies. The counterpart to Spawn Class — reach the reference with Get Object On Entity. Destroy Entity is the other half of the pair and takes an entity instead. Lua/Python horizon.entity.destroyObject · HorizonCode Destroy Object (Ref) · C++ HE::api::entity::destroyObject · action
self()
entity: int
The entity this graph is running on. 0 in a graph that has none — the Game Instance, or a widget that was never attached to one. Always 0 in Lua and Python; use self.entityId / self.entity_id. Lua/Python horizon.entity.self · HorizonCode Get Owning Entity · C++ HE::api::entity::self · query
selfObject()
object: ref
This graph's own instance as an object reference, for handing to Bind Event, Call External or anything else that takes a target. Always 0 in Lua and Python. Lua/Python horizon.entity.selfObject · HorizonCode Get Owning Object · C++ HE::api::entity::selfObject · query
instance(entity: int)
object: ref
The HorizonCode object running on this entity, if one is. An empty reference when the entity carries no class. Lua/Python horizon.entity.instance · HorizonCode Get Object On Entity · C++ HE::api::entity::instance · query · entity is Self in HorizonCode
owned(object: ref)
entity: int
The entity an object reference sits on — the inverse of Instance. Lua/Python horizon.entity.owned · HorizonCode Get Entity Of · C++ HE::api::entity::owned · query
distance(a: int, b: int)
distance: float
Straight-line distance between two entities in metres. Cheaper than subtracting two positions and taking the length, and reads better. Negative when there is no world to measure in, which no running game has. Known issue: it measures between the local positions, so the answer is right only for entities with the same parent (or none). An entity without a transform counts as standing at the origin. Lua/Python horizon.entity.distance · HorizonCode Distance Between · C++ HE::api::entity::distance · query
findByName(name: string)
entity: int
Finds the first entity with this name in the active scene. A linear search over every named entity: fine once in a while, wasteful every frame — store the id in a variable instead. Lua/Python horizon.entity.findByName · HorizonCode Find By Name · C++ HE::api::entity::findByName · query
exists(entity: int)
exists: bool
Is this entity id still valid? The guard to run before touching an entity that something else may have destroyed. false for 0. Lua/Python horizon.entity.exists · HorizonCode Entity Exists · C++ HE::api::entity::exists · query
setVisible(entity: int, visible: bool)
—
Shows or hides whatever the entity draws — mesh, skeletal mesh, light. Physics and scripts are untouched; it stops being drawn, not being there. Lua/Python horizon.entity.setVisible · HorizonCode Set Entity Visible · C++ HE::api::entity::setVisible · action · entity is Self in HorizonCode
saveState(entity: int)
ok: bool
Stores this entity's flagged attributes (see its Save State component) in the active save, keyed by its UUID. Play mode only. Needs play mode, an active save and an enabled Save State component; see the group notes. Lua/Python horizon.entity.saveState · HorizonCode Save Entity State · C++ HE::api::entity::saveState · action · entity is Self in HorizonCode
hasSavedState(entity: int)
has: bool
Does the active save carry state for this entity? Lua/Python horizon.entity.hasSavedState · HorizonCode Has Saved State · C++ HE::api::entity::hasSavedState · query · entity is Self in HorizonCode
applySavedState(entity: int)
ok: bool
Applies every attribute the active save carries for this entity back onto it. Partial: what the save lacks stays as it is. Play mode only. On an entity with a physics body it moves the body too and stops its motion. Lua/Python horizon.entity.applySavedState · HorizonCode Apply Saved State · C++ HE::api::entity::applySavedState · action · entity is Self in HorizonCode
getVisible(entity: int)
visible: bool
Whether the entity is currently drawn. True for an entity that has nothing to draw at all. Lua/Python horizon.entity.getVisible · HorizonCode Get Entity Visible · C++ HE::api::entity::getVisible · query · entity is Self in HorizonCode
World and storage · 8 functions
Transform
◆
Lua and Python have no horizon.transform.*: use the flat horizon.getPosition / setPosition / getRotation / setRotation / getScale / setScale. The world-space rows are HorizonCode and C++ only.
Position, rotation and scale are local: relative to
the parent, in metres and Euler degrees (X pitch, Y yaw, Z roll).
Without a parent, local and world are the same. On an entity with a
rigid body or a character controller, setPosition and
setRotation teleport the body as well, so the physics
step does not pull it back; nothing checks for walls on the way.
The world-space pair getWorldPosition /
setWorldPosition exists only in HorizonCode and C++. A
script that needs where a child really stands keeps such entities
unparented, or does that part in a HorizonCode graph with Get
World Position. Adding up the parents' positions is not enough
once a parent is rotated or scaled.
Function
Returns
Description
getPosition(entity: int)
position: vec3
LOCAL position in metres: for a child entity this is its offset from the parent, not where it ends up. Use Get World Position for that. Without a parent the two are the same, which is why the difference only shows up on the first attached object. Lua/Python only as flat horizon.getPosition · HorizonCode Get Position · C++ HE::api::transform::getPosition · query · entity is Self in HorizonCode
setPosition(entity: int, position: vec3)
—
Moves the entity to a LOCAL position — relative to its parent, if it has one. It teleports: nothing sweeps for collisions on the way, so a physics body can end up inside a wall. For a character use Move instead. Lua/Python only as flat horizon.setPosition · HorizonCode Set Position · C++ HE::api::transform::setPosition · action · entity is Self in HorizonCode
getRotation(entity: int)
rotation: vec3
Rotation as euler angles in degrees (X pitch, Y yaw, Z roll). Lua/Python only as flat horizon.getRotation · HorizonCode Get Rotation · C++ HE::api::transform::getRotation · query · entity is Self in HorizonCode
setRotation(entity: int, rotation: vec3)
—
Turns the entity to these euler angles in degrees. Y is the one that turns a standing character to face elsewhere. Lua/Python only as flat horizon.setRotation · HorizonCode Set Rotation · C++ HE::api::transform::setRotation · action · entity is Self in HorizonCode
getScale(entity: int)
scale: vec3
Scale per axis. 1 is the mesh at its authored size. Lua/Python only as flat horizon.getScale · HorizonCode Get Scale · C++ HE::api::transform::getScale · query · entity is Self in HorizonCode
setScale(entity: int, scale: vec3)
—
Resizes the entity. A physics collider follows the scale only approximately — for anything that has to collide accurately, scale the collider's own extents. Lua/Python only as flat horizon.setScale · HorizonCode Set Scale · C++ HE::api::transform::setScale · action · entity is Self in HorizonCode
getWorldPosition(entity: int)
position: vec3
Where the entity actually stands, with every parent taken into account. Composed on the spot from the parent chain, so it is true even for an entity that moved or was spawned this frame. not reachable from Lua/Python · HorizonCode Get World Position · C++ HE::api::transform::getWorldPosition · query · entity is Self in HorizonCode
setWorldPosition(entity: int, position: vec3)
—
Puts the entity at a world position whatever its parents are doing, by converting through them. Teleports like Set Position. not reachable from Lua/Python · HorizonCode Set World Position · C++ HE::api::transform::setWorldPosition · action · entity is Self in HorizonCode
Every call that changes scenes is a request: it is
queued and carried out at the start of the next frame, before any
script's onUpdate, in the order it was made. So the code after load still runs
in the old scene, and a zone you just loaded is not there yet:
zonePosition returns 0, 0, 0 and
zoneScene an empty string until the next frame. Show,
hide and move requests made right after loadAdditive
still apply to the new zone, because they queue behind it. Scene paths
are relative to the project folder, for example
Scenes/Level2.hescene.
Switching (load) replaces the whole
scene. With hidden = true it only
preloads the scene and its assets;
hasPendingLevel turns true, and
activate swaps it in without a loading hitch. A second
hidden load replaces the pending one.
Zones (loadAdditive) add a scene next
to the current one, at a world offset, and return a zone id at
once. Zone ids count up from 1 and are never reused. A hidden zone
is loaded and simulated but not drawn until showZone.
A zone gets collision, and in the packaged game its scripts start
when it arrives.
◆
In the editor's play mode zones work, but
load and activate only log “runs in
the packaged game” and keep the current scene. Leaving play
mode removes every zone again.
Example: scene.loadAdditive
cave_stream.lua
local M = {}
-- This entity is a trigger volume at the start of the corridor. The cave
-- loads hidden, 200 m along X, while the player walks; the door shows it.
function M.onBeginOverlap(self, other)
if self.zone then return end
-- The position takes FOUR numbers from a script; the fourth is ignored.
self.zone = horizon.scene.loadAdditive("Scenes/Cave.hescene", true, 200, 0, 0, 0)
end
-- "Interact" is an input action.
function M.onInputPressed(self, name)
if name == "Interact" and self.zone then
horizon.scene.showZone(self.zone) -- queued behind the load
end
end
return M
cave_stream.py
import horizon
class CaveStream(horizon.Behavior):
# This entity is a trigger volume at the start of the corridor. The cave
# loads hidden, 200 m along X, while the player walks; the door shows it.
def on_start(self):
self.zone = 0
def on_begin_overlap(self, other):
if self.zone:
return
# The position takes FOUR numbers from a script; the fourth is ignored.
self.zone = horizon.scene.loadAdditive("Scenes/Cave.hescene", True, 200, 0, 0, 0)
# "Interact" is an input action.
def on_input_pressed(self, name):
if name == "Interact" and self.zone:
horizon.scene.showZone(self.zone) # queued behind the load
Function
Returns
Description
load(scene: string, hidden: bool)
—
Replaces the current scene with another one. It happens at the end of the frame, not inside this call — the exec chain after it still runs, in the old scene. In the editor's play mode this only logs a warning. Lua/Python horizon.scene.load · HorizonCode Load Scene · C++ HE::api::scene::load · action
loadAdditive(scene, hidden, x, y, z, _)
zone: int
Loads another scene ALONGSIDE this one at a world offset and returns its zone id, which every other zone call takes. Hidden loads it without showing it, so the streaming in can be paid for before the player arrives. Known issue: the position pin is typed as a colour, so Lua and Python must pass four numbers; the fourth is ignored. With three, Lua raises bad argument #6 and Python IndexError. An offset of 0, 0, 0 leaves the zone where its scene put it. Lua/Python horizon.scene.loadAdditive · HorizonCode Load Scene Additive · C++ HE::api::scene::loadAdditive · action
unloadZone(zone: int)
—
Removes an additively loaded zone and everything in it. Lua/Python horizon.scene.unloadZone · HorizonCode Unload Zone · C++ HE::api::scene::unloadZone · action
activate()
—
Makes the pending level the active one. The second half of a load that was started earlier. Logs a warning when nothing was preloaded. In the editor's play mode this only logs a warning. Lua/Python horizon.scene.activate · HorizonCode Activate Loaded Scene · C++ HE::api::scene::activate · action
hasPendingLevel()
pending: bool
Is a level load waiting to be activated? Lua/Python horizon.scene.hasPendingLevel · HorizonCode Has Pending Scene · C++ HE::api::scene::hasPendingLevel · query
showZone(zone: int)
—
Shows a zone that was loaded hidden. The entities were there all along; this is the moment they start being drawn and ticked. Lua/Python horizon.scene.showZone · HorizonCode Show Zone · C++ HE::api::scene::showZone · action
hideZone(zone: int)
—
Hides a zone without unloading it — cheap to undo, unlike unload. Lua/Python horizon.scene.hideZone · HorizonCode Hide Zone · C++ HE::api::scene::hideZone · action
zonePosition(zone: int)
position: vec3
The world offset a zone was loaded at. 0, 0, 0 until the zone has arrived, the frame after loadAdditive. Lua/Python horizon.scene.zonePosition · HorizonCode Get Zone Position · C++ HE::api::scene::zonePosition · query
setZonePosition(zone: int, position: vec3)
—
Moves a whole loaded zone to a new world offset. How an endless world recycles a chunk instead of loading a new one. Rebuilds the zone's collision at the new place, which stops moving bodies in it. Lua/Python horizon.scene.setZonePosition · HorizonCode Set Zone Position · C++ HE::api::scene::requestZonePosition · action
zoneScene(zone: int)
scene: string
Which scene asset a zone was loaded from. Empty until the zone has arrived. Lua/Python horizon.scene.zoneScene · HorizonCode Get Zone Scene · C++ HE::api::scene::zoneScene · query
loadedZones()
zones: int[]
The ids of every additively loaded zone, as an array. Lua/Python horizon.scene.loadedZones · HorizonCode Loaded Zones · C++ HE::api::scene::loadedZones · query · list: HorizonCode/C++ only
available()
scenes: string[]
Every scene asset in the project, as an array of paths — so a level select does not have to hardcode the list. Lua/Python horizon.scene.available · HorizonCode Available Scenes · C++ HE::api::scene::availableScenes · query · list: HorizonCode/C++ only
Loading assets ahead of time. Everything a scene uses is loaded with
it, so these calls are for what a scene does not name yet: the level
behind a door, a mesh a script will spawn. An asset is named by its
path inside the project's Content folder, as the Content
Browser shows it, for example Meshes/Crate.hasset.
load reads the file right away, in the calling frame,
so call it before the moment the asset is needed.
load on an asset that is already in memory returns
true without reading it again. A path that does not exist
returns false and logs where it looked.
unload returns false the second time.
typeName answers only for a loaded asset, with names like
StaticMesh, Texture or
Material, and an empty string otherwise.
Function
Returns
Description
load(asset: string)
loaded: bool
Brings an asset into memory ahead of the moment it is needed — the mesh behind the door, the music for the next area. Already loaded is a success, not a second load. Reading the file is the slow part, so do it early rather than in the frame that needs it. Lua/Python horizon.content.load · HorizonCode Load Asset · C++ HE::api::content::load · action
unload(asset: string)
unloaded: bool
Lets go of an asset again and frees what it held. False means it was not loaded in the first place. Anything still using it — a mesh component, a playing sound — will stop finding it, so unload what you have left behind, not what is on screen. Lua/Python horizon.content.unload · HorizonCode Unload Asset · C++ HE::api::content::unload · action
isLoaded(asset: string)
loaded: bool
Whether this asset is in memory right now. Asking does not load it, so this is the safe way to check before deciding to. Lua/Python horizon.content.isLoaded · HorizonCode Is Asset Loaded · C++ HE::api::content::isLoaded · query
typeName(asset: string)
type: string
What kind of asset this is, as text: StaticMesh, Texture, Material, Widget and so on. Empty for an asset that is not loaded — Load Asset first. Useful when a path arrives from somewhere and you want to know what you were handed. Lua/Python horizon.content.typeName · HorizonCode Asset Type Name · C++ HE::api::content::typeName · query
Savegames with a fixed shape. A save follows a SaveGame
Template asset, which lists its fields and their defaults; the
project's default template (Project Settings) is the one
create uses. One save is active at a
time and every field call works on it, in memory, until
write puts it on disk as
Saves/<id>.json in the game's data folder (see
File System). write replaces the file
in one step, so a crash leaves the old save or the new one, never
half of each.
A save id may only contain A–Z a–z 0–9 _ -; anything
else (a space, a dot) fails with a warning.
Each getter takes a default and returns it when there is no active
save, the template has no such field, or the field has another
type; the matching setter returns false. All three
cases log a warning.
Numbers: a Float, Int or Enum field goes through
setNumber / getNumber. An Int field drops
the fraction toward zero (2.7 → 2, −2.7 → −2). A Float field keeps
about seven digits: 0.1 comes back as
0.10000000149011612.
A struct field takes a value built with
horizon.structs.<Name>() or read with
getStruct (see Project
Types). A plain table without __type is refused.
A save remembers its template, and load uses that one,
not the project default. It keeps the fields the template still
has and fills new ones with their defaults, so a save from an older
version of the game still loads.
delete removes the file only. If it was the active save,
it stays active, and the next write creates the file
again; call close as well.
Example: save.write
checkpoint.lua
local M = {}
local SLOT = "slot1"
-- "level" (Int) and "stats" (a struct with "kills") are fields of the
-- project's SaveGame Template.
function M.onStart(self)
-- Continue from the save if there is one, else start a new one.
if not (horizon.save.exists(SLOT) and horizon.save.load(SLOT)) then
horizon.save.create(SLOT)
end
horizon.log(string.format("Level %d", horizon.save.getNumber("level", 1)))
end
-- This entity is the checkpoint's trigger volume.
function M.onBeginOverlap(self, other)
horizon.save.setNumber("level", horizon.save.getNumber("level", 1) + 1)
local stats = horizon.save.getStruct("stats") -- keeps __type
stats.kills = stats.kills + 1
horizon.save.setStruct("stats", stats)
horizon.save.write() -- nothing is on disk before this
end
return M
checkpoint.py
import horizon
SLOT = "slot1"
class Checkpoint(horizon.Behavior):
# "level" (Int) and "stats" (a struct with "kills") are fields of the
# project's SaveGame Template.
def on_start(self):
# Continue from the save if there is one, else start a new one.
if not (horizon.save.exists(SLOT) and horizon.save.load(SLOT)):
horizon.save.create(SLOT)
horizon.log(f"Level {horizon.save.getNumber('level', 1):.0f}")
# This entity is the checkpoint's trigger volume.
def on_begin_overlap(self, other):
horizon.save.setNumber("level", horizon.save.getNumber("level", 1) + 1)
stats = horizon.save.getStruct("stats") # keeps __type
stats["kills"] += 1
horizon.save.setStruct("stats", stats)
horizon.save.write() # nothing is on disk before this
Function
Returns
Description
create(id: string)
ok: bool
Starts a NEW save from the project's SaveGame Template, with the fields seeded from the template defaults, and makes it active. In memory only until Write Save. Replaces the active save without writing it. Lua/Python horizon.save.create · HorizonCode Create Save · C++ HE::api::save::create · action
load(id: string)
ok: bool
Loads Saves/<id>.json, re-validates it against the template it names and makes it active. Fails if the save or its template is missing. Lua/Python horizon.save.load · HorizonCode Load Save · C++ HE::api::save::load · action
write()
ok: bool
Persists the active save to disk, atomically. Lua/Python horizon.save.write · HorizonCode Write Save · C++ HE::api::save::write · action
close()
—
Drops the active save without writing it. Lua/Python horizon.save.close · HorizonCode Close Save · C++ HE::api::save::close · action
activeId()
id: string
Id of the active save; empty when there is none. Lua/Python horizon.save.activeId · HorizonCode Active Save Id · C++ HE::api::save::activeId · query
list()
ids: string[]
Ids of every save on disk. Lua/Python horizon.save.list · HorizonCode List Saves · C++ HE::api::save::list · query · list: HorizonCode/C++ only
exists(id: string)
exists: bool
Is there a save with this id on disk? Lua/Python horizon.save.exists · HorizonCode Save Exists · C++ HE::api::save::exists · query
delete(id: string)
ok: bool
Deletes a save file. Does not touch the active save. Deleting the active save leaves it active; write would create the file again. Lua/Python horizon.save.delete · HorizonCode Delete Save · C++ HE::api::save::remove · action
fields()
names: string[]
Field names the active save's template declares — so a graph never has to hardcode them. Lua/Python horizon.save.fields · HorizonCode Save Fields · C++ HE::api::save::fields · query · list: HorizonCode/C++ only
setNumber(field: string, value: float)
ok: bool
Writes a Float, Int or Enum field of the active save. The field is picked from the template on the node itself. An Int or Enum field drops the fraction toward zero. Lua/Python horizon.save.setNumber · HorizonCode Save Set Number · C++ HE::api::save::setNumber · action
getNumber(field: string, default: float)
value: float
Reads a Float, Int or Enum field. An unknown field, or no active save, yields the Default input rather than zero. A Float field keeps about seven digits. Lua/Python horizon.save.getNumber · HorizonCode Save Get Number · C++ HE::api::save::getNumber · query
setString(field: string, value: string)
ok: bool
Writes a String field of the active save. Lua/Python horizon.save.setString · HorizonCode Save Set String · C++ HE::api::save::setString · action
getString(field: string, default: string)
value: string
Reads a String field; falls back to the Default input. Lua/Python horizon.save.getString · HorizonCode Save Get String · C++ HE::api::save::getString · query
setBool(field: string, value: bool)
ok: bool
Writes a Bool field of the active save. Lua/Python horizon.save.setBool · HorizonCode Save Set Bool · C++ HE::api::save::setBool · action
getBool(field: string, default: bool)
value: bool
Reads a Bool field; falls back to the Default input. Lua/Python horizon.save.getBool · HorizonCode Save Get Bool · C++ HE::api::save::getBool · query
setStruct(field: string, value: struct)
ok: bool
Writes a Struct field. The value's definition must match the one the template declares. Needs a value from horizon.structs.<Name>() or getStruct; a table without __type is refused. Lua/Python horizon.save.setStruct · HorizonCode Save Set Struct · C++ HE::api::save::setStructV · action
getStruct(field: string)
value: struct
Reads a Struct field of the active save. Lua/Python horizon.save.getStruct · HorizonCode Save Get Struct · C++ HE::api::save::getStructV · query
Relative paths point into the game's own data folder, the sandbox;
.. cannot leave it. An absolute path works only for a file
or folder the user picked in a file dialog, or
when the project allows Files outside the
project. Anything else fails with the call's “failed” value and a
log line that names the path.
Where the sandbox is: in the editor, the Saved folder next
to the project file. In the exported game, a per-user folder named
after the project, HorizonCreations/<Project>/Saved
under %APPDATA% on Windows,
~/Library/Application Support on macOS and
~/.local/share on Linux. Savegames
(Saves/) and Preferences
(Prefs.json) live there too, so fs.* can see
and delete them.
writeText creates missing folders on the way.
readText returns an empty string for a missing file.
copy never overwrites: it returns false
when the target exists. renamedoes
overwrite an existing file at the target, without asking.
remove deletes files only, never a folder.
makeDir returns true when the folder
already exists.
An empty path means the top of the sandbox for list
and watch, and is refused everywhere else.
size and modified return
-1 for anything that is not there, and
size also for a folder. Both pass through a 32-bit
float: sizes are exact up to 16 MB, and modified is up
to 64 seconds off, the same issue as Date and
Time. It can even lie a little in the future.
◆
watch reports changes through the HorizonCode event
On File Changed on the GameInstance, and only in the
exported game: the editor's play mode does not check watched paths,
and scripts have no callback for it (see
Events). From a script, read the file again
on a timer and compare; modified
is too coarse for that.
Function
Returns
Description
writeText(path: string, text: string)
ok: bool
Writes a text file, replacing whatever was there. Paths are relative to the game's own data directory; it cannot write outside it. For savegames use the Save calls, which handle the format and the atomic write. Lua/Python horizon.fs.writeText · HorizonCode Write Text File · C++ HE::api::fs::writeText · action
readText(path: string)
text: string
Reads a whole text file. Empty when the file does not exist — ask Exists first if the difference matters. Lua/Python horizon.fs.readText · HorizonCode Read Text File · C++ HE::api::fs::readText · action
exists(path: string)
exists: bool
Is there a file at this path? Lua/Python horizon.fs.exists · HorizonCode File Exists · C++ HE::api::fs::exists · query
remove(path: string)
ok: bool
Deletes a file. Ok is false if it was not there. Files only; a folder is refused. Lua/Python horizon.fs.remove · HorizonCode Delete File · C++ HE::api::fs::remove · action
makeDir(path: string)
ok: bool
Creates a directory, parents included. Ok is true if it already existed. Lua/Python horizon.fs.makeDir · HorizonCode Make Directory · C++ HE::api::fs::makeDir · action
isDir(path: string)
isDir: bool
Is there a DIRECTORY at this path? Exists answers about anything; this one answers about the thing you can list. Lua/Python horizon.fs.isDir · HorizonCode Is Directory · C++ HE::api::fs::isDir · query
size(path: string)
bytes: float
The file's size in bytes, or -1 for anything that is not a readable file — a directory included, since a folder's size is not a number one call can honestly give. Exact up to 16 MB (32-bit float). Lua/Python horizon.fs.size · HorizonCode File Size · C++ HE::api::fs::size · query
modified(path: string)
time: float
When the file was last written, in seconds, on the same clock Now uses. So "how old is this file" is Now minus this, and not a second time format to learn. -1 when there is nothing there. Known issue: up to 64 seconds off, as in Date and Time. Lua/Python horizon.fs.modified · HorizonCode File Modified Time · C++ HE::api::fs::modified · query
list(dir: string)
names: string[]
The names of everything directly inside a directory, sorted. Names only, not full paths — joining stays yours. Empty for a path that is not a directory. Leave the path empty for the top of your own folder. Lua/Python horizon.fs.list · HorizonCode List Directory · C++ HE::api::fs::list · query · list: HorizonCode/C++ only
rename(from: string, to: string)
ok: bool
Moves or renames, in one step. Both paths follow the same rules as every other file call. Overwrites an existing file at to, unlike copy. Lua/Python horizon.fs.rename · HorizonCode Rename File · C++ HE::api::fs::rename · action
copy(from: string, to: string)
ok: bool
Copies a file. It will NOT overwrite: if something is already at the destination it does nothing and Ok is false. This is the one file call that could destroy something you did not name, so it refuses instead. Lua/Python horizon.fs.copy · HorizonCode Copy File · C++ HE::api::fs::copy · action
watch(path: string)
handle: int
Keeps an eye on a file or a folder. From now on On File Changed fires with the path whenever it appears, disappears or changes; for a folder that means its immediate contents, one level deep. Returns a handle for Stop Watching, or 0 when the path is out of reach. Checked about once a second, so it is for reacting, not for timing. Only in the exported game, and only the GameInstance's On File Changed hears it; scripts get no callback. Lua/Python horizon.fs.watch · HorizonCode Watch File · C++ HE::api::fs::watch · action
unwatch(handle: int)
—
Stops a watch, using the handle Watch File gave you. Watches also all end when the application does. Lua/Python horizon.fs.unwatch · HorizonCode Stop Watching File · C++ HE::api::fs::unwatch · action
Small settings that outlive the game: volume, the last folder, “don't
show this again”. Each value sits under a key of your choosing in
Prefs.json in the game's data folder (see
File System), shared by every script and graph of
the game. Every set and remove writes the file at once.
Progress belongs in a save instead.
A getter returns its fallback when the key was never set or holds
another type: getBool on a number gives the fallback,
not true. has tells a missing key from one
that is set to the default value.
Numbers are stored with about seven significant digits:
setNumber("volume", 0.1) writes
0.10000000149011612, and whole numbers are exact only
up to 16 777 216. Keep ids and large counts as strings.
▲
Known issue in the editor: preferences follow you into the
next project. The file is read once per run of the
program. After opening another project in the same editor session,
the getters still answer with the first project's preferences, and
the next set writes all of them into the new project's
Prefs.json (measured on build 7d49d44f). Restart the
editor after switching projects before you play. An exported game
has only one project and is not affected.
Example: prefs.getBool
tutorial_hint.lua
local M = {}
function M.onStart(self)
if not horizon.prefs.getBool("tutorial.skip", false) then
horizon.log("Tutorial: press Space to jump")
end
end
-- "SkipTutorial" is an input action, e.g. on the hint's close button.
function M.onInputPressed(self, name)
if name == "SkipTutorial" then
horizon.prefs.setBool("tutorial.skip", true) -- on disk at once
end
end
return M
tutorial_hint.py
import horizon
class TutorialHint(horizon.Behavior):
def on_start(self):
if not horizon.prefs.getBool("tutorial.skip", False):
horizon.log("Tutorial: press Space to jump")
# "SkipTutorial" is an input action, e.g. on the hint's close button.
def on_input_pressed(self, name):
if name == "SkipTutorial":
horizon.prefs.setBool("tutorial.skip", True) # on disk at once
Function
Returns
Description
getString(key: string, fallback: string)
value: string
Reads a saved setting as text, or the fallback when it was never set. Preferences are small scraps like a last folder — not the save system, which is shaped by a template and belongs to a game's progress. Lua/Python horizon.prefs.getString · HorizonCode Get Pref String · C++ HE::api::prefs::getString · query
getNumber(key: string, fallback: float)
value: float
Reads a saved setting as a number, or the fallback when it was never set or holds something else. Lua/Python horizon.prefs.getNumber · HorizonCode Get Pref Number · C++ HE::api::prefs::getNumber · query
getBool(key: string, fallback: bool)
value: bool
Reads a saved setting as true/false, or the fallback when it was never set. The natural home for "don't show this again". Lua/Python horizon.prefs.getBool · HorizonCode Get Pref Bool · C++ HE::api::prefs::getBool · query
setString(key: string, value: string)
—
Saves a text setting under a key. Written to disk immediately, so a crash cannot cost more than the last change. Lua/Python horizon.prefs.setString · HorizonCode Set Pref String · C++ HE::api::prefs::setString · action
setNumber(key: string, value: float)
—
Saves a numeric setting under a key. Written to disk immediately. Stored with about seven digits: 0.1 becomes 0.10000000149011612. Lua/Python horizon.prefs.setNumber · HorizonCode Set Pref Number · C++ HE::api::prefs::setNumber · action
setBool(key: string, value: bool)
—
Saves a true/false setting under a key. Written to disk immediately. Lua/Python horizon.prefs.setBool · HorizonCode Set Pref Bool · C++ HE::api::prefs::setBool · action
has(key: string)
present: bool
True when this key has ever been set. Lets a first run be told apart from one where the user deliberately chose the default. Lua/Python horizon.prefs.has · HorizonCode Has Pref · C++ HE::api::prefs::has · query
remove(key: string)
removed: bool
Forgets one setting, so the next read gets its fallback again. False when there was nothing under that key. Lua/Python horizon.prefs.remove · HorizonCode Remove Pref · C++ HE::api::prefs::remove · action
clear()
—
Forgets every setting at once — what a "reset to defaults" button does. Lua/Python horizon.prefs.clear · HorizonCode Clear Prefs · C++ HE::api::prefs::clear · action
Queries against the physics world and pushes on its bodies. Without a
running physics world (edit mode, a world with no bodies built) every
query misses, every push returns false and every read is
zero. Units are metres, seconds, newtons and kilograms; the one
exception is spin, which is in radians per second.
Hits from Lua/Python.raycast and the
four casts return ten values: hit, entity, px, py, pz, nx, ny,
nz, distance, layer. On a miss they are false
and zeros. The flat horizon.raycast
returns a table / dict (entity, x, y, z, nx, ny, nz,
distance, no layer) or nil / None.
The direction does not have to be normalised.
A ray started inside a collider hits that collider
at distance 0. A ray from a character's own position therefore
finds the character. Start it outside the own capsule (a step
ahead, see the example below) or give the character its own
collision layer and leave that bit out of the mask.
Shape casts report the centre, not the contact.
For sphereCast, boxCast and
capsuleCast, point is where the shape's
centre stopped (a 0.25 m sphere dropped on a floor at y = 1 reports
y = 1.25), which is what a camera boom or a placement check wants.
The shape casts ignore trigger volumes; raycast and
the overlaps see them.
Layer masks are bitfields: bit N is collision layer
N, 65535 (or -1) is every layer, 0 sees nothing. The layer names
are in Project Settings under Collision Layers.
World and local. Every point a query returns is in
world space, and so are the positions of
addForceAtPosition / addImpulseAtPosition.
setPosition and setPositionAndReset take a
local position, like horizon.setPosition: for a
child entity the value is relative to its parent. Joint anchors are
local to their own entity.
Forces (addForce, addImpulse,
addTorque, the two …AtPosition) need a
Dynamic rigid body and return false with a log
line on a static or kinematic one, so also on a character.
A character moves through Locomotion.
setVelocity addresses the character controller when
there is one, otherwise the rigid body. On a character with a
Movement component the next frame overwrites the horizontal part
with whatever locomotion.move asked for (nothing =
standing still), so use setVelocity on characters only
without one.
setGravity changes gravity for rigid bodies in the
whole world. A character falls by its own Character Controller
gravity and does not follow it.
Joints tie two rigid bodies together; the entity
passed first owns the joint, and only it reports hasJoint.
A type outside 0–4 becomes Fixed. setJointMotor works on
a Hinge or Slider only and returns false otherwise. A
joint made at runtime is written to the owner's Joint component, the
same component the editor places, and removeJoint or a
break removes it again.
▲
List results do not reach Lua or Python.overlapSphere, overlapSphereLayers,
overlapBox, overlapCapsule,
raycastAll and pollJointBroken return
scalar zeros there instead of lists (see Lists,
measured on build 7d49d44f). pollJointBroken still
empties the queue, so a script calling it takes the events away from
a HorizonCode graph that polls the same queue. Until this is fixed,
use these from HorizonCode, and in scripts react to contacts with
onBeginOverlap on a trigger
volume.
Example: physics.raycast
push_crate.lua
local M = {}
local REACH = 2.0 -- metres in front of the character
local PUSH = 4.0 -- impulse, newton-seconds
-- "Interact" is a button action. Pushes whatever dynamic body is in front.
function M.onInputPressed(self, name)
if name ~= "Interact" then return end
local e = self.entityId
local x, y, z = horizon.getPosition(e)
local _, yaw = horizon.getRotation(e)
local r = horizon.math.radians(yaw)
local fx, fz = -horizon.math.sin(r), -horizon.math.cos(r) -- facing, -Z at yaw 0
-- Start a step ahead: a ray from inside the own capsule would hit it.
local ox, oz = x + fx * 0.75, z + fz * 0.75
local hit, other, px, py, pz = horizon.physics.raycast(ox, y, oz, fx, 0, fz, REACH)
if hit and horizon.physics.hasPhysics(other) then
-- The hit point is world space, which is what AtPosition expects.
horizon.physics.addImpulseAtPosition(other, fx * PUSH, 0, fz * PUSH, px, py, pz)
end
end
return M
push_crate.py
import horizon
REACH = 2.0 # metres in front of the character
PUSH = 4.0 # impulse, newton-seconds
class PushCrate(horizon.Behavior):
# "Interact" is a button action. Pushes whatever dynamic body is in front.
def on_input_pressed(self, name):
if name != "Interact":
return
e = self.entity_id
x, y, z = horizon.getPosition(e)
_, yaw, _ = horizon.getRotation(e)
r = horizon.math.radians(yaw)
fx, fz = -horizon.math.sin(r), -horizon.math.cos(r) # facing, -Z at yaw 0
# Start a step ahead: a ray from inside the own capsule would hit it.
ox, oz = x + fx * 0.75, z + fz * 0.75
hit, other, px, py, pz, *_ = horizon.physics.raycast(ox, y, oz, fx, 0, fz, REACH)
if hit and horizon.physics.hasPhysics(other):
# The hit point is world space, which is what AtPosition expects.
horizon.physics.addImpulseAtPosition(other, fx * PUSH, 0, fz * PUSH, px, py, pz)
Example: physics.setPositionAndReset
kill_plane.lua
local M = {}
local FLOOR = -20 -- below this height the character has fallen out
function M.onStart(self)
self.sx, self.sy, self.sz = horizon.getPosition(self.entityId)
end
function M.onUpdate(self, dt)
local _, y = horizon.getPosition(self.entityId)
if y < FLOOR then
-- Back to where the level started it, without the speed of the fall.
horizon.physics.setPositionAndReset(self.entityId, self.sx, self.sy, self.sz)
end
end
return M
kill_plane.py
import horizon
FLOOR = -20 # below this height the character has fallen out
class KillPlane(horizon.Behavior):
def on_start(self):
self.spawn = horizon.getPosition(self.entity_id)
def on_update(self, dt):
_, y, _ = horizon.getPosition(self.entity_id)
if y < FLOOR:
# Back to where the level started it, without the speed of the fall.
horizon.physics.setPositionAndReset(self.entity_id, *self.spawn)
Fires a ray from Origin along Direction and reports the first thing it hits. Direction does not have to be normalised. Hit is false when nothing is in the way, and the other outputs are then meaningless rather than stale — always branch on Hit first. A ray that starts inside a collider hits it at distance 0, so a ray from a character's own position finds the character. Sees trigger volumes. Lua/Python horizon.physics.raycast, or flat horizon.raycast · HorizonCode Raycast · C++ HE::api::physics::raycast · query
setVelocity(entity: int, velocity: vec3)
—
Sets a body's velocity in metres per second outright, discarding whatever it had. For a nudge use Add Impulse, which adds to the motion instead of replacing it. On a character with a Movement component the next frame replaces the horizontal part with what locomotion.move asked for. Lua/Python horizon.physics.setVelocity, or flat horizon.setVelocity · HorizonCode Set Velocity · C++ HE::api::physics::setVelocity · action · entity is Self in HorizonCode
isGrounded(entity: int)
grounded: bool
Is this character controller standing on something right now? False for an entity that has no character controller. Lua/Python horizon.physics.isGrounded, or flat horizon.isGrounded · HorizonCode Is Grounded (Physics) · C++ HE::api::physics::isGrounded · query · entity is Self in HorizonCode
Like Raycast, but sweeps a sphere of Radius along the ray, so it catches what a thin ray slips past — a gap between two crates, the edge of a step. The extra cost is worth it for anything a character has to stand on. point is where the sphere's centre stopped, not the contact point. Ignores trigger volumes. Lua/Python horizon.physics.sphereCast · HorizonCode Sphere Cast · C++ HE::api::physics::sphereCast · query
overlapSphere(center: vec3, radius: float)
entities: int[]
Every entity whose collider overlaps this sphere, as an array. The explosion-radius query; walk the result with a For Each. From Lua/Python this returns 0 instead of a list (build 7d49d44f). Lua/Python horizon.physics.overlapSphere · HorizonCode Overlap Sphere · C++ HE::api::physics::overlapSphere · query · list: HorizonCode/C++ only
Raycast, but it only sees the collision layers you name. Layer Mask is a bitfield and not a layer number: add 1 for layer 0, 2 for layer 1, 4 for layer 2 and so on, or pass 65535 for every layer. Zero sees nothing at all. The layer names live in Project Settings under Collision Layers. Lua/Python horizon.physics.raycastLayers · HorizonCode Raycast (Layers) · C++ HE::api::physics::raycastLayers · query
Sphere Cast restricted to the collision layers in Layer Mask, which is a bitfield: 1 is layer 0, 2 is layer 1, 4 is layer 2, 65535 is all of them. What a camera boom wants, so it stops on walls and ignores the player. point is the sphere's centre where it stopped. Lua/Python horizon.physics.sphereCastLayers · HorizonCode Sphere Cast (Layers) · C++ HE::api::physics::sphereCastLayers · query
Overlap Sphere restricted to the collision layers in Layer Mask, a bitfield in the same shape as the two casts above. An explosion that hurts enemies and leaves the scenery alone is this node with one bit set. Lua/Python horizon.physics.overlapSphereLayers · HorizonCode Overlap Sphere (Layers) · C++ HE::api::physics::overlapSphereLayers · query · list: HorizonCode/C++ only
Sweeps a box along a direction and reports the first thing it touches. Half Extents is half the box's size on each axis, Rotation is its tilt in degrees, the same numbers the Details panel shows for an entity. Layer Mask is a bitfield: 1 is layer 0, 2 is layer 1, 65535 is all of them. Distance is where the box's centre stopped, not where it touched. point is the box's centre where it stopped. Ignores trigger volumes. Lua/Python horizon.physics.boxCast · HorizonCode Box Cast · C++ HE::api::physics::boxCast · query
Sweeps a capsule along a direction: the query that asks whether a character fits through a gap, using that character's own Radius and Height. Height is the full height including both rounded ends. Rotation tilts the capsule in degrees, Layer Mask is a bitfield as on the other casts. point is the capsule's centre where it stopped. Ignores trigger volumes. Lua/Python horizon.physics.capsuleCast · HorizonCode Capsule Cast · C++ HE::api::physics::capsuleCast · query
Every entity whose collider is inside this box right now, as an array. Half Extents is half the box's size on each axis and Rotation turns it, both in the same units the Details panel uses. A room, a corridor or a selection rectangle, where Overlap Sphere would also catch the corners. Lua/Python horizon.physics.overlapBox · HorizonCode Overlap Box · C++ HE::api::physics::overlapBox · query · list: HorizonCode/C++ only
Every entity inside an upright capsule, as an array. Height is the full height including both ends, and Rotation lays it on its side if you want that. The query a respawn makes before it puts a player somewhere: is anybody already standing there? Lua/Python horizon.physics.overlapCapsule · HorizonCode Overlap Capsule · C++ HE::api::physics::overlapCapsule · query · list: HorizonCode/C++ only
Fires a ray and reports EVERY body along it rather than the first, nearest first. The five outputs are parallel arrays, so index 3 of Points belongs to index 3 of Entities. A shot that passes through two enemies, or a line of sight that has to know it crossed a window. Each entity appears once. From Lua/Python this returns nine zeros instead of five lists (build 7d49d44f). Lua/Python horizon.physics.raycastAll · HorizonCode Raycast All · C++ HE::api::physics::raycastAll · query · list: HorizonCode/C++ only
addForce(entity: int, force: vec3)
ok: bool
Adds a continuous force (newtons) for this tick — call it every frame while it should push. Returns false when the entity has no dynamic body, which is the usual reason nothing moves. false on a static or kinematic body, which includes a character. Lua/Python horizon.physics.addForce · HorizonCode Add Force · C++ HE::api::physics::addForce · action · entity is Self in HorizonCode
addImpulse(entity: int, impulse: vec3)
ok: bool
Adds an instant change in momentum: a jump, a hit, an explosion. Call it once, not every frame. false on a static or kinematic body, which includes a character; a character jumps with locomotion.jump. Lua/Python horizon.physics.addImpulse · HorizonCode Add Impulse · C++ HE::api::physics::addImpulse · action · entity is Self in HorizonCode
addTorque(entity: int, torque: vec3)
ok: bool
Adds rotational force about the world axes, spinning the body. Lua/Python horizon.physics.addTorque · HorizonCode Add Torque · C++ HE::api::physics::addTorque · action · entity is Self in HorizonCode
A continuous force applied at a point rather than through the middle, so it turns the body as well as moving it. Position is a point in the WORLD, not an offset inside the entity, so a raycast hit point can be wired straight into it. Call it every frame while it should push. Lua/Python horizon.physics.addForceAtPosition · HorizonCode Add Force At Position · C++ HE::api::physics::addForceAtPosition · action · entity is Self in HorizonCode
An instant push at a point rather than through the middle: the crate tumbles away from the blast instead of sliding away from it. Position is a point in the WORLD, so a hit point wires straight in. Call it once, not every frame. Lua/Python horizon.physics.addImpulseAtPosition · HorizonCode Add Impulse At Position · C++ HE::api::physics::addImpulseAtPosition · action · entity is Self in HorizonCode
getVelocity(entity: int)
velocity: vec3
The body's current velocity in metres per second. Zero for an entity without a physics body. Lua/Python horizon.physics.getVelocity · HorizonCode Get Velocity (Physics) · C++ HE::api::physics::getVelocity · query · entity is Self in HorizonCode
Sets how fast the body spins, in RADIANS per second about the world axes and not in degrees. One full turn a second is about 6.28. It turns rigid bodies only: a character controller is kept upright and does not spin. Lua/Python horizon.physics.setAngularVelocity · HorizonCode Set Angular Velocity · C++ HE::api::physics::setAngularVelocity · action · entity is Self in HorizonCode
getAngularVelocity(entity: int)
angularVelocity: vec3
How fast the body is spinning, in RADIANS per second about the world axes and not in degrees. Zero for an entity without a rigid body. Lua/Python horizon.physics.getAngularVelocity · HorizonCode Get Angular Velocity · C++ HE::api::physics::getAngularVelocity · query · entity is Self in HorizonCode
setGravity(gravity: vec3)
—
Sets gravity for the WHOLE physics world, not for one entity. The default is (0, -9.81, 0). Rigid bodies only: a character falls by the gravity on its own Character Controller. Lua/Python horizon.physics.setGravity · HorizonCode Set Gravity · C++ HE::api::physics::setGravity · action
getGravity()
gravity: vec3
The physics world's gravity vector. Lua/Python horizon.physics.getGravity · HorizonCode Get Gravity · C++ HE::api::physics::getGravity · query
setPosition(entity: int, position: vec3)
ok: bool
Teleports the entity's physics body and its transform to this position in one step. Nothing sweeps on the way, and the velocity is kept — Set Position And Stop is the respawn variant. The position is local, relative to the parent, like horizon.setPosition. For a world point on a child use transform.setWorldPosition (HorizonCode only). false and a log line without a body. Lua/Python horizon.physics.setPosition · HorizonCode Set Position (Physics) · C++ HE::api::physics::setPosition · action · entity is Self in HorizonCode
setPositionAndReset(entity: int, position: vec3)
ok: bool
Teleports like Set Position and zeroes the velocity on arrival. What a respawn wants, so a player put back at a checkpoint does not keep the fall that killed them. Local position like setPosition; zeroes the linear and the angular velocity. Lua/Python horizon.physics.setPositionAndReset · HorizonCode Set Position And Stop (Physics) · C++ HE::api::physics::setPositionAndReset · action · entity is Self in HorizonCode
hasPhysics(entity: int)
has: bool
Whether the entity has a rigid body or a character controller at all. False without a physics world, and the honest answer to why a force did nothing. Lua/Python horizon.physics.hasPhysics · HorizonCode Has Physics · C++ HE::api::physics::hasPhysics · query · entity is Self in HorizonCode
Ties two rigid bodies together. Type picks the kind: 0 Fixed welds them, 1 Point is a ball socket, 2 Hinge turns on one axis, 3 Slider travels along one, 4 Distance keeps two points apart like a rope. Anchors are local to their own entity, Min and Max Limit are degrees for a hinge and metres for a slider, and Min above Max means no limit. A type outside 0–4 becomes Fixed. The first entity owns the joint. Lua/Python horizon.physics.addJoint · HorizonCode Add Joint · C++ HE::api::physics::addJoint · action
removeJoint(entity: int)
ok: bool
Cuts an entity's joint, both the constraint and the component, so it does not come back the next time the scene is loaded. The call that drops a grapple line or breaks a chain apart on purpose. Lua/Python horizon.physics.removeJoint · HorizonCode Remove Joint · C++ HE::api::physics::removeJoint · action · entity is Self in HorizonCode
hasJoint(entity: int)
has: bool
True while this entity is actually jointed to something in the simulation. A joint whose other end has not spawned yet is authored but not built, and this reports the simulation rather than the component. Only the owner, the first entity passed to addJoint, reports true. Lua/Python horizon.physics.hasJoint · HorizonCode Has Joint · C++ HE::api::physics::hasJoint · query · entity is Self in HorizonCode
Drives a hinge or a slider under its own power: the door swings open, the platform rises. Target Speed is radians per second for a hinge and metres per second for a slider. Max Force is the switch — zero turns the motor off, and a target of zero with force behind it is a brake that holds the joint still. The other three joint types have no motor. false and an error log on a Fixed, Point or Distance joint. Lua/Python horizon.physics.setJointMotor · HorizonCode Set Joint Motor · C++ HE::api::physics::setJointMotor · action · entity is Self in HorizonCode
How much force this joint carries before it lets go, in newtons; 0 never breaks. When it breaks the joint is gone for good — the component with it — and the pair shows up once in Poll Joint Broken. Lua/Python horizon.physics.setJointBreakForce · HorizonCode Set Joint Break Force · C++ HE::api::physics::setJointBreakForce · action · entity is Self in HorizonCode
Whether the two jointed bodies may touch each other. Off by default, which is what a chain of overlapping links needs; on for a door that must not swing through its own frame. Lua/Python horizon.physics.setJointCollideConnected · HorizonCode Set Joint Collide Connected · C++ HE::api::physics::setJointCollideConnected · action · entity is Self in HorizonCode
pollJointBroken()
entitiesA: int[], entitiesB: int[]
Every joint that BROKE since the last call, as two parallel arrays: index i of Entities A owned the joint, index i of Entities B was the other end. Reading empties the list, so read it in one place. A joint that was removed or whose entity was destroyed never appears here. From Lua/Python this returns 0, 0 and still empties the queue (build 7d49d44f). Lua/Python horizon.physics.pollJointBroken · HorizonCode Poll Joint Broken · C++ HE::api::physics::pollJointBroken · action · list: HorizonCode/C++ only
Sends an entity with a Nav Agent component across
the scene's baked NavMesh. The agent walks by itself once
moveTo has started it; if it has a Character Controller
it is steered through it, so it collides and falls like the player.
Every call on an entity without a Nav Agent does nothing, returns
false / -1 and logs has no NavAgentComponent.
Destinations are world positions, also for a
parented agent, and they have to lie on the walkable surface: a
point beside the mesh or a few metres above it is refused.
For a target that moves, call moveTo again; each call
plans a new route from where the agent stands.
moveTo plans the route inside the call, so its
false is final: no NavMesh, destination off it, or no
connection. A refused call also stops an agent that was already
walking, and the log line says which of the reasons it was.
After arrival isMoving and hasPath are
false and remainingDistance is -1, the
same as after stop or a refusal. It never reads 0, so
test "arrived" with isMoving. The agent stops within
the Nav Agent's Stopping Distance of the target.
setSpeed is the agent's own speed (default 3.5 m/s),
not the Movement component's Max Speed. A negative value becomes 0,
and an agent at speed 0 stands still while isMoving
stays true.
Example: nav.moveTo
patrol.lua
local M = {}
-- On an entity with a Nav Agent. World positions on the baked NavMesh.
local POINTS = { {6, 0, -6}, {6, 0, 6}, {-6, 0, 6} }
local function goNext(self)
self.i = self.i % #POINTS + 1
local p = POINTS[self.i]
if not horizon.nav.moveTo(self.entityId, p[1], p[2], p[3]) then
horizon.log("patrol point " .. self.i .. " is not reachable")
end
end
function M.onStart(self)
self.i = 0
goNext(self)
end
function M.onUpdate(self, dt)
-- isMoving turns false on arrival (and after a refusal).
if not horizon.nav.isMoving(self.entityId) then goNext(self) end
end
return M
patrol.py
import horizon
# On an entity with a Nav Agent. World positions on the baked NavMesh.
POINTS = [(6, 0, -6), (6, 0, 6), (-6, 0, 6)]
class Patrol(horizon.Behavior):
def on_start(self):
self.i = -1
self.go_next()
def go_next(self):
self.i = (self.i + 1) % len(POINTS)
if not horizon.nav.moveTo(self.entity_id, *POINTS[self.i]):
horizon.log(f"patrol point {self.i} is not reachable")
def on_update(self, dt):
# isMoving turns false on arrival (and after a refusal).
if not horizon.nav.isMoving(self.entity_id):
self.go_next()
Function
Returns
Description
moveTo(entity: int, x: float, y: float, z: float)
started: bool
Sends a Nav Agent walking to a world position: the route across the baked nav mesh is searched right here, and then followed over the next frames without anything else being called. The one node an enemy needs in order to come after the player. It answers whether the walk started, and the answer is worth branching on, because a false means the agent will not move at all: no nav mesh has been baked, the destination is off the walkable surface, or the two ends sit on parts of the level that do not connect. A refused call also stops an agent that was already walking somewhere. For a target that keeps moving, simply call it again — each call replans from where the agent stands. The point must lie on the NavMesh surface; the same x/z five metres above it is refused. Lua/Python horizon.nav.moveTo · HorizonCode Move To · C++ HE::api::nav::moveTo · action · entity is Self in HorizonCode
stop(entity: int)
—
Halts the agent where it stands and throws its route away. The way to break off a chase or hold a patrol still during a conversation. It leaves the destination as it was, so Move To starts a fresh walk afterwards. Lua/Python horizon.nav.stop · HorizonCode Stop Moving · C++ HE::api::nav::stop · action · entity is Self in HorizonCode
isMoving(entity: int)
moving: bool
Is this agent walking a route right now? It falls to false by itself the moment the agent arrives, which is how a patrol knows it is time to hand out the next waypoint. Lua/Python horizon.nav.isMoving · HorizonCode Is Moving · C++ HE::api::nav::isMoving · query · entity is Self in HorizonCode
hasPath(entity: int)
hasPath: bool
Whether the agent is holding a route it can still follow. Is Moving is the order that was given; this is whether there turned out to be a way. Telling the two apart is how "walking" is distinguished from "was sent somewhere it cannot reach". Lua/Python horizon.nav.hasPath · HorizonCode Has Path · C++ HE::api::nav::hasPath · query · entity is Self in HorizonCode
remainingDistance(entity: int)
distance: float
How far the agent still has to walk, measured in metres along the route's corners rather than straight through the walls between. Good for slowing an NPC down near its goal, or for deciding that a chaser is close enough to strike. It is -1, never 0, whenever there is no route to measure — an agent standing still, or one that has just arrived — so compare against 0 only after Has Path says there is something to compare. Lua/Python horizon.nav.remainingDistance · HorizonCode Remaining Distance · C++ HE::api::nav::remainingDistance · query · entity is Self in HorizonCode
setSpeed(entity: int, speed: float)
—
The speed in metres per second at which the agent walks its route from now on. The current route is kept and only the pace changes, so this is how one guard strolls a patrol and then charges. It is the Nav Agent's own speed, not the character's Max Speed, which belongs to input-driven movement; a negative value is taken as standing still. At speed 0 the agent stands still and isMoving stays true. Lua/Python horizon.nav.setSpeed · HorizonCode Set Agent Speed · C++ HE::api::nav::setSpeed · action · entity is Self in HorizonCode
What a character is doing right now, read from its Character
Controller. Queries only; to make a character move use
Locomotion. An entity without a Character
Controller reads 0 and false, without a log line.
All speeds are metres per second. speed is horizontal
only (falling is not running), verticalSpeed is
positive while rising.
forwardAmount and rightAmount are the same
horizontal velocity split along the character's facing, also
in metres per second, not a fraction: a character strafing
right at 5 m/s reads rightAmount 5. Divide by
speed for −1…1, and guard against a speed of 0.
Forward is the entity's −Z axis turned by its yaw.
isGrounded is the same answer as
physics.isGrounded and the flat
horizon.isGrounded, and the one
locomotion.jump checks.
Function
Returns
Description
speed(entity: int)
speed: float
How fast the character is moving across the ground, in metres per second, ignoring any vertical motion. The value an animator's walk/run blend wants. Lua/Python horizon.movement.speed · HorizonCode Get Speed · C++ HE::api::movement::speed · query · entity is Self in HorizonCode
verticalSpeed(entity: int)
speed: float
The character's vertical speed in metres per second: positive rising, negative falling. What tells a jump from a fall. Lua/Python horizon.movement.verticalSpeed · HorizonCode Get Vertical Speed · C++ HE::api::movement::verticalSpeed · query · entity is Self in HorizonCode
isGrounded(entity: int)
grounded: bool
Is the character standing on something? Read from the character controller, so it is the same answer physics used this tick. Lua/Python horizon.movement.isGrounded · HorizonCode Is Grounded · C++ HE::api::movement::isGrounded · query · entity is Self in HorizonCode
velocity(entity: int)
velocity: vec3
The character's full velocity vector in metres per second. Lua/Python horizon.movement.velocity · HorizonCode Get Velocity · C++ HE::api::movement::velocity · query · entity is Self in HorizonCode
forwardAmount(entity: int)
amount: float
How fast the character moves the way it is facing, in metres per second: positive ahead, negative backwards, 0 when still or moving sideways. It is a speed and not a -1 to 1 fraction, so divide by Get Speed for that. One half of a locomotion blend space. Lua/Python horizon.movement.forwardAmount · HorizonCode Get Forward Amount · C++ HE::api::movement::forwardAmount · query · entity is Self in HorizonCode
rightAmount(entity: int)
amount: float
The sideways half of the same pair, also in metres per second: negative strafing left, positive strafing right. Lua/Python horizon.movement.rightAmount · HorizonCode Get Right Amount · C++ HE::api::movement::rightAmount · query · entity is Self in HorizonCode
Tells a character what to do this frame. move,
look, setMaxSpeed and
setOrientToMovement write the entity's
Movement component and do nothing, silently, on an
entity without one; the jumps need a Character Controller and log
when there is none. A Player Character gets both components
automatically. The Movement system turns the request into velocity
once per frame before physics steps.
move is an intent for one frame: call it
every frame the character should walk. Two calls in the same frame
add up, and the sum is cut back to length 1, so
move(e, 1, 0, 0) twice still walks at the top speed,
not twice it. Length 0.5 walks at half speed.
The direction is read as the Movement component's Move
Direction Is setting says: World (the default)
takes it as it is, Camera (the default for a Player
Character) turns it by the main camera rig's heading, so
(0, 0, -1) is "away from the camera". With an input
action from the WASD preset, forward is
move(e, x, 0, -y).
The Y component is dropped, but only after the length is cut to 1:
move(e, 0.5, 3, 0) walks at 0.16 of the top speed.
Pass 0 for Y.
look adds its yaw to the character's rotation;
several calls in a frame add up. The pitch is accepted and
ignored, nothing reads it (build 7d49d44f). Tilt the view through
the camera rig with
camera.addYawPitch.
setOrientToMovement(e, true) turns the character
towards its travel direction at the component's turn rate
(720°/s by default). Leave it off while look or a
camera rig owns the facing, or the two fight each other.
jump uses the Character Controller's Jump Speed,
jumpWith a speed of its own. Both return
true only when the character left the ground; a
second call in the same frame, a call in mid-air or a speed of 0
or less returns false. A jump within 0.12 s of walking
off a ledge is still granted.
Example: locomotion.move
player_move.lua
local M = {}
-- On a Player Character. "Move" is an Axis2D input action (the WASD
-- preset: W is +y), "Sprint" and "Jump" are button actions.
function M.onUpdate(self, dt)
local e = self.entityId
local x, y = horizon.input.actionAxis2D("Move")
-- Forward is -Z. With "Move Direction Is: Camera" (the Player Character
-- default) that is away from the camera.
horizon.locomotion.move(e, x, 0, -y)
horizon.locomotion.setMaxSpeed(e, horizon.input.actionDown("Sprint") and 8 or 5)
end
function M.onInputPressed(self, name)
if name == "Jump" and horizon.locomotion.jump(self.entityId) then
horizon.log("jump")
end
end
return M
player_move.py
import horizon
class PlayerMove(horizon.Behavior):
# On a Player Character. "Move" is an Axis2D input action (the WASD
# preset: W is +y), "Sprint" and "Jump" are button actions.
def on_update(self, dt):
e = self.entity_id
x, y = horizon.input.actionAxis2D("Move")
# Forward is -Z. With "Move Direction Is: Camera" (the Player Character
# default) that is away from the camera.
horizon.locomotion.move(e, x, 0, -y)
horizon.locomotion.setMaxSpeed(e, 8 if horizon.input.actionDown("Sprint") else 5)
def on_input_pressed(self, name):
if name == "Jump" and horizon.locomotion.jump(self.entity_id):
horizon.log("jump")
Function
Returns
Description
move(entity: int, direction: vec3)
—
Tells the character to move in a direction this frame; the length is the throttle, 0 to 1, and anything longer is cut back to 1. The direction is read as the Movement component's "Move Direction Is" says: World by default, the main camera's heading for a Player Character. Up and down are ignored. It has to be called every frame it should move — the intent is cleared at the end of each one, so a missed frame reads as "stopped" rather than as "keep going". Two calls in one frame add up. Does nothing, without a log line, on an entity without a Movement component. The Y component counts towards the length before it is dropped. Lua/Python horizon.locomotion.move · HorizonCode Move · C++ HE::api::locomotion::move · action · entity is Self in HorizonCode
look(entity: int, yaw: float, pitch: float)
—
Turns the character by Yaw degrees this frame. Pitch is accepted but nothing reads it yet; tilt the view with the camera rig instead. Several calls in a frame add up. Lua/Python horizon.locomotion.look · HorizonCode Look · C++ HE::api::locomotion::look · action · entity is Self in HorizonCode
setMaxSpeed(entity: int, speed: float)
—
The character's top speed in metres per second at full input. Changing it is how sprinting and wading are done. Default 5 m/s. Not the speed of a Nav Agent, see nav.setSpeed. Lua/Python horizon.locomotion.setMaxSpeed · HorizonCode Set Max Speed · C++ HE::api::locomotion::setMaxSpeed · action · entity is Self in HorizonCode
setOrientToMovement(entity: int, on: bool)
—
Whether the character turns to face the way it walks. Switch it off while something else owns the facing — a camera rig with coupled rotation, an aim mode. Lua/Python horizon.locomotion.setOrientToMovement · HorizonCode Set Orient To Movement · C++ HE::api::locomotion::setOrientToMovement · action · entity is Self in HorizonCode
jump(entity: int)
jumped: bool
Throws the character upward at the Jump Speed its Character Controller was authored with. Unlike Move this is a single event, not an intent held per frame: call it once when the button goes down. It returns whether the character actually left the ground, and refuses in mid-air — which is what stops a held button from flying. So when a jump never fires, look at Is Grounded before you look at the speed. It is not quite that strict: a jump asked for within a tenth of a second of walking off a ledge is still granted, because a player who presses a frame late means to jump. Lua/Python horizon.locomotion.jump · HorizonCode Jump · C++ HE::api::locomotion::jump · action · entity is Self in HorizonCode
jumpWith(entity: int, speed: float)
jumped: bool
A jump at the upward speed given here in metres per second, instead of the authored one. For the jumps one number cannot cover: a charge that grows while the button is held, a low hop out of a crouch, a launch pad that throws whoever steps on it. The ground rule is the same, and a speed of zero or less is refused rather than treated as a jump. Lua/Python horizon.locomotion.jumpWith · HorizonCode Jump With Speed · C++ HE::api::locomotion::jumpWith · action · entity is Self in HorizonCode
Which player controller drives which character. The references here
are HorizonCode object references, not entity ids:
a controller is an instance of a PlayerController class, a
character an instance of a PlayerCharacter class. In Lua and
Python they are plain integers, 0 meaning "none". To get from one to
an entity use horizon.entity.owned(ref),
the other way horizon.entity.instance(e).
controller() is the session's first controller, 0 in a
project without a PlayerController class; character()
is what it possesses. Both are 0 outside a play session.
A controller drives one character and a character has one
controller: possess releases the controller's previous
character and takes the new one away from any other controller.
possess(0, …) does nothing.
The table only records numbers and checks none of them, so a
wrong reference is stored as it is. Pass what
controller() and entity.instance return.
A Lua or Python script is not a controller, so possession does not
decide which script hears input: input actions reach every script
(see Callbacks). What possession changes is
which HorizonCode character graph receives the controller's input
events, and which character the camera rig follows.
Function
Returns
Description
possess(controller: ref, character: ref)
—
Hands a character to a player controller: from here on the controller's input drives it, and the camera rig follows it. Possessing a second character releases the first. In Lua/Python both references are integers; possess(0, …) does nothing. Lua/Python horizon.player.possess · HorizonCode Possess · C++ HE::api::player::possess · action
unpossess(controller: ref)
—
Releases whatever the controller was driving. The character stays in the world, just without anyone at the controls. Lua/Python horizon.player.unpossess · HorizonCode Un Possess · C++ HE::api::player::unpossess · action
possessed(controller: ref)
character: ref
The character this controller is currently driving, or an empty reference. Lua/Python horizon.player.possessed · HorizonCode Get Possessed Character · C++ HE::api::player::possessed · query
controllerOf(character: ref)
controller: ref
Which controller is driving this character — the inverse of Possessed. Lua/Python horizon.player.controllerOf · HorizonCode Get Controller · C++ HE::api::player::controllerOf · query
controller()
controller: ref
The local player's controller. The starting point in a single-player game, where there is exactly one. 0 in a project without a PlayerController class. A reference, not an entity: entity.owned gives the entity. Lua/Python horizon.player.controller · HorizonCode Get Player Controller · C++ HE::api::player::controller · query
character()
character: ref
The character the local player is driving right now. Shorthand for Possessed of Controller. A reference, not an entity: horizon.entity.owned(horizon.player.character()) is the character's entity. Lua/Python horizon.player.character · HorizonCode Get Player Character · C++ HE::api::player::character · query
Polling the keyboard, mouse, gamepad and the project's input actions.
The engine takes one snapshot per frame, so every query gives the same
answer for the rest of that frame. Only a running game or play mode
fills it; in the editor outside play the values are stale.
For one-shot reactions prefer the action callbacks
(onInputPressed and friends)
over polling.
Key names are SDL scancode names and case-sensitive:
"W", "Space", "Left Shift",
"Return", "Escape", "Up".
keyDown("w") is always false.
Mouse buttons: 0 left, 1 right, 2 middle.
mousePosition and mouseDelta return two
numbers in Lua and a tuple (x, y) in Python.
Gamepad names are SDL's, not case-sensitive:
buttons "a", "b", "x",
"y", "leftshoulder", "dpup",
"start"; axes "leftx", "lefty",
"rightx", "righty", "lefttrigger",
"righttrigger". "south" and
"cross" are not names and read false.
Sticks go −1…1 with Y positive downwards, triggers
0…1, and the deadzone is already applied.
Actions are looked up by the InputAction asset's
name, case-sensitive; an unknown name reads false / 0.
onUpdate runs before the actions are updated, so a
poll from there sees the previous frame's state;
actionPressed is still true for exactly one frame per
press. The callbacks always carry the current frame. While the game
is paused or the input mode is UI only, an action reads as released
unless its asset has Fires while the game is paused ticked.
Input modes decide who receives input. The default is
GameAndUI: both do, and a click on a widget is masked out
of the game. GameOnly leaves the UI drawn but deaf.
UIOnly gives the UI everything: the game then reads no
keys, no mouse buttons and no gamepad (it reports disconnected), only
mousePosition stays true. The mode does not show or hide
the cursor; do that with the flat
horizon.showCursor /
hideCursor. Play mode starts in GameAndUI.
▲
Known issue: scrollDelta is always 0 in
the running game and in play mode (build 7d49d44f). The per-frame
snapshot is filled without the mouse wheel. Read the wheel through
an input action instead: bind the Mouse Wheel source to an Axis
action and poll it with actionAxis or handle
onInputAxis.
Example: input.setModeGameAndUI
inventory.lua
local M = {}
-- "Inventory" is a button action. While the inventory is open the cursor is
-- visible and clicks go to the UI; the game still reads the keys.
function M.onInputPressed(self, name)
if name ~= "Inventory" then return end
self.open = not self.open
if self.open then
horizon.input.setModeGameAndUI()
horizon.showCursor()
else
horizon.input.setModeGameOnly()
horizon.hideCursor()
end
end
return M
inventory.py
import horizon
class Inventory(horizon.Behavior):
open = False
# "Inventory" is a button action. While the inventory is open the cursor
# is visible and clicks go to the UI; the game still reads the keys.
def on_input_pressed(self, name):
if name != "Inventory":
return
self.open = not self.open
if self.open:
horizon.input.setModeGameAndUI()
horizon.showCursor()
else:
horizon.input.setModeGameOnly()
horizon.hideCursor()
Function
Returns
Description
keyDown(key: string)
down: bool
Is this key held down right now? The name is SDL's spelling ("W", "Space", "Escape"). True for as long as it is held, so use it for movement and put one-shot actions behind an Input Action instead. Case-sensitive: "w" is always false. Two-word keys read "Left Shift", "Left Ctrl". Lua/Python horizon.input.keyDown · HorizonCode Key Down · C++ HE::api::input::keyDown · query
mouseButton(button: int)
down: bool
Is this mouse button held? 0 left, 1 right, 2 middle. Lua/Python horizon.input.mouseButton · HorizonCode Mouse Button · C++ HE::api::input::mouseButton · query
mousePosition()
position: vec2
The pointer's position in window pixels, measured from the top-left. Lua/Python horizon.input.mousePosition · HorizonCode Mouse Position · C++ HE::api::input::mousePosition · query
mouseDelta()
delta: vec2
How far the pointer moved since the previous frame, in pixels. This is what mouse look reads — not the position, which stops changing once the cursor is captured. Lua/Python horizon.input.mouseDelta · HorizonCode Mouse Delta · C++ HE::api::input::mouseDelta · query
scrollDelta()
scroll: float
How far the wheel turned this frame; positive is away from the user. Always 0 in the running game and in play mode (build 7d49d44f): the wheel is not put into the snapshot. Bind Mouse Wheel to an Axis action and use actionAxis. Lua/Python horizon.input.scrollDelta · HorizonCode Scroll Delta · C++ HE::api::input::scrollDelta · query
gamepadConnected()
connected: bool
Is a gamepad plugged in? Worth checking before showing button prompts. Lua/Python horizon.input.gamepadConnected · HorizonCode Gamepad Connected · C++ HE::api::input::gamepadConnected · query
gamepadButton(button: string)
down: bool
Is this gamepad button held? SDL's names, so "a", "b", "dpup" — not "south" and not "cross". Not case-sensitive; "south" and "cross" are not names and read false. Lua/Python horizon.input.gamepadButton · HorizonCode Gamepad Button · C++ HE::api::input::gamepadButton · query
gamepadAxis(axis: string)
value: float
A gamepad axis from -1 to 1, deadzone already applied. Names are SDL's: "leftx", "lefty", "righttrigger". Y axes are positive downwards, as in SDL. Lua/Python horizon.input.gamepadAxis · HorizonCode Gamepad Axis · C++ HE::api::input::gamepadAxis · query
actionDown(action: string)
down: bool
Whether an input action (by the name of its InputAction asset, "Jump") is held this frame. The polling twin of the Input.<Action>.Pressed event: ask this inside Tick rather than keeping a flag yourself. Reads as released while the game is paused or the UI has the input. Lua/Python horizon.input.actionDown · HorizonCode Input Action Down · C++ HE::api::input::actionDown · query
actionPressed(action: string)
pressed: bool
Whether an input action went down THIS frame — true for one frame per press, the same moment Input.<Action>.Pressed fires. Polled from onUpdate this is last frame's press (still exactly one frame per press); onInputPressed is current. Lua/Python horizon.input.actionPressed · HorizonCode Input Action Pressed · C++ HE::api::input::actionPressed · query
actionReleased(action: string)
released: bool
Whether an input action went up THIS frame — true for one frame per release, the same moment Input.<Action>.Released fires. Lua/Python horizon.input.actionReleased · HorizonCode Input Action Released · C++ HE::api::input::actionReleased · query
actionAxis(action: string)
value: float
The value of a one-dimensional axis action this frame, as the mapping context resolved it (-1 to 1 for keys and sticks, a displacement for the mouse). Zero while silenced by a pause or the UI. Lua/Python horizon.input.actionAxis · HorizonCode Input Action Axis · C++ HE::api::input::actionAxis · query
actionAxis2D(action: string)
value: vec2
The value of a two-dimensional axis action this frame, as a Vec2 — a stick, WASD, the mouse. Zero while silenced by a pause or the UI. Lua/Python horizon.input.actionAxis2D · HorizonCode Input Action Axis 2D · C++ HE::api::input::actionAxis2D · query
setModeGameOnly()
—
The game receives input and the UI does not: the cursor is captured and hidden, mouse look works, buttons cannot be clicked. First-person play. Lua/Python horizon.input.setModeGameOnly · HorizonCode Set Input Mode: Game Only · C++ HE::api::input::setModeGameOnly · action
setModeGameAndUI()
—
Both receive input — the cursor is visible, the UI is clickable, and the game still reads keys. Inventory screens and build modes. The default, and what play mode starts in. Lua/Python horizon.input.setModeGameAndUI · HorizonCode Set Input Mode: Game and UI · C++ HE::api::input::setModeGameAndUI · action
setModeUIOnly()
—
Only the UI receives input; the game sees nothing. What a pause menu wants, so a click behind it cannot fire a weapon. Also reports the gamepad as disconnected to the game. Actions with Fires while the game is paused still work, so the key that opened the menu can close it. Lua/Python horizon.input.setModeUIOnly · HorizonCode Set Input Mode: UI Only · C++ HE::api::input::setModeUIOnly · action
mode()
mode: string
Which input mode is active: "GameOnly", "GameAndUI" or "UIOnly". Lua/Python horizon.input.mode · HorizonCode Input Mode · C++ HE::api::input::modeName · query
Steering a character's animation from gameplay: the parameters its
animator state machine tests, and the weights of its animation layers.
Parameters live on the entity's Animator State Machine component,
layers on its Animation Layers component; without the component the
setters do nothing and the getters answer 0 or an empty string.
Parameters are numbers.setParam takes
a float. A yes/no parameter is 1 or 0: in Lua
setParam(e, "Grounded", true) is an error (number
expected, got boolean), Python turns True into 1.
A name the state machine asset never declared is created, not
rejected, so a typo writes a parameter nothing reads.
getParam answers 0 for a name that was never set.
Layers are found by name, first match. Two layers
may share a name; every function then acts on the first of them.
setLayerWeight clamps to 0 to 1.
playLayer restarts that layer's own playhead and
leaves its weight alone.
notifiesOf and layerNames return lists,
which scripts cannot receive yet: from Lua and Python they answer an
empty string (see List results). Notifies
themselves reach a script as the
onAnimationNotify callbacks.
notifiesOf loads the clip if it is not in memory.
Example: animator.setParam
anim_speed.lua
local M = {}
-- On a character with Movement and an Animator State Machine whose
-- transitions test "Speed" (m/s) and "Grounded" (1 or 0).
function M.onUpdate(self, dt)
local e = self.entityId
horizon.animator.setParam(e, "Speed", horizon.movement.speed(e))
-- Parameters are numbers: a boolean has to become 1 or 0.
horizon.animator.setParam(e, "Grounded", horizon.movement.isGrounded(e) and 1 or 0)
end
return M
anim_speed.py
import horizon
class AnimSpeed(horizon.Behavior):
# On a character with Movement and an Animator State Machine whose
# transitions test "Speed" (m/s) and "Grounded" (1 or 0).
def on_update(self, dt):
e = self.entity_id
horizon.animator.setParam(e, "Speed", horizon.movement.speed(e))
# Parameters are numbers: a boolean has to become 1 or 0.
horizon.animator.setParam(e, "Grounded", 1 if horizon.movement.isGrounded(e) else 0)
Function
Returns
Description
setParam(entity: int, name: string, value: float)
—
Writes a named parameter of the entity's animator state machine — the values its transitions test. This is how gameplay drives animation: set Speed, let the graph decide between walk and run. The value is a number: in Lua true is an error, Python turns True into 1. A name the asset never declared is created. Lua/Python horizon.animator.setParam · HorizonCode Set Animator Param · C++ HE::api::animator::setParam · action · entity is Self in HorizonCode
getParam(entity: int, name: string)
value: float
Reads a named animator parameter back. 0 for an unknown name or an entity without an animator. Lua/Python horizon.animator.getParam · HorizonCode Get Animator Param · C++ HE::api::animator::getParam · query · entity is Self in HorizonCode
getState(entity: int)
state: string
The name of the state playing right now, as the animator asset spells it. Empty when there is no state machine. Lua/Python horizon.animator.getState · HorizonCode Get Animator State · C++ HE::api::animator::getState · query · entity is Self in HorizonCode
notifiesOf(clipPath: string)
names: string[]
The notify names written on an animation clip's timeline, by the clip's asset path. Notify names are free text with nothing checking them, so a misspelled one fires nothing and says nothing — this is how to see what a clip actually carries. Empty for an unknown path or a clip without any. Loads the clip if it is not in memory yet. Lua/Python horizon.animator.notifiesOf · HorizonCode Get Clip Notifies · C++ HE::api::animator::notifiesOf · query · list: HorizonCode/C++ only
How strongly one animation layer is laid over the base pose, 0 to 1. The layer is named the way it is named in the Animation Layers component — by name and not by position, because reordering the stack must not silently fade a different body part. Clamped; unknown names do nothing. With two layers of the same name, the first one. Lua/Python horizon.animator.setLayerWeight · HorizonCode Set Layer Weight · C++ HE::api::animator::setLayerWeight · action · entity is Self in HorizonCode
getLayerWeight(entity: int, layer: string)
weight: float
The weight an animation layer is currently at. 0 for an unknown layer or an entity without an Animation Layers component. With two layers of the same name, the first one. Lua/Python horizon.animator.getLayerWeight · HorizonCode Get Layer Weight · C++ HE::api::animator::getLayerWeight · query · entity is Self in HorizonCode
playLayer(entity: int, layer: string)
—
Restarts one layer's own playhead at 0, so a one-shot layer (a reload, a flinch) plays again instead of resuming where it stopped. The weight is left alone: fading in is a separate decision and usually takes frames. Lua/Python horizon.animator.playLayer · HorizonCode Play Layer · C++ HE::api::animator::playLayer · action · entity is Self in HorizonCode
layerNames(entity: int)
names: string[]
The layer names on an entity, in the order they are laid onto the base pose. Layer names are free text with nothing checking them, so this is how to see what a character actually carries. Lua/Python horizon.animator.layerNames · HorizonCode Get Layer Names · C++ HE::api::animator::layerNames · query · entity is Self in HorizonCode · list: HorizonCode/C++ only
Firing and stopping the Particle System component on an entity. An
emitter plays on its own from the start (its Playing box is
ticked by default); these functions are for effects that have to happen
at a moment. Calling play, stop or
burst on an entity without a Particle System writes a
warning to the log (at most every five seconds) and does nothing;
isPlaying answers false without a warning,
because asking is how a script finds out.
One-shots. An emitter whose graph does not loop emits
its Max Particles once and finishes. play
restarts it from the beginning, however often.
isPlaying stays true until the last
particle has died, not only until the emitting ends.
stop is soft. It stops the emitting and
lets the particles that are out live their lifetime, so
isPlaying is still true right after it and
turns false when the last one dies.
burst(e, count) emits at the emitter's
current world position and returns how many it actually made: never
more than Max Particles alive at once (with 10 alive and a
cap of 30, a burst of 500 makes 20), and 0 for a count of 0 or
below. A burst also restarts a stopped or finished emitter.
Example: particle.burst
impact.lua
local M = {}
-- On a crate with a Particle System whose graph does not loop (sparks):
-- every hit fires a handful of sparks and gives the camera a short jolt.
function M.onCollisionEnter(self, other)
local made = horizon.particle.burst(self.entityId, 24)
if made > 0 then
-- 5 cm, 0.5 degrees, 18 shakes per second, a quarter of a second.
horizon.camera.playShake(0.05, 0.5, 18, 0.25)
end
end
return M
impact.py
import horizon
class Impact(horizon.Behavior):
# On a crate with a Particle System whose graph does not loop (sparks):
# every hit fires a handful of sparks and gives the camera a short jolt.
def on_collision_enter(self, other):
made = horizon.particle.burst(self.entity_id, 24)
if made > 0:
# 5 cm, 0.5 degrees, 18 shakes per second, a quarter of a second.
horizon.camera.playShake(0.05, 0.5, 18, 0.25)
Function
Returns
Description
play(entity: int)
—
Starts the emitter, or starts it again from the beginning. A one-shot that has already run needs this to run a second time: it resets the burst it has emitted, so firing the same effect twice fires it twice. Particles still in the air are left alone, which is what lets a fast repeat overlap its own tail. Also on an emitter that is still running: its one-shot count starts again. Lua/Python horizon.particle.play · HorizonCode Play Effect · C++ HE::api::particle::play · action · entity is Self in HorizonCode
stop(entity: int)
—
Stops the emitter producing anything more, while the particles already out live out their lifetime and fade normally. Deliberately not the same as hiding the entity, which leaves it simulating unseen, nor the same as a pause, which would freeze a cloud of smoke in mid-air. isPlaying stays true until the last particle already out has died. Lua/Python horizon.particle.stop · HorizonCode Stop Effect · C++ HE::api::particle::stop · action · entity is Self in HorizonCode
burst(entity: int, count: int)
emitted: int
Emits a number of particles at once from an entity's Particle System, ignoring its emit rate entirely. This is what a hit, a muzzle flash or a footstep is: so many at a moment, which a rate cannot express — an emit rate high enough to look instant depends on the frame rate. It answers how many were actually made, which can be fewer than asked for when the emitter's Max Particles is already reached. Emits at the emitter's current world position. Also restarts a stopped or finished emitter. 0 for a count of 0 or below. Lua/Python horizon.particle.burst · HorizonCode Burst Particles · C++ HE::api::particle::burst · action · entity is Self in HorizonCode
isPlaying(entity: int)
playing: bool
Whether the effect is still running — either still emitting, or with particles still alive. It goes false the moment a one-shot has completely finished, which is how a graph waits for an effect instead of guessing a duration. false without a Particle System, and no warning. Lua/Python horizon.particle.isPlaying · HorizonCode Is Effect Playing · C++ HE::api::particle::isPlaying · query · entity is Self in HorizonCode
Sounds by asset path, controlled afterwards through the handle
play or playAt returns. Handles are whole
numbers counting up from 1; 0 means nothing was
started: the asset path did not load, or there is no audio
engine. The audio engine belongs to a play session, so outside one
(the editor while not playing, a tool script) every function here does
nothing and the queries answer 0, false, or 1 for the
pitch.
Every argument is required.play(asset, volume,
pitch, loop) has no defaults in Python (a missing
loop is an IndexError); in Lua a missing
loop is read as false.
playAt takes the position as three numbers:
playAt(asset, x, y, z, volume, pitch, loop, minDist,
maxDist).
Stop what you start. A sound that finishes on its
own is no longer playing, but the engine keeps it, with its own copy
of the clip, until it is stopped: its handle still answers its volume
and pitch, and fifty finished one-shots are fifty held voices (build
7d49d44f). For sounds fired again and again (footsteps, shots) call
stop on the previous handle before playing the next,
or stopAll at a natural break. A scene change stops
everything.
Transport.isPlaying is
false both for a paused and for a finished sound;
isPaused tells them apart. seek clamps to
the clip (5 s into a 1 s clip lands at 1 s, −2 at 0) and works while
paused; getTime stands still while paused.
Values are not clamped, with one exception: a
volume above 1 amplifies and a negative one is stored as it is,
while a pitch of 0 or below is ignored and the old pitch stays.
Spatial sounds are heard from the scene's Audio
Listener (the first one). Without a listener the ears stay at the
world origin. setSoundPosition only matters for a sound
started with playAt.
setBusVolume creates a bus that does not exist yet, so a
misspelled bus name silently becomes a new, empty bus.
Example: audio.playAt
footsteps.lua
local M = {}
local STEP = "Audio/footstep.hasset"
-- The walk clip carries "FootL" and "FootR" notifies on its timeline.
function M.onAnimationNotify(self, name)
if name ~= "FootL" and name ~= "FootR" then return end
-- A finished sound is held until it is stopped, so free the last step.
if self.step then horizon.audio.stop(self.step) end
local x, y, z = horizon.getPosition(self.entityId)
local pitch = horizon.random.range(0.9, 1.1) -- no two steps alike
self.step = horizon.audio.playAt(STEP, x, y, z, 0.7, pitch, false, 1, 25)
end
return M
footsteps.py
import horizon
STEP = "Audio/footstep.hasset"
class Footsteps(horizon.Behavior):
step = 0
# The walk clip carries "FootL" and "FootR" notifies on its timeline.
def on_animation_notify(self, name):
if name not in ("FootL", "FootR"):
return
# A finished sound is held until it is stopped, so free the last step.
if self.step:
horizon.audio.stop(self.step)
x, y, z = horizon.getPosition(self.entity_id)
pitch = horizon.random.range(0.9, 1.1) # no two steps alike
self.step = horizon.audio.playAt(STEP, x, y, z, 0.7, pitch, False, 1, 25)
Example: audio.setVolume
music_fade.lua
local M = {}
local MUSIC = "Audio/theme.hasset"
local VOLUME, FADE = 0.6, 2.0 -- fade length in seconds
function M.onStart(self)
self.music = horizon.audio.play(MUSIC, VOLUME, 1, true)
self.left = 0
end
-- "EndLevel" is a button action.
function M.onInputPressed(self, name)
if name == "EndLevel" and self.music ~= 0 then self.left = FADE end
end
function M.onUpdate(self, dt)
if self.left <= 0 then return end
self.left = self.left - dt
if self.left <= 0 then
horizon.audio.stop(self.music) -- stop, not volume 0: frees the voice
self.music = 0
else
horizon.audio.setVolume(self.music, VOLUME * self.left / FADE)
end
end
return M
music_fade.py
import horizon
MUSIC = "Audio/theme.hasset"
VOLUME, FADE = 0.6, 2.0 # fade length in seconds
class MusicFade(horizon.Behavior):
def on_start(self):
self.music = horizon.audio.play(MUSIC, VOLUME, 1, True)
self.left = 0.0
# "EndLevel" is a button action.
def on_input_pressed(self, name):
if name == "EndLevel" and self.music != 0:
self.left = FADE
def on_update(self, dt):
if self.left <= 0:
return
self.left -= dt
if self.left <= 0:
horizon.audio.stop(self.music) # stop, not volume 0: frees the voice
self.music = 0
else:
horizon.audio.setVolume(self.music, VOLUME * self.left / FADE)
Plays a sound flat — the same in both ears, wherever the listener is. Music, UI clicks, narration. The handle it returns is what Stop and Is Playing take; a one-shot can simply drop it. 0 when the asset does not load or outside a play session. In Lua a missing loop reads as false; Python needs all four arguments. Lua/Python horizon.audio.play · HorizonCode Play Sound · C++ HE::api::audio::play · action
Plays a sound at a world position, quieter with distance. Full volume inside Min Dist, silent past Max Dist. Distance is measured from the scene's Audio Listener; without one the ears stay at the world origin. The position is three numbers: playAt(asset, x, y, z, volume, pitch, loop, minDist, maxDist). Lua/Python horizon.audio.playAt · HorizonCode Play Sound At · C++ HE::api::audio::playAt · action
stop(handle: int)
—
Stops one playing sound by handle. A handle that has already finished is harmless. The only way, besides stopAll, to free a sound that finished by itself (see the group notes). Lua/Python horizon.audio.stop · HorizonCode Stop Sound · C++ HE::api::audio::stop · action
stopAll()
—
Stops every sound at once — what a scene change or a hard cut wants. Lua/Python horizon.audio.stopAll · HorizonCode Stop All Sounds · C++ HE::api::audio::stopAll · action
isPlaying(handle: int)
playing: bool
Is this handle still sounding? False once it has finished or been stopped. Lua/Python horizon.audio.isPlaying · HorizonCode Is Sound Playing · C++ HE::api::audio::isPlaying · query
setBusVolume(bus: string, volume: float)
—
Sets the volume of a mixer bus by name, so a whole group — music, sfx, voice — moves together. This is what a settings screen writes to. Creates the bus when none has that name, so a misspelled name is a new, empty bus. Lua/Python horizon.audio.setBusVolume · HorizonCode Set Bus Volume · C++ HE::api::audio::setBusVolume · action
setSoundPosition(handle: int, position: vec3)
—
Moves a playing spatial sound to a new world position. For a sound that has to follow something that moves while it plays. Lua/Python horizon.audio.setSoundPosition · HorizonCode Set Sound Position · C++ HE::api::audio::setSoundPosition · action
pause(handle: int)
—
Pauses one sound by handle, keeping its place. Resume Sound picks up exactly there. Unlike Stop, the sound is still alive afterwards — a pause menu wants this, a scene change wants Stop. Lua/Python horizon.audio.pause · HorizonCode Pause Sound · C++ HE::api::audio::pause · action
resume(handle: int)
—
Continues a paused sound from where Pause Sound left it. Harmless on a sound that is not paused. Lua/Python horizon.audio.resume · HorizonCode Resume Sound · C++ HE::api::audio::resume · action
isPaused(handle: int)
paused: bool
Is this handle paused? Is Playing says false for a paused sound AND for one that has finished; this is how to tell them apart. Lua/Python horizon.audio.isPaused · HorizonCode Is Sound Paused · C++ HE::api::audio::isPaused · query
setVolume(handle: int, volume: float)
—
Changes the volume of one playing sound. 1 is as played, 0 is silent; a fade is this called every tick with a shrinking value. Not clamped: above 1 amplifies, a negative value is stored as it is. Lua/Python horizon.audio.setVolume · HorizonCode Set Sound Volume · C++ HE::api::audio::setVolume · action
getVolume(handle: int)
volume: float
The current volume of one playing sound — what Play or Set Sound Volume last gave it. 0 for a handle that is gone. A sound that finished by itself but was not stopped still answers its last volume. Lua/Python horizon.audio.getVolume · HorizonCode Get Sound Volume · C++ HE::api::audio::getVolume · query
setPitch(handle: int, pitch: float)
—
Changes the pitch of one playing sound. 1 is as recorded, 2 is an octave up and twice as fast, 0.5 an octave down. An engine note or a slow-motion moment is this. A pitch of 0 or below is ignored; the old pitch stays. Lua/Python horizon.audio.setPitch · HorizonCode Set Sound Pitch · C++ HE::api::audio::setPitch · action
getPitch(handle: int)
pitch: float
The current pitch of one playing sound. 1 for a handle that is gone. Lua/Python horizon.audio.getPitch · HorizonCode Get Sound Pitch · C++ HE::api::audio::getPitch · query
setLooping(handle: int, loop: bool)
—
Turns looping on or off for a sound that is already playing — a loop that should end after its current pass gets looping switched off, not Stop. Lua/Python horizon.audio.setLooping · HorizonCode Set Sound Looping · C++ HE::api::audio::setLooping · action
seek(handle: int, seconds: float)
—
Jumps a playing sound to a time in seconds from its start. Past the end goes to the end, below zero to the start. Works on a paused sound too. Lua/Python horizon.audio.seek · HorizonCode Seek Sound · C++ HE::api::audio::seek · action
getTime(handle: int)
seconds: float
How far into the sound the playhead is, in seconds — of the clip itself, so pitch does not stretch it. 0 for a handle that is gone. Lua/Python horizon.audio.getTime · HorizonCode Get Sound Time · C++ HE::api::audio::getTime · query
getLength(handle: int)
seconds: float
How long the sound is, in seconds. Together with Get Sound Time this is a progress bar. 0 for a handle that is gone. Lua/Python horizon.audio.getLength · HorizonCode Get Sound Length · C++ HE::api::audio::getLength · query
The camera the picture comes from, and the camera rig that moves it. No
function here takes an entity: they all act on the active
camera, the one marked Main, or the first camera in
the scene when none is. The rig functions act on that camera's Camera
Rig component; without one they do nothing and read 0.
Position and rotation are the camera's own transform.getPosition and getRotation read it and the
setters write it, relative to the camera's parent. A camera parented
under an entity at (10, 0, 0) and sitting 5 m behind it reads
0, 2, 5, not 10, 2, 5. For a camera at the
top of the hierarchy that is the world position. While a rig drives
the camera it rewrites the transform every frame, so
setPosition/setRotation last until the next
frame; setFov stays.
Rig numbers are forgiving, not checked.setRigMode treats anything but 0 as third person.
setTargetYawMode treats anything but 0 and 2 as Follow.
addYawPitch clamps the pitch to the rig's limits (by
default −80° to 75°) but lets the yaw run past 360.
setLagSpeeds turns negative speeds into 0.
setArmLength takes a negative length as it is, which
puts the camera in front of the target.
Shake moves the transform, the FOV kick does not.
The rig writes the shaken pose into the camera's transform, so
getPosition and getRotation jitter while a
shake runs. A FOV kick is added on top of the field of view and
getFov keeps answering the authored value.
playShake returns 0 when there is no rig; handles start
at 1.
Blending.blendTo(camera, seconds, curve)
makes that camera the main one at once (every other camera loses
Main), so from the next call on the other functions act on
it. The ease itself needs a Camera Rig on the camera blended to;
without one it is a cut and isBlending stays
false. Curves outside 0 to 2 become smoothstep.
Example: camera.kickFov
sprint_fov.lua
local M = {}
-- "Sprint" is a button action. The kick widens the view for a moment and
-- falls back by itself; getFov keeps answering the authored value.
function M.onInputPressed(self, name)
if name == "Sprint" then
horizon.camera.kickFov(8, 0.15, 0.6, 0.4) -- degrees, attack, hold, decay
horizon.camera.setLagEnabled(true) -- the camera trails a little
end
end
function M.onInputReleased(self, name)
if name == "Sprint" then horizon.camera.setLagEnabled(false) end
end
return M
sprint_fov.py
import horizon
class SprintFov(horizon.Behavior):
# "Sprint" is a button action. The kick widens the view for a moment and
# falls back by itself; getFov keeps answering the authored value.
def on_input_pressed(self, name):
if name == "Sprint":
horizon.camera.kickFov(8, 0.15, 0.6, 0.4) # degrees, attack, hold, decay
horizon.camera.setLagEnabled(True) # the camera trails a little
def on_input_released(self, name):
if name == "Sprint":
horizon.camera.setLagEnabled(False)
Function
Returns
Description
getPosition()
position: vec3
The active camera's position, read from its own transform: relative to its parent when it has one, so it is the world position only for a camera at the top of the hierarchy. The active camera is the one marked Main, else the first camera in the scene. Lua/Python horizon.camera.getPosition · HorizonCode Get Camera Position · C++ HE::api::camera::getPosition · query
setPosition(position: vec3)
—
Moves the active camera. Ignored the moment a camera rig is driving it — the rig recomputes the position from its target every frame. Relative to the camera's parent. While a rig drives the camera it lasts one frame. Lua/Python horizon.camera.setPosition · HorizonCode Set Camera Position · C++ HE::api::camera::setPosition · action
getRotation()
rotation: vec3
The active camera's rotation as euler degrees, from its own transform — relative to its parent, like Get Camera Position. Lua/Python horizon.camera.getRotation · HorizonCode Get Camera Rotation · C++ HE::api::camera::getRotation · query
setRotation(rotation: vec3)
—
Turns the active camera. Same caveat as Set Position: a rig overrides it. While a rig drives the camera it lasts one frame. Lua/Python horizon.camera.setRotation · HorizonCode Set Camera Rotation · C++ HE::api::camera::setRotation · action
getFov()
degrees: float
The vertical field of view in degrees. Lua/Python horizon.camera.getFov · HorizonCode Get Camera FOV · C++ HE::api::camera::getFov · query
setFov(degrees: float)
—
Sets the vertical field of view in degrees. Widening it while sprinting is the classic speed cue; animate it rather than snapping. Lua/Python horizon.camera.setFov · HorizonCode Set Camera FOV · C++ HE::api::camera::setFov · action
setRigMode(mode: int)
—
Switches the camera rig between first person (0) and third person (1). First person is the same rig with an arm length of zero. Any value other than 0 is third person. Lua/Python horizon.camera.setRigMode · HorizonCode Set Camera Mode · C++ HE::api::camera::setRigMode · action
getRigMode()
mode: int
0 first person, 1 third person. 0 when there is no rig at all. Lua/Python horizon.camera.getRigMode · HorizonCode Get Camera Mode · C++ HE::api::camera::getRigMode · query
setRigTarget(entity: int)
—
Points the rig at an entity to follow. Setting it to nothing puts the rig back on whichever character the player possesses. 0, or an id that is not an entity, also means the possessed character. Lua/Python horizon.camera.setRigTarget · HorizonCode Set Camera Target · C++ HE::api::camera::setRigTarget · action · entity is Self in HorizonCode
setArmLength(length: float)
—
How far behind the target the third-person camera sits, in metres. 0 puts it in the target's head. Not clamped: a negative length puts the camera in front of the target. Lua/Python horizon.camera.setArmLength · HorizonCode Set Camera Distance · C++ HE::api::camera::setArmLength · action
getArmLength()
length: float
The rig's current arm length in metres. Lua/Python horizon.camera.getArmLength · HorizonCode Get Camera Distance · C++ HE::api::camera::getArmLength · query
setTargetYawMode(mode: int)
—
Free (0) lets the character face where it walks while the camera looks elsewhere. Follow (1) turns the character with the camera, which is what makes strafing and walking backwards work. Follow Smoothed (2) does the same, but swings the body after the camera at the rig's turn rate. Any value other than 0 and 2 is Follow. Lua/Python horizon.camera.setTargetYawMode · HorizonCode Set Target Rotation Mode · C++ HE::api::camera::setTargetYawMode · action
getTargetYawMode()
mode: int
0 Free (the character faces where it walks), 1 Follow (it turns with the camera), 2 Follow Smoothed (it swings after the camera). Lua/Python horizon.camera.getTargetYawMode · HorizonCode Get Target Rotation Mode · C++ HE::api::camera::getTargetYawMode · query
getRigYaw()
degrees: float
Which way the rig is looking, in degrees. Lua/Python horizon.camera.getRigYaw · HorizonCode Get Camera Yaw · C++ HE::api::camera::getRigYaw · query
getRigPitch()
degrees: float
How far up or down the rig is looking, in degrees. Lua/Python horizon.camera.getRigPitch · HorizonCode Get Camera Pitch · C++ HE::api::camera::getRigPitch · query
addYawPitch(deltaYaw: float, deltaPitch: float)
—
Turns the rig by these degrees, clamped to the pitch limits the rig carries. This is what a look input drives — adding to the angles rather than setting them is what keeps two input sources from fighting. A default rig limits the pitch to −80° … 75°. The yaw is not wrapped: it runs past 360. Lua/Python horizon.camera.addYawPitch · HorizonCode Turn Camera · C++ HE::api::camera::addYawPitch · action
setLagEnabled(enabled: bool)
—
Switches camera lag on or off. With it on the camera eases after its target instead of being welded to it; off is the default, so an existing scene looks exactly as it did. Lua/Python horizon.camera.setLagEnabled · HorizonCode Set Camera Lag · C++ HE::api::camera::setLagEnabled · action
getLagEnabled()
enabled: bool
Whether the rig is easing after its target rather than following it rigidly. Lua/Python horizon.camera.getLagEnabled · HorizonCode Get Camera Lag · C++ HE::api::camera::getLagEnabled · query
setLagSpeeds(position: float, rotation: float)
—
How quickly the camera catches up, per second: the first number moves the camera to its target, the second swings the boom round to the look direction. Larger is tighter; 0 never catches up at all. Negative speeds become 0. Lua/Python horizon.camera.setLagSpeeds · HorizonCode Set Camera Lag Speeds · C++ HE::api::camera::setLagSpeeds · action
snapRig()
—
Puts the camera exactly where it belongs on the next frame instead of easing into it. This is what a teleport, a respawn or a cut needs — without it the camera sails across the level to catch up. Lua/Python horizon.camera.snapRig · HorizonCode Snap Camera · C++ HE::api::camera::snapRig · action
Shakes the camera: a position amplitude in metres, a rotation amplitude in degrees, a frequency in shakes per second and a duration. A duration of 0 or less runs until it is stopped, which is what the handle it returns is for; a one-shot can drop it. 0 without a rig. The shake is written into the camera's transform, so getPosition jitters while it runs and returns to the unshaken pose afterwards. Lua/Python horizon.camera.playShake · HorizonCode Play Camera Shake · C++ HE::api::camera::playShake · action
stopShake(handle: int)
—
Ends one running shake by the handle Play Camera Shake gave out. An unknown or already-finished handle does nothing. Lua/Python horizon.camera.stopShake · HorizonCode Stop Camera Shake · C++ HE::api::camera::stopShake · action
stopAllShakes()
—
Ends every shake on the camera at once — what a cutscene or a death screen wants, rather than remembering each handle. Lua/Python horizon.camera.stopAllShakes · HorizonCode Stop All Camera Shakes · C++ HE::api::camera::stopAllShakes · action
Pushes the field of view by these degrees and lets it fall back: attack, hold and decay in seconds. Negative degrees zoom in. It never changes the camera's own FOV setting, so Get Camera FOV keeps answering what the author set. Added on top of the field of view; getFov does not include it. Lua/Python horizon.camera.kickFov · HorizonCode Kick Camera FOV · C++ HE::api::camera::kickFov · action
blendTo(camera: int, seconds: float, curve: int)
—
Hands the view to another camera over a number of seconds. Curve 0 is linear, 1 smoothstep, 2 ease-out. 0 seconds is a straight cut, and so is switching the main camera by hand — a blend only ever starts here. The camera blended to needs a camera rig of its own; to a camera without one this is always a cut. The new camera is Main at once. A curve outside 0 to 2 becomes smoothstep. Lua/Python horizon.camera.blendTo · HorizonCode Blend To Camera · C++ HE::api::camera::blendTo · action
isBlending()
blending: bool
Whether the picture is currently easing in from another camera. True until the blend has fully arrived. Lua/Python horizon.camera.isBlending · HorizonCode Is Camera Blending · C++ HE::api::camera::isBlending · query
Presentation · 2 functions
Material
◆
Lua and Python have no horizon.material.*: use the flat horizon.getMaterialParam / setMaterialParam, which take the value as one to four numbers.
The named parameters of a node-graph material, the ones its
Param nodes declare, read and written at runtime. Every
parameter holds four numbers whatever the shader reads, so a scalar
parameter is its first number. From Lua and Python,
horizon.setMaterialParam(e, name, x [, y, z, w]) fills
missing numbers with 0 and returns false when the entity
has no Material component or the material declares no such parameter;
horizon.getMaterialParam returns four zeros then.
▲
Known issue (build 7d49d44f): a set changes every entity with
the same material. The value is written into the material
asset in memory, not onto the entity. Set a parameter on one crate and
every crate sharing its material changes. An entity that overrides the
parameter in its Details panel keeps its override, and
getParam does not see that override: it reads the asset.
Until this is fixed, give an entity that needs its own value its own
material asset.
Function
Returns
Description
getParam(entity: int, name: string)
value: color
Reads one named parameter of the entity's material as a colour. The name is the one the material graph's Param node declares; all four components are stored, whatever the shader reads. It reads the material asset's value, not an override set on this entity's Details panel. Lua/Python only as flat horizon.getMaterialParam · HorizonCode Get Material Param · C++ HE::api::material::getParam · query · entity is Self in HorizonCode
setParam(entity: int, name: string, value: color)
ok: bool
Writes one named parameter of the entity's material. It writes the material ASSET, not this entity: every entity using the same material changes with it, while an entity whose Details panel overrides this parameter keeps its override. Ok is false when the entity has no material or the parameter is not declared. Known issue: every entity with the same material changes too (see the group notes). Lua/Python only as flat horizon.setMaterialParam · HorizonCode Set Material Param · C++ HE::api::material::setParam · action · entity is Self in HorizonCode
The sky, the day-night cycle and the weather look of the scene: every
field of the Environment component on the Sky entity, which the
Environment window adds. If a scene carries more than one Sky, these
functions and the renderer both use the first. The table below is
generated from the engine's own field list, so it always has every
field there is.
Colours take four numbers. A color field
is set from Lua and Python with r, g, b, a:
horizon.env.setSunColor(1, 0.8, 0.6, 1). The fourth
number is ignored, but it has to be there: with three, Lua raises
bad argument #4 and Python IndexError. The
getters return four numbers with alpha always 1. int
fields need a whole number (1.7 is an error in both languages);
bool fields accept true/false
or, in Python, 1/0.
Nothing is clamped. A setter stores exactly what it
is given: a time of day of 1.7, a negative fog density or a cloud
quality of 9 read back unchanged. Stay inside the ranges the
descriptions give. Time of day is wrapped into 0 to 1 only when
Auto Advance moves it on.
Time of day needs the cycle switched on.setTimeOfDay only moves the sun while
DayNightCycle is true; the Details panel switches that
on for you when you drag the slider, a script does not. For a clock
that runs by itself set AutoAdvance as well: with a
CycleSeconds of 10, one second moves the time by 0.1,
and with MoonPhaseAuto the moon phase moves one cycle
per MoonCycleDays days.
Weather. A Weather component writes six of these
fields (cloud coverage, fog density, wind speed, rain, snow, wetness)
toward its preset, blending over its transition time. It backs off a
field as soon as its value differs from the last value Weather wrote,
so a value a script sets stays until a new preset is
selected, which takes all six back. The lightning
Flash is rewritten every tick. Every other field is
never touched by Weather.
No Sky entity (removed in the Environment window):
every getter answers 0, false or black, and every
setter does nothing. A time of day of 0 then does not mean midnight.
Example: env.setTimeOfDay
sleep.lua
local M = {}
-- "Sleep" is a button action: skip to the next morning.
function M.onInputPressed(self, name)
if name ~= "Sleep" then return end
horizon.env.setDayNightCycle(true) -- without it the sun ignores the clock
horizon.env.setTimeOfDay(0.3) -- a little after sunrise
-- Colours are four numbers; the fourth is ignored but required.
horizon.env.setSunColor(1.0, 0.85, 0.7, 1)
-- Weather lets go of a field a script changed, until the next preset.
horizon.env.setFogDensity(0.02)
end
return M
sleep.py
import horizon
class Sleep(horizon.Behavior):
# "Sleep" is a button action: skip to the next morning.
def on_input_pressed(self, name):
if name != "Sleep":
return
horizon.env.setDayNightCycle(True) # without it the sun ignores the clock
horizon.env.setTimeOfDay(0.3) # a little after sunrise
# Colours are four numbers; the fourth is ignored but required.
horizon.env.setSunColor(1.0, 0.85, 0.7, 1)
# Weather lets go of a field a script changed, until the next preset.
horizon.env.setFogDensity(0.02)
All 58 fields as one table, 116 functions: for a
field Name, horizon.env.getName() reads it
and horizon.env.setName(value) writes it; in
HorizonCode they are Get … and Set … nodes, in C++
HE::api::env::getName(ctx) /
setName(ctx, value). Setters are actions, getters
queries. Each field is the value of the same name on the Sky entity's
Environment component, which its Details panel shows. The
Weather column says what a Weather component in the scene
does to the field.
Field
Type
Weather
Description
Time Of Day getTimeOfDay / setTimeOfDay
float
—
The sky's clock: 0 and 1 are midnight, 0.25 sunrise, 0.5 noon. It only moves the sun while Day Night Cycle is on. Not wrapped by the setter: 1.7 reads back as 1.7 until Auto Advance moves the clock on.
Day Cycle Seconds getCycleSeconds / setCycleSeconds
float
—
How many real seconds a full day takes while Auto Advance runs the clock.
Sun Intensity getSunIntensity / setSunIntensity
float
—
How strong the sun is, and with it the whole daylit scene.
These rows work on entities that carry UI components:
an element on a UI canvas in the scene, addressed by its entity id like
any other entity. Widgets built in the Widget Designer are a different
thing with their own ids, see Widget. From a
script, pass the id: the element the script sits on is
self.entityId (Python self.entity_id),
another one is found once with horizon.entity.findByName.
Which component answers. The text rows need a UI
Text component: setText on anything else does nothing and
getText answers "". The colour is the Image
tint, else the Text colour, else a Button's normal colour, in that
order. An element with none of the three ignores
setColor and reads as white (1, 1, 1, 1). Visibility,
position and size belong to every element; an entity without UI
answers false and 0, 0.
A colour is four numbers and a position or size two:
setColor(e, 1, 0.5, 0, 1), setPosition(e, x, y).
With three colour numbers Lua throws bad argument #5 and
Python an IndexError. A missing visible
reads as false in Lua, so the element disappears; Python
throws.
Nothing is clamped or checked: a negative size is
stored as it is.
pointerOverUI asks the widget layer: it is
true while the pointer is over a widget, and while a
widget dialog (widget.showModal) is open, wherever the
pointer is. UI elements on a scene canvas do not count.
▲
Known issue (build 7d49d44f): setMaterialParam
changes every element with the same material. The value is
written into the material asset in memory, not onto the element, the
same as material.setParam. Two
progress bars sharing one fill material show the same fill. Give an
element that needs its own value its own material asset.
Example: ui.setText
level_clock.lua
local M = {}
local LIMIT = 90 -- seconds for the level
-- This script sits on the HUD's UI Text entity.
function M.onStart(self)
self.left = LIMIT
end
function M.onUpdate(self, dt)
if self.left <= 0 then return end
self.left = math.max(0, self.left - dt)
local e = self.entityId
horizon.ui.setText(e, string.format("%d:%02d", self.left // 60, math.floor(self.left) % 60))
if self.left < 10 then
horizon.ui.setColor(e, 1, 0.25, 0.2, 1) -- a colour is four numbers
end
if self.left == 0 then
horizon.ui.setVisible(e, false)
end
end
return M
level_clock.py
import horizon
LIMIT = 90 # seconds for the level
# This script sits on the HUD's UI Text entity.
class LevelClock(horizon.Behavior):
def on_start(self):
self.left = LIMIT
def on_update(self, dt):
if self.left <= 0:
return
self.left = max(0.0, self.left - dt)
e = self.entity_id
horizon.ui.setText(e, "%d:%02d" % (self.left // 60, int(self.left) % 60))
if self.left < 10:
horizon.ui.setColor(e, 1, 0.25, 0.2, 1) # a colour is four numbers
if self.left == 0:
horizon.ui.setVisible(e, False)
Function
Returns
Description
getText(entity: int)
text: string
The text of a UI Text element, by entity. Empty for an element that is not one. Lua/Python horizon.ui.getText, or flat horizon.getUIText · HorizonCode Get UI Text · C++ HE::api::ui::getText · query · entity is Self in HorizonCode
setText(entity: int, text: string)
—
Replaces a UI Text element's text. The usual way a score, a timer or a subtitle reaches the screen. Only a UI Text element takes it; on any other element it does nothing. Lua/Python horizon.ui.setText, or flat horizon.setUIText · HorizonCode Set UI Text · C++ HE::api::ui::setText · action · entity is Self in HorizonCode
getColor(entity: int)
color: color
The element's colour, alpha included: an Image's tint, else a Text's colour, else a Button's normal colour. White for an element with none of the three. (1, 1, 1, 1) for an element with no Image, Text or Button. Lua/Python horizon.ui.getColor, or flat horizon.getUIColor · HorizonCode Get UI Color · C++ HE::api::ui::getColor · query · entity is Self in HorizonCode
setColor(entity: int, color: color)
—
Sets the element's colour. On an Image element it is the tint over the texture, on a Text element the text itself, on a Button its normal colour. Any other element ignores it. Four numbers: setColor(e, r, g, b, a). Image tint first, else Text colour, else a Button's normal colour; any other element ignores it. Lua/Python horizon.ui.setColor, or flat horizon.setUIColor · HorizonCode Set UI Color · C++ HE::api::ui::setColor · action · entity is Self in HorizonCode
getVisible(entity: int)
visible: bool
Is this UI element currently shown? Lua/Python horizon.ui.getVisible, or flat horizon.isUIVisible · HorizonCode Get UI Visible · C++ HE::api::ui::getVisible · query · entity is Self in HorizonCode
setVisible(entity: int, visible: bool)
—
Shows or hides one UI element. Hiding a container hides everything inside it, and a hidden element stops receiving clicks. In Lua a missing visible reads as false and hides the element. Lua/Python horizon.ui.setVisible, or flat horizon.setUIVisible · HorizonCode Set UI Visible · C++ HE::api::ui::setVisible · action · entity is Self in HorizonCode
getPosition(entity: int)
position: vec2
The element's position inside its canvas, in UI units from its anchor. Lua/Python horizon.ui.getPosition, or flat horizon.getUIPosition · HorizonCode Get UI Position · C++ HE::api::ui::getPosition · query · entity is Self in HorizonCode
setPosition(entity: int, position: vec2)
—
Moves a UI element inside its canvas. Measured from its anchor, so what this means depends on which corner or edge the element is anchored to. Lua/Python horizon.ui.setPosition, or flat horizon.setUIPosition · HorizonCode Set UI Position · C++ HE::api::ui::setPosition · action · entity is Self in HorizonCode
getSize(entity: int)
size: vec2
The element's width and height in UI units. Lua/Python horizon.ui.getSize, or flat horizon.getUISize · HorizonCode Get UI Size · C++ HE::api::ui::getSize · query · entity is Self in HorizonCode
setSize(entity: int, size: vec2)
—
Resizes a UI element. A stretched anchor overrides this on the axes it stretches. Not checked: a negative size is stored as it is. Lua/Python horizon.ui.setSize, or flat horizon.setUISize · HorizonCode Set UI Size · C++ HE::api::ui::setSize · action · entity is Self in HorizonCode
Sets a material parameter for a UI Image element that uses a material — a progress bar's fill, a shader-driven panel. It writes the MATERIAL ASSET, not the element: every element and entity using the same material changes with it, so give an element that needs its own value its own material. Ok is false when the element has no material or the parameter is not declared. The value is four numbers. Known issue: it writes the material asset, so every element sharing that material changes (see the group notes). Lua/Python horizon.ui.setMaterialParam, or flat horizon.setUIMaterialParam · HorizonCode Set UI Material Param · C++ HE::api::ui::setMaterialParam · action · entity is Self in HorizonCode
pointerOverUI()
over: bool
Is the pointer over any UI right now? The guard to put in front of a click that acts on the world, so a press on a button does not also shoot. Counts widgets only, and is true the whole time a widget dialog is open. Lua/Python horizon.ui.pointerOverUI · HorizonCode Is Pointer Over UI · C++ HE::api::ui::pointerOverUI · query
Live instances of Widget Designer assets. A widget is addressed by the
id that horizon.createWidget(path)
returns (0 when the asset does not load); showing, hiding and destroying
it are the flat showWidget, hideWidget and
destroyWidget. A new widget starts hidden until
showWidget (or showModal, openPopup,
window.show) puts it on screen. Widgets
live outside the entity world, so a widget id is not an entity id, and
in an exported game a script and a HorizonCode graph share the same
widgets.
Always pass the widget. Several descriptions say
“leave Widget unwired” for the calling widget's own. That is a
HorizonCode graph living in the widget; a script has no widget of its
own, and 0 answers false or 0.
Scripts receive none of the widget events.On Row
Bind, On Selection Changed, On Dismissed,
On Right Clicked, On Animation Finished and On Clip
Finished go to the widget's own HorizonCode graph only. Neither
are there Set External, Call External or Bind
Event for scripts. In practice: a script can size, select and
scroll a ListView, but its rows are filled by the row widget's own
graph; a script notices a closed dialog by asking
isVisible each frame (see the example under
showModal).
References are not widget ids.addChild, listRow and childRef
return object references for those HorizonCode nodes;
isVisible on one answers false. From a
script a child reference is good for removeChild.
Animation ok checks less than it seems.
It is false for a missing widget or element, but a
property name the element does not have is accepted and animates
nothing, and an unknown easing name runs as "Linear". The
easings are "Linear", "In Quad",
"Out Quad", "In Out Quad",
"In Cubic", "Out Cubic",
"In Out Cubic" and "Out Back"; directions for
the clip rows are "Forward", "Backward" and
"Ping Pong" (anything else is Forward). Colours are four
numbers, points two: animateColor(w, "Card", "Color", 1, 0, 0,
1, 0.5, "Linear").
Lists are named by the element's name in the
designer. A negative count becomes 0; an index at or past the count
leaves the selection as it was, and still answers true.
listRow is 0 for an item that is scrolled out of view.
Closing is hiding. After closeTopLayer,
Escape or Back, a dialog or popup widget answers
isVisiblefalse; show it again to reopen it.
Example: widget.showModal
pause_dialog.lua
local M = {}
local MENU = "UI/pause_menu.hasset" -- a widget with an element called "Panel"
function M.onStart(self)
self.menu = horizon.createWidget(MENU) -- starts hidden; 0 if the asset is missing
end
function M.onInputPressed(self, name)
if name ~= "Pause" or self.paused or self.menu == 0 then return end
self.paused = true
horizon.time.pause()
horizon.showCursor()
-- A dialog: dims the game, takes the input, closes on Escape or Back.
horizon.widget.showModal(self.menu)
horizon.widget.animate(self.menu, "Panel", "Render Opacity", 0, 0, "Linear")
horizon.widget.animate(self.menu, "Panel", "Render Opacity", 1, 0.2, "Out Quad")
end
function M.onUpdate(self, dt)
-- A script gets no On Dismissed: notice the close by asking.
if self.paused and not horizon.widget.isVisible(self.menu) then
self.paused = false
horizon.time.resume()
horizon.hideCursor()
end
end
return M
pause_dialog.py
import horizon
MENU = "UI/pause_menu.hasset" # a widget with an element called "Panel"
class PauseMenu(horizon.Behavior):
paused = False
def on_start(self):
self.menu = horizon.createWidget(MENU) # starts hidden; 0 if the asset is missing
def on_input_pressed(self, name):
if name != "Pause" or self.paused or self.menu == 0:
return
self.paused = True
horizon.time.pause()
horizon.showCursor()
# A dialog: dims the game, takes the input, closes on Escape or Back.
horizon.widget.showModal(self.menu)
horizon.widget.animate(self.menu, "Panel", "Render Opacity", 0, 0, "Linear")
horizon.widget.animate(self.menu, "Panel", "Render Opacity", 1, 0.2, "Out Quad")
def on_update(self, dt):
# A script gets no On Dismissed: notice the close by asking.
if self.paused and not horizon.widget.isVisible(self.menu):
self.paused = False
horizon.time.resume()
horizon.hideCursor()
Function
Returns
Description
setZOrder(widget: ref, z: int)
—
Which widget draws over which: higher is nearer the front. For keeping a pause menu above a HUD. Takes the Widget from a Create Widget node. Lua/Python horizon.widget.setZOrder, or flat horizon.setWidgetZOrder · HorizonCode Set Widget Z-Order · C++ HE::api::widget::setZOrder · action
isVisible(widget: ref)
visible: bool
Is this widget on screen? A new widget starts hidden, so this is false until Show Widget (or Show Modal, Open Popup), again after Hide Widget or a closed dialog, and for a Widget that was destroyed. false right after createWidget, and for a child or row reference, which is not a widget id. Lua/Python horizon.widget.isVisible, or flat horizon.isWidgetVisible · HorizonCode Is Widget Visible · C++ HE::api::widget::isVisible · query
callFunction(widget: ref, function: string)
ok: bool
Calls a public function on the widget's own graph by name — the way the outside talks to a screen. Ok is false when the widget or the function is not there. Only a widget with a HorizonCode graph has functions to call. Lua/Python horizon.widget.callFunction, or flat horizon.callWidgetFunction · HorizonCode Call Widget Function · C++ HE::api::widget::callFunction · action
Puts another widget INSIDE this one, while it runs: the asset is grafted under the element you name, so a Vertical Box called "List" stacks a row per call. That is how a list of unknown length is built — a todo list, search results, a chat. The Child that comes back is a live object: Set External writes its public variables, Call External runs its functions, Bind Event listens to what it emits. 0 when the widget, the element or the asset is not there. The reference is for HorizonCode's Set/Call External; from a script it is good for removeChild. Lua/Python horizon.widget.addChild · HorizonCode Add Widget Child · C++ HE::api::widget::addChild · action
removeChild(widget: ref, child: ref)
ok: bool
Takes one of those rows out again, by the Child the Add gave you. Its elements and its logic both go. Ok is false when that child is not in this widget (already removed, or it belongs to another one). Lua/Python horizon.widget.removeChild · HorizonCode Remove Widget Child · C++ HE::api::widget::removeChild · action
clearChildren(widget: ref, parent: string)
removed: int
Empties one element of everything Add Widget Child put in it, and says how many went. The short way to rebuild a list from scratch instead of tracking every row you added. Lua/Python horizon.widget.clearChildren · HorizonCode Clear Widget Children · C++ HE::api::widget::clearChildren · action
Tells a ListView how many items there are. It keeps no items of its own: it works out which of them fit on screen, puts up only those rows, and asks you to fill each one in through On Row Bind. That is why ten thousand items cost ten rows instead of ten thousand elements. Name the list by the name its element has in the designer. Safe to call from inside On Row Bind — an endless list that loads more when its last row appears is written exactly that way; the new count takes effect on the next frame. A negative count becomes 0. Rows are filled by the row widget's own graph: a script gets no On Row Bind. Lua/Python horizon.widget.setListCount · HorizonCode Set List Count · C++ HE::api::widget::setListCount · action
listCount(widget: ref, list: string)
count: int
How many items the list was last told it has. Not how many rows are on screen — that is the list's own business. Lua/Python horizon.widget.listCount · HorizonCode Get List Count · C++ HE::api::widget::listCount · query
listRow(widget: ref, list: string, index: int)
row: ref
The live row showing that item, or 0 when it is scrolled out of sight. This is what an On Row Bind handler writes into: Set External for the row's public variables, Call External for its functions. Do not hold on to it — the same row is pointed at a different item as soon as the list scrolls. A reference, not a widget id; a script has no Set External to write into it. Lua/Python horizon.widget.listRow · HorizonCode Get List Row · C++ HE::api::widget::listRow · query
refreshList(widget: ref, list: string)
ok: bool
Asks every row on screen to be filled in again, without moving anything. What you call after sorting, filtering or editing your data: the list never saw it, so it cannot notice that it changed. The selection is by INDEX, so after a sort it points at whichever items now sit at those positions — remap it yourself if it has to follow the same rows. Calling this from inside On Row Bind is allowed: it takes effect on the next frame rather than looping back into the bind that asked for it. Lua/Python horizon.widget.refreshList · HorizonCode Refresh List · C++ HE::api::widget::refreshList · action
Picks or unpicks one item. An index of -1 clears the selection. Does nothing when the list's Selection is None; in Single mode picking one drops the other. Fires On Selection Changed when the set really changed. An index at or past the count changes nothing and still answers true. A script gets no On Selection Changed. Lua/Python horizon.widget.setListSelected · HorizonCode Set List Selected · C++ HE::api::widget::setListSelected · action
listSelected(widget: ref, list: string)
index: int
The picked item's index, or -1 when nothing is picked. With Multiple selection this is the lowest of them. Lua/Python horizon.widget.listSelected · HorizonCode Get List Selected · C++ HE::api::widget::listSelected · query
Scrolls just far enough for that item to be fully visible, and does nothing when it already is — so stepping through a list does not re-centre it on every step. Lua/Python horizon.widget.scrollListToItem · HorizonCode Scroll List To Item · C++ HE::api::widget::scrollListToItem · action
Moves a NUMBER of one element from where it is to where you say, over the given seconds, along an easing curve ("Linear", "Out Quad", "Out Back"…). Opacity, corner radius, rotation, font size. Zero seconds writes it at once. Starting a second one on the same property replaces the first from wherever the value has got to, so a value can be retargeted mid-flight without a jump. OnAnimationFinished says when it lands, and names the property it was. ok is true for a property name the element does not have (nothing moves), and an unknown easing runs as "Linear". Lua/Python horizon.widget.animate · HorizonCode Animate Number · C++ HE::api::widget::animate · action · widget is Self in HorizonCode
The same for a COLOUR — a fade, a flash, a hover that arrives instead of snapping. Every colour an element has can be animated, including a button's hovered and pressed ones. The colour is four numbers, as everywhere. Lua/Python horizon.widget.animateColor · HorizonCode Animate Color · C++ HE::api::widget::animateColor · action · widget is Self in HorizonCode
The same for a POINT: Position to slide something in, Size to grow it. "Out Back" is the curve that makes a dialog land rather than arrive. The point is two numbers. Lua/Python horizon.widget.animateVec2 · HorizonCode Animate Position · C++ HE::api::widget::animateVec2 · action · widget is Self in HorizonCode
Stops what is running on that property, or on the whole element when the property is left empty. The value stays where it got to — a stop is not a rewind — and a stopped animation reports nothing, because cancelled is not finished. Lua/Python horizon.widget.stopAnimation · HorizonCode Stop Animating Property · C++ HE::api::widget::stopAnimation · action · widget is Self in HorizonCode
Plays one of the animations the widget carries — the ones made in the Designer's timeline — picked from the dropdown. Leave Widget unwired and it plays this widget's own. Whether it loops is the animation's own decision. Direction runs it forwards, backwards, or out and back. Restore After Completed puts the properties it moved back the way they were when it finishes, which is the whole of "flash this and undo it". An embedded component's animations count too, so a page can play what its component brought with it. OnClipFinished says when it ends; a looping one never does, and never restores either. A clip name the widget does not carry answers false. Directions: "Forward", "Backward", "Ping Pong". Lua/Python horizon.widget.playAnimation · HorizonCode Play Animation · C++ HE::api::widget::playAnimation · action · widget is Self in HorizonCode
The same, but you decide about looping instead of the animation. For the case where one clip is both the spinner that runs until you stop it and the flourish that plays once. Lua/Python horizon.widget.playAnimationLooped · HorizonCode Play Animation Looped · C++ HE::api::widget::playAnimationLooped · action · widget is Self in HorizonCode
stopAnimationClip(widget: ref, animation: string)
stopped: int
Stops that animation, or every one of the widget's when the name is left empty. Values stay where they got to, and nothing is reported — cancelled is not finished. Lua/Python horizon.widget.stopAnimationClip · HorizonCode Stop Animation · C++ HE::api::widget::stopAnimationClip · action · widget is Self in HorizonCode
Whether that animation is running right now. What a toggle asks before deciding which way to go. Lua/Python horizon.widget.isPlayingAnimation · HorizonCode Is Animation Playing · C++ HE::api::widget::isPlayingAnimation · query · widget is Self in HorizonCode
childRef(widget: ref, element: string)
child: ref
A reference to the component sitting in one of this widget's slots, by the slot's name. A component is a class like any other, so this is how you call its functions, bind its events and read its public variables — and how a page tells one of three identical cards apart. Leave Widget unwired for this widget's own slots. 0 for an element that is not a component slot. Lua/Python horizon.widget.childRef · HorizonCode Get Child Widget · C++ HE::api::widget::childRef · query · widget is Self in HorizonCode
stopAllAnimations(widget: ref)
stopped: int
Stops everything moving in the widget: the authored animations and the single-property ones both. What a screen being torn down or swapped reaches for, because it does not have to name what it started. Values stay where they got to; Restore Original State is the one that puts them back. Lua/Python horizon.widget.stopAllAnimations · HorizonCode Stop All Animations · C++ HE::api::widget::stopAllAnimations · action · widget is Self in HorizonCode
restoreOriginalState(widget: ref)
restored: int
Stops everything and puts every property an animation ever touched back the way it was BEFORE anything animated it — not the way it was a moment ago. Stopping is part of it: a clip left running would write over the restored values on the next frame. Outputs how many properties were put back, which is 0 when nothing had been animated. Lua/Python horizon.widget.restoreOriginalState · HorizonCode Restore Original State · C++ HE::api::widget::restoreOriginalState · action · widget is Self in HorizonCode
showModal(widget: ref)
—
Puts this widget up as a DIALOG: the screen behind it dims, nothing under it can be clicked, scrolled or reached with the keyboard, and the focus starts inside it. It is raised above every other widget, so it cannot end up blocking input while drawing behind something. It stays until Close Top Layer, Escape or the Back button — that is what makes it modal. A script gets no On Dismissed: ask isVisible to notice that the dialog was closed. Lua/Python horizon.widget.showModal · HorizonCode Show Modal Widget · C++ HE::api::widget::showModal · action
openPopup(widget: ref, x: float, y: float)
—
Puts the widget up at a point on screen — in render-target pixels — and lets a click anywhere else dismiss it. Its root elements are moved there and pushed back inside the screen, whatever anchors they were drawn with, so a menu near the bottom edge opens upwards instead of off the screen. No dimming: a popup is left by looking elsewhere, a dialog has to be answered. Lua/Python horizon.widget.openPopup · HorizonCode Open Popup · C++ HE::api::widget::openPopup · action
openPopupAtPointer(widget: ref)
—
The same, at the mouse. This is the context menu: answer On Right Clicked with it and the menu appears where the click was. The engine remembers where the pointer last was, so the graph does not have to. Lua/Python horizon.widget.openPopupAtPointer · HorizonCode Open Popup At Pointer · C++ HE::api::widget::openPopupAtPointer · action
closeTopLayer()
closed: bool
Closes the topmost dialog, popup or open dropdown and says whether there was one. Exactly what Escape and the gamepad's Back button do — so a "Cancel" button is this node and nothing else. The widget that closes gets On Dismissed, and the keyboard focus goes back where it was before it opened. Hide Widget on a dialog does the same thing — hiding one IS closing it, so it lets go of the input, the focus and the dimming and fires On Dismissed too. This node is the one to reach for when you do not have the dialog's own id, which is the usual case for a Cancel button. false when nothing was open. Lua/Python horizon.widget.closeTopLayer · HorizonCode Close Top Layer · C++ HE::api::widget::closeTopLayer · action
Interface and app · 1 function
Cursor
◆
Lua and Python have no horizon.cursor.*: use the flat horizon.showCursor() / horizon.hideCursor().
One row, and no horizon.cursor.* group: from a script use
the flat horizon.showCursor()
and horizon.hideCursor(). Hiding the cursor also captures
the mouse for camera control, showing it releases the mouse. In the
editor this applies to play mode only.
Function
Returns
Description
setVisible(show: bool)
—
Shows or hides the mouse cursor. A first-person game hides it while playing and shows it again for menus; see also the Input Mode calls, which decide who receives the clicks. Lua/Python only as flat horizon.showCursor / horizon.hideCursor · HorizonCode Set Cursor Visible · C++ HE::api::cursor::setVisible · action
The colours and text size of every widget. A Theme asset carries a
light and a dark set of colour roles; set switches the
asset, setMode picks between its two sets. Widgets that name
their own theme in the designer keep it.
Mode names are exact:"Light",
"Dark" or "System". Anything else, including
"dark", is ignored with a log line.
Preference and mode are two answers.getPreference returns what was asked for, including
"System"; getMode returns what is on screen,
only ever Light or Dark. Store the preference, show the mode (see the
example under setMode). Where
the desktop reports nothing, System resolves to Dark.
Font scale is clamped to 0.5 … 3:
setFontScale(5) reads back 3, 0.1 reads
back 0.5.
set answers false for a path that does not
load and for a Theme asset whose JSON cannot be read; the current
theme stays.
Example: theme.setMode
theme_toggle.lua
local M = {}
-- Store the PREFERENCE, not the mode: System is a rule that follows the desktop.
local NEXT = { System = "Light", Light = "Dark", Dark = "System" }
function M.onStart(self)
horizon.theme.setMode(horizon.prefs.getString("theme", "System"))
horizon.theme.setFontScale(horizon.prefs.getNumber("textScale", 1))
end
function M.onInputPressed(self, name)
if name == "CycleTheme" then
local pref = NEXT[horizon.theme.getPreference()]
horizon.theme.setMode(pref)
horizon.prefs.setString("theme", pref)
horizon.log("theme " .. pref .. ", showing " .. horizon.theme.getMode())
elseif name == "BiggerText" or name == "SmallerText" then
local step = name == "BiggerText" and 0.25 or -0.25
horizon.theme.setFontScale(horizon.theme.getFontScale() + step) -- kept in 0.5..3
horizon.prefs.setNumber("textScale", horizon.theme.getFontScale())
end
end
return M
theme_toggle.py
import horizon
# Store the PREFERENCE, not the mode: System is a rule that follows the desktop.
NEXT = {"System": "Light", "Light": "Dark", "Dark": "System"}
class ThemeToggle(horizon.Behavior):
def on_start(self):
horizon.theme.setMode(horizon.prefs.getString("theme", "System"))
horizon.theme.setFontScale(horizon.prefs.getNumber("textScale", 1))
def on_input_pressed(self, name):
if name == "CycleTheme":
pref = NEXT[horizon.theme.getPreference()]
horizon.theme.setMode(pref)
horizon.prefs.setString("theme", pref)
horizon.log("theme %s, showing %s" % (pref, horizon.theme.getMode()))
elif name in ("BiggerText", "SmallerText"):
step = 0.25 if name == "BiggerText" else -0.25
horizon.theme.setFontScale(horizon.theme.getFontScale() + step) # kept in 0.5..3
horizon.prefs.setNumber("textScale", horizon.theme.getFontScale())
Function
Returns
Description
set(themeAsset: string)
ok: bool
Switches the whole application to another Theme asset. Every element bound to a colour role takes the new colour at once — that is what a role is for. Ok is false when there is no such asset or it cannot be read. Lua/Python horizon.theme.set · HorizonCode Set Theme · C++ HE::api::theme::set · action
setMode(mode: string)
—
"Light", "Dark" or "System". Both colours live in the SAME theme, so this is a switch and not a second set of widgets: every bound colour re-resolves immediately. System is the default and follows the desktop, including while the application runs. Anything else is ignored. The names are exact: "dark" is ignored. Lua/Python horizon.theme.setMode · HorizonCode Set Theme Mode · C++ HE::api::theme::setMode · action
getMode()
mode: string
Which of the two colours is on screen right now — Light or Dark, never System. This is what to ask when the answer decides something else, like which icon to show. Dark where the desktop reports no preference. Lua/Python horizon.theme.getMode · HorizonCode Get Theme Mode · C++ HE::api::theme::mode · query
getPreference()
preference: string
What was ASKED for: Light, Dark or System. This is what a Preferences screen shows and writes back, because System is a rule and not a colour — storing what it resolved to today would stop it following tomorrow. Lua/Python horizon.theme.getPreference · HorizonCode Get Theme Preference · C++ HE::api::theme::preference · query
setFontScale(scale: float)
—
How big the text is for the person reading it: 1 is what the designer drew, 1.5 is half again. Only the TEXT grows — corners, padding and tab strips stay where they were authored, because a whole interface zoomed is a different setting (the display scale, which the system supplies). Clamped to 0.5 … 3. Clamped to 0.5 … 3. Lua/Python horizon.theme.setFontScale · HorizonCode Set Text Size · C++ HE::api::theme::setFontScale · action
getFontScale()
scale: float
The text size actually in use, after clamping — what a Preferences screen shows next to its slider. Lua/Python horizon.theme.getFontScale · HorizonCode Get Text Size · C++ HE::api::theme::fontScale · query
Second windows for a desktop application: a tool window beside the main
one, a document per window. Window 0 is the main window;
window.setTitle(0, …) and window.setSize(0, …)
are app.setTitle and app.setSize, and
close(0) is refused with a pointer to app.quit.
Only an exported application has windows to give: in the editor
open answers 0, and so it does on a renderer without a
path into a second window (Software and Metal have one). A width or
height of 0 or less opens at 640 × 480.
▲
Known issue (build 7d49d44f): from Lua and Python only
window.show works. The script bridge hands a
script no window service, in the exported app as well:
open answers 0 and close,
setTitle and setSize only log “no window
service bound by the host”. show moves a widget between
windows and works, but with no second window to open, 0 is the only
useful target. Use these rows from a HorizonCode graph.
Function
Returns
Description
open(title: string, width: int, height: int)
window: int
Opens a SECOND window with its own widget tree, and answers its id. A tool window beside the main one, a document per window, a panel off to the side. Answers 0 when it was refused: in the editor, and on a renderer with no path into a second window (Software and Metal have one). From Lua/Python always 0, see the group notes. A width or height of 0 or less opens at 640 × 480. Lua/Python horizon.window.open · HorizonCode Open Window · C++ HE::api::window::open · action
close(window: int)
—
Closes a window this application opened. Everything that hung in it is destroyed with it and On Window Closed fires — the same event its own close button gives you, because it is the same thing happening. Ending the whole application is Quit Game, not this. From Lua/Python this only logs, see the group notes. Lua/Python horizon.window.close · HorizonCode Close Window · C++ HE::api::window::close · action
setTitle(window: int, title: string)
—
Sets the title of one window by its id. Window 0 is the main one, and for it this does exactly what Set Window Title does. From Lua/Python this only logs, see the group notes. Window 0 is app.setTitle. Lua/Python horizon.window.setTitle · HorizonCode Set Title Of Window · C++ HE::api::window::setTitle · action
setSize(window: int, width: int, height: int)
—
Resizes one window by its id, in logical points. Window 0 is the main one. Both sides must be positive. From Lua/Python this only logs, see the group notes. Window 0 is app.setSize. Lua/Python horizon.window.setSize · HorizonCode Set Size Of Window · C++ HE::api::window::setSize · action
show(window: int, widget: ref)
—
Puts a widget in a window and shows it there. A real move: anything the widget was holding in the window it came from — a dialog's grab, the keyboard focus — stays behind, because a dialog that keeps its old window sealed is a window nobody can click again. Window 0 brings it back to the main window. Works from scripts, and shows the widget as well. Lua/Python horizon.window.show · HorizonCode Show Widget In Window · C++ HE::api::window::show · action
The operating system's own message boxes and file pickers. Every one
of them blocks: the frame stops until the person
answers, so a game keeps them for what really has to be read or chosen
first. No permission is needed.
Choosing is the permission for the
fs rows: a path returned by
openFile or saveFile, and everything inside
a folder from pickFolder, may be read and written with an
absolute path from then on, even when the project does not allow
files outside the project. It does not open
db.open or print.toPdf, see
Database.
An empty path means cancelled, and it is also the
answer when the picker fails. A picker left open for five minutes is
given up on and answers empty as well.
message kind 1 is a warning, 2 an error, anything else
information. confirm returns true for the
first button; empty labels become "OK" and
"Cancel", Return takes the first button and Escape the
second.
Hidden runs (HE_HIDDEN_WINDOW, headless
exports and tests) show no box: message writes its text
to the log and confirm answers false. The
three pickers do not check for a hidden run, so keep them out of one.
Function
Returns
Description
message(title: string, text: string, kind: int)
—
Shows a native message box and waits until the user dismisses it. Kind is 0 for information, 1 for a warning, 2 for an error. Blocking on purpose: it is for what must be read before anything else happens. Kind 1 is a warning, 2 an error, anything else information. A hidden run writes the text to the log instead. Lua/Python horizon.dialog.message · HorizonCode Show Message Dialog · C++ HE::api::dialog::message · action
Asks a yes/no question in a native dialog and returns true for the first button. Both labels are yours, so it can ask "Save" against "Discard" rather than only ever Yes and No. Return takes the first button, Escape the second. A hidden run answers false. Lua/Python horizon.dialog.confirm · HorizonCode Show Confirm Dialog · C++ HE::api::dialog::confirm · action
openFile(filter: string)
path: string
The system's own file picker. Returns the chosen path, or empty if the person cancelled — cancelling is a normal answer, not a failure.
What comes back is ALLOWED to the file calls from then on, even in a project that permits nothing outside its own folder: somebody choosing a file is the permission. This is how an application opens a document.
Filter is a description and its extensions in one string, "Text files:txt;md", or empty for anything. The pick opens the path to the fs rows, not to db.open or print.toPdf. Lua/Python horizon.dialog.openFile · HorizonCode Open File Dialog · C++ HE::api::dialog::openFile · action
saveFile(filter: string)
path: string
The system's own "save as" picker. Same as Open File Dialog in every other way, including that the path it returns becomes writable to the file calls. The file usually does not exist yet — that is the point. Lua/Python horizon.dialog.saveFile · HorizonCode Save File Dialog · C++ HE::api::dialog::saveFile · action
pickFolder()
path: string
Asks for a directory. Everything INSIDE it becomes reachable to the file calls, not just the folder itself, which is what makes "choose a workspace folder" work with no permission set anywhere. The pick opens the folder to the fs rows, not to db.open or print.toPdf. Lua/Python horizon.dialog.pickFolder · HorizonCode Pick Folder Dialog · C++ HE::api::dialog::pickFolder · action
The system clipboard, shared with every other application. Text only,
and no permission is needed. Reading an empty clipboard gives
"", as does a clipboard holding something that is not
text; hasText is the cheap check for greying out a Paste
button. A process without a window (a
headless tool) has no clipboard: setText fails with a log
line and getText answers "".
Function
Returns
Description
getText()
text: string
The text currently on the system clipboard, or an empty string when there is none. The same clipboard Ctrl+C and Ctrl+V use in a text field. Lua/Python horizon.clipboard.getText · HorizonCode Get Clipboard Text · C++ HE::api::clipboard::getText · query
setText(text: string)
—
Puts text on the system clipboard, replacing whatever was there. Every other application on the machine can then paste it. Lua/Python horizon.clipboard.setText · HorizonCode Set Clipboard Text · C++ HE::api::clipboard::setText · action
hasText()
has: bool
True when the system clipboard currently holds text. Use it to grey out a Paste button instead of pasting nothing. Lua/Python horizon.clipboard.hasText · HorizonCode Has Clipboard Text · C++ HE::api::clipboard::hasText · query
The application around the game: its window, menu bar, tray icon,
notifications and login item. These rows exist for desktop
applications built with the engine and need a host that provides each
piece. The exported application provides all of them. Play mode in the
editor provides only quit (it stops play mode),
size and requestRedraw; everything else logs
“no … bound by the host” and does nothing.
▲
Known issue (build 7d49d44f): from Lua and Python only
app.quit reaches the host. The script bridge
passes a script the quit handler and nothing else, in the exported
application as well. Every other row either logs “no … bound by the
host” and does nothing, or answers its empty value:
size 0, 0, isMaximized,
notifyAvailable, autostart,
menuItemEnabled and menuItemCheckedfalse, notifyfalse.
requestRedraw is silent. Build window chrome, menus and the
tray from a HorizonCode graph; its On Menu Item and On
Tray Item events have no script counterpart either.
Ids and labels. A menu, menu entry or tray entry
without an id is refused with a log line; an empty label shows the id.
Adding a menu id twice is one menu. A shortcut that does not parse is
dropped with a log line and the entry arrives without it;
"Ctrl" means Command on a Mac.
Refused values.setSize with a side of 0
or less is ignored; notify with neither title nor text
answers false. On macOS notify needs the
packaged app.
setAutostart needs the project permission Run other
programs; reading autostart needs none.
Function
Returns
Description
quit()
—
Ends the game. In the editor it stops play mode instead, so a quit button can be tested without closing the editor. The one app row that works from Lua and Python. Lua/Python horizon.app.quit · HorizonCode Quit Game · C++ HE::api::app::quit · action
setTitle(title: string)
—
Sets the text in the window's title bar. Only an application owns its window outright — in the editor this is ignored rather than renaming the editor itself. From Lua/Python this does nothing, see the group notes. Lua/Python horizon.app.setTitle · HorizonCode Set Window Title · C++ HE::api::app::setTitle · action
setSize(width: int, height: int)
—
Resizes the window, in logical points. Both sides must be positive; a zero or negative size is refused rather than passed on to the platform. From Lua/Python this does nothing, see the group notes. Lua/Python horizon.app.setSize · HorizonCode Set Window Size · C++ HE::api::app::setSize · action
size()
size: vec2
The window's current size in logical points, as X and Y. On a high-DPI display this is the points size, not the pixel count behind it. From Lua/Python always 0, 0, see the group notes. Lua/Python horizon.app.size · HorizonCode Get Window Size · C++ HE::api::app::size · query
requestRedraw()
—
Draws one more frame. An application sleeps until something happens, so anything that changes the screen without an input event behind it — a timer, a finished load — has to say so here. From Lua/Python this does nothing, see the group notes. Lua/Python horizon.app.requestRedraw · HorizonCode Request Redraw · C++ HE::api::app::requestRedraw · action
minimize()
—
Puts the window away into the dock or the taskbar. The other half of a title bar a window draws itself: without a system frame there is no minimise button but the one you drew. From Lua/Python this does nothing, see the group notes. Lua/Python horizon.app.minimize · HorizonCode Minimize Window · C++ HE::api::app::minimize · action
maximize(maximized: bool)
—
Fills the screen's work area with the window, or puts it back where it was when this is off. One call for both, because one button on a title bar does both: wire it to Is Window Maximized inverted. From Lua/Python this does nothing, see the group notes. Lua/Python horizon.app.maximize · HorizonCode Maximize Window · C++ HE::api::app::maximize · action
isMaximized()
maximized: bool
Whether the window is maximised right now. Asked of the system rather than remembered, so it stays right after somebody double-clicked the title bar or used the platform's own shortcut. From Lua/Python always false, see the group notes. Lua/Python horizon.app.isMaximized · HorizonCode Is Window Maximized · C++ HE::api::app::isMaximized · query
showTray(tooltip: string)
—
Puts the application's icon in the system tray (the menu bar on macOS) with a tooltip. Called again while it is up, it only changes the tooltip. The icon is the one the export generated, so the tray and the window agree about what this application looks like. From Lua/Python this does nothing, see the group notes. Lua/Python horizon.app.showTray · HorizonCode Show Tray Icon · C++ HE::api::app::showTray · action
hideTray()
—
Takes the tray icon away again. It also goes when the application exits — an icon left behind by a program that is gone is the worst thing a tray can do — so this is for hiding it while still running. From Lua/Python this does nothing, see the group notes. Lua/Python horizon.app.hideTray · HorizonCode Hide Tray Icon · C++ HE::api::app::hideTray · action
addTrayItem(id: string, label: string)
—
Adds an entry to the tray menu, in order. The ID is what On Tray Item receives when somebody chooses it, and the label is what they read: translating the menu then cannot quietly rewire what its entries do. From Lua/Python this does nothing, see the group notes. Lua/Python horizon.app.addTrayItem · HorizonCode Add Tray Item · C++ HE::api::app::addTrayItem · action
clearTrayMenu()
—
Removes every entry from the tray menu, leaving the icon in place. What a menu offers usually depends on what the application is doing, and rebuilding it is how that stays true. From Lua/Python this does nothing, see the group notes. Lua/Python horizon.app.clearTrayMenu · HorizonCode Clear Tray Menu · C++ HE::api::app::clearTrayMenu · action
addMenu(id: string, label: string)
—
Adds a menu to the application's menu bar, in order. The id is how Add Menu Item names it afterwards; the label is what is drawn in the strip. Adding the same id twice is one menu, not two. On macOS the same menus go into the SYSTEM bar next to the Apple symbol instead, and the strip in the window is not drawn at all — a page that leaves room for the bar gets that room back there. From Lua/Python this does nothing, see the group notes. Lua/Python horizon.app.addMenu · HorizonCode Add Menu · C++ HE::api::app::addMenu · action
Adds an entry to the menu with that id. Choosing it fires On Menu Item with the ENTRY's id — the label is only what somebody reads, so translating the menu leaves what it does alone. Shortcut is the chord that chooses it without opening the menu, written the way people write one: "Ctrl+Shift+S", "F5", "Alt+Left". It fires the same On Menu Item, because it is the same entry answered a faster way. Ctrl means Command on a Mac, so a chord is written once and is right on every platform; leave it empty for an entry that has none. From Lua/Python this does nothing, see the group notes. Lua/Python horizon.app.addMenuItem · HorizonCode Add Menu Item · C++ HE::api::app::addMenuItem · action
addMenuSeparator(menu: string)
—
Adds a dividing line to a menu. It carries no id and cannot be chosen: it is there to group the entries above and below it. From Lua/Python this does nothing, see the group notes. Lua/Python horizon.app.addMenuSeparator · HorizonCode Add Menu Separator · C++ HE::api::app::addMenuSeparator · action
clearMenuBar()
—
Removes the whole menu bar. A menu usually changes as a set, so the way to change one is to clear it and build the new one. From Lua/Python this does nothing, see the group notes. Lua/Python horizon.app.clearMenuBar · HorizonCode Clear Menu Bar · C++ HE::api::app::clearMenuBar · action
setMenuItemEnabled(id: string, enabled: bool)
—
Greys a menu entry out, or brings it back. Addressed by the ENTRY's id — the same id On Menu Item carries — so an id used in two menus is one command offered twice and both rows follow. A disabled row cannot be chosen and its shortcut does nothing either, which is the half that matters: a greyed-out Save whose Ctrl+S still saves has said one thing and done another. From Lua/Python this does nothing, see the group notes. Lua/Python horizon.app.setMenuItemEnabled · HorizonCode Set Menu Item Enabled · C++ HE::api::app::setMenuItemEnabled · action
setMenuItemChecked(id: string, checked: bool)
—
Puts a mark beside a menu entry, or takes it away — for a row that names a state ("Show Toolbar") rather than an action. Choosing the row still fires On Menu Item and nothing else: whether the mark then moves is the application's decision, because a menu that ticked itself would be wrong the moment the command it named failed. From Lua/Python this does nothing, see the group notes. Lua/Python horizon.app.setMenuItemChecked · HorizonCode Set Menu Item Checked · C++ HE::api::app::setMenuItemChecked · action
menuItemEnabled(id: string)
enabled: bool
Whether that menu entry can be chosen right now. False for an id no row carries. From Lua/Python always false, see the group notes. Lua/Python horizon.app.menuItemEnabled · HorizonCode Is Menu Item Enabled · C++ HE::api::app::menuItemEnabled · query
menuItemChecked(id: string)
checked: bool
Whether that menu entry carries its mark right now — what a row bound to a setting reads to flip it instead of remembering it. From Lua/Python always false, see the group notes. Lua/Python horizon.app.menuItemChecked · HorizonCode Is Menu Item Checked · C++ HE::api::app::menuItemChecked · query
notify(title: string, text: string)
shown: bool
Puts a banner in the system's notification centre — what an application says when it has finished something nobody is watching any more. It is not a dialog: nothing waits for it and nothing comes back. True means the system took it, not that somebody read it; whether it appears is the system's decision (Do Not Disturb, this app's own switch). On macOS it needs the packaged app — a game runtime started straight from a build folder has no identity to post as. From Lua/Python always false, see the group notes. Lua/Python horizon.app.notify · HorizonCode Notify · C++ HE::api::app::notify · action
notifyAvailable()
available: bool
Can this program show notifications at all? False in the editor preview, and on a Linux without notify-send. Ask once instead of finding out per notification. From Lua/Python always false, see the group notes. Lua/Python horizon.app.notifyAvailable · HorizonCode Notifications Available · C++ HE::api::app::notifyAvailable · query
setAutostart(enabled: bool)
—
Arranges for this application to start when the user logs in, or takes that back. It needs the project's "Run other programs" permission: it asks the SYSTEM to run a program at every login, which is more than running one now, not less. From Lua/Python this does nothing, see the group notes. Lua/Python horizon.app.setAutostart · HorizonCode Set Start at Login · C++ HE::api::app::setAutostart · action · needs permission Run other programs
autostart()
enabled: bool
Whether this application is currently set to start at login. Reads what the system was told, not what the application believes — so a checkbox bound to it stays right even after somebody changed it elsewhere. From Lua/Python always false, see the group notes. Lua/Python horizon.app.autostart · HorizonCode Starts at Login · C++ HE::api::app::autostart · query
Other programs on the machine. run and
openUrl need the project permission Run other
programs; which needs none, so a script can find out
what is installed before anyone decides to allow it.
run blocks the frame until the program
ends or the timeout kills it; a timeout of 0 means 30 seconds. It
returns four values, ok, exitCode, out, err:
ok is true for exit code 0,
out and err are the complete output. A
program that could not be started gives exit code −1. Nothing goes
through a shell.
openUrl hands the address to the desktop (browser, mail
client, file manager) and answers false for an empty one
or when the desktop refuses it. which answers the full
path, or "".
▲
Known issue (build 7d49d44f): from Lua and Python
run passes no arguments. The argument list is a
list pin. A Lua table or Python list is
rejected (bad argument #2 … string expected,
TypeError); a string is accepted and dropped. So
horizon.process.run("git", "status", 10) runs
git with no arguments at all. From a script only programs
that need no arguments work; everything else belongs in a HorizonCode
graph for now.
Example: process.openUrl
open_manual.lua
local M = {}
local MANUAL = "https://example.com/manual/"
function M.onInputPressed(self, name)
if name ~= "Help" then return end
-- openUrl needs "Run other programs". Without it, hand the address over.
if horizon.process.openUrl(MANUAL) then return end
horizon.clipboard.setText(MANUAL)
horizon.dialog.message("Manual", "The manual's address is on your clipboard:\n" .. MANUAL, 0)
end
return M
open_manual.py
import horizon
MANUAL = "https://example.com/manual/"
class OpenManual(horizon.Behavior):
def on_input_pressed(self, name):
if name != "Help":
return
# openUrl needs "Run other programs". Without it, hand the address over.
if horizon.process.openUrl(MANUAL):
return
horizon.clipboard.setText(MANUAL)
horizon.dialog.message("Manual", "The manual's address is on your clipboard:\n" + MANUAL, 0)
Function
Returns
Description
run(exe, argsIgnored, timeoutSeconds)
ok, exitCode, out, err
Runs another program and WAITS for it. Arguments go in one at a time — this is not a command line, so a path with a space in it needs no quoting and cannot inject anything.
Four answers, because a caller who only wants to know whether it worked reads Ok, and one who has to explain a failure to a person needs the exit code and Err. A non-zero exit code is an ANSWER, not a breakage.
It blocks the frame, so give it a timeout you are willing to wait; left at zero it is thirty seconds. Needs the project's "Run other programs" permission. Known issue: from Lua/Python the arguments are not passed, see the group notes. A program that cannot be started gives exit code −1. Lua/Python horizon.process.run · HorizonCode Run Program · C++ HE::api::process::run · action · list: HorizonCode/C++ only · needs permission Run other programs
openUrl(url: string)
ok: bool
Hands a web address (or a file) to whatever this desktop opens it with — a browser, the file manager, the mail client. The usual way to show a manual or a release page. Needs the project's "Run other programs" permission. Lua/Python horizon.process.openUrl · HorizonCode Open URL · C++ HE::api::process::openUrl · action · needs permission Run other programs
which(exe: string)
path: string
Where the system would find this program, or empty if it would not find it at all. Deliberately needs NO permission: asking whether something is installed runs nothing, and it is how a script tells somebody what it would need before they decide to allow it. Lua/Python horizon.process.which · HorizonCode Find Program · C++ HE::api::process::which · query
Paper and PDF. toPdf writes a plain page of text itself;
file hands an existing file to the system's print queue.
The PDF layout is fixed: A4, Courier 10 pt, 80
characters by 60 lines per page. Lines break at your newlines and at
the last space that fits, pages when they are full.
title becomes the document title in its properties.
Characters of Latin-1 come through (ä, é);
every other one, the euro sign included, prints as ?.
Missing folders on the way to the path are created.
toPdf always needs the permission Files
outside the project, even for a relative path, which
lands in the project's Saved folder like the fs
rows. A path the user picked in a file dialog does not stand in for
it (build 7d49d44f).
file and available use the system's
lp, on macOS and Linux. On Windows both answer
false. file needs Run other programs
and answers true once the queue took the file, which says
nothing about paper; a missing file is false.
Function
Returns
Description
toPdf(path: string, text: string, title: string)
ok: bool
Writes text as a PDF: a relative path lands in the project's Saved folder, like the file nodes. It is set in Courier and laid out as a page of text: lines break at your newlines and at the page width, pages break when they are full; characters outside Latin-1 print as '?'. Needs the project's "Files outside the project" permission for EVERY path, a relative one included, and a path picked in a file dialog does not stand in for it. Needed even for a relative path. Only Latin-1 characters survive, others print as ?. Lua/Python horizon.print.toPdf · HorizonCode Write PDF · C++ HE::api::print::toPdf · action · needs permission Files outside the project
file(path: string)
ok: bool
Hands a file to the system's printing. Ok means it was handed over, not that it came out of a printer — what the queue does next is between the user and their printer. Needs the "Run other programs" permission, because that is what this is. Not available on Windows yet. Uses lp; false on Windows and for a missing file. Lua/Python horizon.print.file · HorizonCode Print File · C++ HE::api::print::file · action · needs permission Run other programs
available()
available: bool
Is there anything here to print with? Check it before offering a Print button. It answers false on Windows, where printing still needs its own piece of work. Lua/Python horizon.print.available · HorizonCode Printing Available · C++ HE::api::print::available · query
SQLite databases, as files. open returns a handle for the
other rows, 0 when it failed; at most 16 are open at once, and all close
when the application ends. The path follows the fs rules: relative
lands in the project's Saved folder, .. is refused, an
absolute path needs the permission. There is no in-memory database:
":memory:" is an ordinary file name.
▲
open always needs the permission Files outside
the project, even for a relative path inside the
project. A file or folder the user picked in a dialog does not stand
in for it, although it does for the fs rows and the
editor's permission help says it would (build 7d49d44f). Without the
permission open answers 0.
Values go behind ?, as a JSON array in
params: '["Ada", 36.5]'. Strings, numbers,
true/false (stored as 1/0) and
null; a nested array or object is stored as its JSON
text. Placeholders the array does not reach are NULL,
and a params that is not an array is ignored. Python
builds it with json.dumps. Lua has no JSON writer, and
horizon.json.set* can overwrite array slots but not add
them: start from a template such as '["", 0]' (see the
example under query).
exec runs one statement. Of
"INSERT …; INSERT …" only the first runs, and the answer
is still true. exec on a SELECT runs it and
throws the rows away.
query answers JSON text: an array with
one object per row, keyed by column name. Integers come back as
integers, REAL as floats (12.0), NULL and BLOB as
null. An empty result and a failed query both give
"[]"; lastError tells them apart. More than
10 000 rows are cut off, with a note in lastError.
lastError is SQLite's own message and empty after a call
that worked. On a handle that is not open, exec is
false, query"[]" and
lastError empty. lastInsertId reads back
ids above 2 147 483 647 as 2 147 483 647.
SQL cannot reach other files: ATTACH,
VACUUM INTO and the pragmas that move SQLite's own files
fail with “not authorized”.
Example: db.query
highscores.lua
local M = {}
local PLAYER = "Player 1"
function M.onStart(self)
-- Lands in the project's Saved folder, but needs the "Files outside the
-- project" permission all the same (see the group notes).
self.db = horizon.db.open("scores.db")
if self.db ~= 0 then
horizon.db.exec(self.db, "CREATE TABLE IF NOT EXISTS scores(name TEXT, points INTEGER)", "")
end
end
function M.onInputPressed(self, name)
if name ~= "LevelDone" or self.db == 0 then return end
local points = math.max(0, 1000 - math.floor(horizon.time.elapsed() * 10))
-- Values travel as a JSON array behind the ?s, never pasted into the SQL.
-- json.set* can overwrite array slots but not add them, so start from two.
local params = horizon.json.setNumber(horizon.json.setString('["", 0]', "[0]", PLAYER), "[1]", points)
if not horizon.db.exec(self.db, "INSERT INTO scores(name, points) VALUES(?, ?)", params) then
horizon.log("scores: " .. horizon.db.lastError(self.db))
return
end
local rows = horizon.db.query(self.db,
"SELECT name, points FROM scores ORDER BY points DESC LIMIT 5", "")
for i = 0, horizon.json.count(rows, "") - 1 do
horizon.log(string.format("%d. %s %d", i + 1,
horizon.json.getString(rows, "[" .. i .. "].name", "?"),
horizon.json.getNumber(rows, "[" .. i .. "].points", 0)))
end
end
return M
highscores.py
import horizon
import json
PLAYER = "Player 1"
class Highscores(horizon.Behavior):
def on_start(self):
# Lands in the project's Saved folder, but needs the "Files outside the
# project" permission all the same (see the group notes).
self.db = horizon.db.open("scores.db")
if self.db:
horizon.db.exec(self.db, "CREATE TABLE IF NOT EXISTS scores(name TEXT, points INTEGER)", "")
def on_input_pressed(self, name):
if name != "LevelDone" or not self.db:
return
points = max(0, 1000 - int(horizon.time.elapsed() * 10))
# Values travel as a JSON array behind the ?s, never pasted into the SQL.
if not horizon.db.exec(self.db, "INSERT INTO scores(name, points) VALUES(?, ?)",
json.dumps([PLAYER, points])):
horizon.log("scores: " + horizon.db.lastError(self.db))
return
rows = json.loads(horizon.db.query(
self.db, "SELECT name, points FROM scores ORDER BY points DESC LIMIT 5", ""))
for i, row in enumerate(rows, 1):
horizon.log("%d. %s %d" % (i, row["name"], row["points"]))
Function
Returns
Description
open(path: string)
handle: int
Opens a SQLite database file, creating it if it is not there yet, and gives you a handle for the other Database nodes. A relative path lands in the project's Saved folder, like the file nodes. Needs the project's "Files outside the project" permission for EVERY path, a relative one included, and unlike the file nodes a path picked in a dialog does not stand in for it. 0 means it did not open. Needed even for a relative path; a path picked in a dialog does not replace it (see the group notes). Lua/Python horizon.db.open · HorizonCode Open Database · C++ HE::api::db::open · action · needs permission Files outside the project
close(handle: int)
—
Closes a database. They also all close when the application does. Lua/Python horizon.db.close · HorizonCode Close Database · C++ HE::api::db::close · action
exec(handle: int, sql: string, params: string)
ok: bool
Runs SQL that gives no rows back — CREATE, INSERT, UPDATE, DELETE. Put a ? where each value goes and pass the values as a JSON array in Params (for example ["Ada", 36]); never paste them into the SQL yourself, or a name with a quote in it becomes somebody else's command. Ok is false when it failed, and Database Error says why. Runs only the first statement of several, and still answers true. Lua/Python horizon.db.exec · HorizonCode Run SQL · C++ HE::api::db::exec · action
query(handle: int, sql: string, params: string)
rows: string
Runs a SELECT and gives the rows back as JSON — an array of objects, one per row, which the JSON nodes read. Same ? and Params as Run SQL. An empty result and a failed one both come back as [], so check Database Error to tell them apart. Very large results are cut off (Database Error says so); use LIMIT. Lua/Python horizon.db.query · HorizonCode Query SQL · C++ HE::api::db::query · action
changes(handle: int)
rows: int
How many rows the last Run SQL on this database actually changed. This is how you tell "it worked" from "nothing matched". Lua/Python horizon.db.changes · HorizonCode Rows Changed · C++ HE::api::db::changes · query
lastInsertId(handle: int)
id: int
The row id the last INSERT on this database created. Ids above 2 147 483 647 read back as 2 147 483 647. Lua/Python horizon.db.lastInsertId · HorizonCode Last Insert Id · C++ HE::api::db::lastInsertId · query
lastError(handle: int)
error: string
Why the last call on this database failed, in SQLite's own words, or empty when it did not. Lua/Python horizon.db.lastError · HorizonCode Database Error · C++ HE::api::db::lastError · query
Web requests that do not stop the game. get and
post return a ticket at once; the answer
is read later with that ticket. Both need the project permission
Network access. Ticket 0 means the request never started: no
permission, an empty URL, or a build without an HTTP backend
(available). Plain http:// works as well as
https://.
Scripts poll. HorizonCode gets On Http
Response; a script does not, so it asks done(ticket)
in onUpdate (see the example under
get).
ok is about the connection, not the
answer. A 404 is oktrue with
status 404. When the server was never reached, ok is
false, the status 0 and error says why
(“Could not connect to the server.”).
One request at a time. Requests go out one after
another in the order they were made, each with a 5-second timeout, so
a slow server delays the ones queued behind it. post
with an empty content type sends application/json.
Answers are kept for a while, not forever. The last
32 are kept. An older one, a forgotten one and an unknown ticket all
read the same: donefalse, status 0, empty
body. Read an answer when it arrives, then forget it.
Example: http.get
fetch_news.lua
local M = {}
local URL = "https://example.com/news.json" -- answers {"headline": "..."}
-- This script sits on a UI Text entity; the headline goes there.
function M.onStart(self)
self.ticket = horizon.http.get(URL) -- 0: no "Network access" permission
horizon.ui.setText(self.entityId, self.ticket ~= 0 and "Loading..." or "")
end
function M.onUpdate(self, dt)
local t = self.ticket
-- A script gets no On Http Response: ask until the ticket is answered.
if t == 0 or not horizon.http.done(t) then return end
self.ticket = 0
if not horizon.http.ok(t) then -- never reached the server
horizon.ui.setText(self.entityId, "Offline")
horizon.log("news: " .. horizon.http.error(t))
elseif horizon.http.status(t) ~= 200 then -- reached it, and it said no
horizon.ui.setText(self.entityId, "")
horizon.log("news: HTTP " .. horizon.http.status(t))
else
local body = horizon.http.body(t)
horizon.ui.setText(self.entityId, horizon.json.getString(body, "headline", ""))
end
horizon.http.forget(t)
end
return M
fetch_news.py
import horizon
URL = "https://example.com/news.json" # answers {"headline": "..."}
# This script sits on a UI Text entity; the headline goes there.
class FetchNews(horizon.Behavior):
def on_start(self):
self.ticket = horizon.http.get(URL) # 0: no "Network access" permission
horizon.ui.setText(self.entity_id, "Loading..." if self.ticket else "")
def on_update(self, dt):
t = self.ticket
# A script gets no On Http Response: ask until the ticket is answered.
if t == 0 or not horizon.http.done(t):
return
self.ticket = 0
if not horizon.http.ok(t): # never reached the server
horizon.ui.setText(self.entity_id, "Offline")
horizon.log("news: " + horizon.http.error(t))
elif horizon.http.status(t) != 200: # reached it, and it said no
horizon.ui.setText(self.entity_id, "")
horizon.log("news: HTTP %d" % horizon.http.status(t))
else:
body = horizon.http.body(t)
horizon.ui.setText(self.entity_id, horizon.json.getString(body, "headline", ""))
horizon.http.forget(t)
Function
Returns
Description
get(url: string)
ticket: int
Starts a GET and returns straight away with a ticket number. The answer arrives later as On Http Response, carrying that same ticket. Needs the project's "Network access" permission; 0 means it never started. Lua/Python horizon.http.get · HorizonCode HTTP Get · C++ HE::api::http::get · action · needs permission Network access
Starts a POST with this body. An empty Content Type means application/json. Like Get it returns a ticket and answers later. Lua/Python horizon.http.post · HorizonCode HTTP Post · C++ HE::api::http::post · action · needs permission Network access
done(ticket: int)
done: bool
Has this ticket been answered yet? Usually not needed — On Http Response fires exactly when it turns true — but useful for a screen that shows "loading" while it waits. false again once the answer is forgotten or pushed out by 32 newer ones. Lua/Python horizon.http.done · HorizonCode Response Arrived · C++ HE::api::http::done · query
ok(ticket: int)
ok: bool
Did the request REACH the server and come back? This is about the connection, not about the answer: a 404 is ok=true with status 404. Lua/Python horizon.http.ok · HorizonCode Response OK · C++ HE::api::http::ok · query
status(ticket: int)
status: int
The HTTP status code, 200 for the ordinary success. 0 when the request never got an answer, and then Response Error says why. Lua/Python horizon.http.status · HorizonCode Response Status · C++ HE::api::http::status · query
body(ticket: int)
body: string
What came back, as text. Feed it to the JSON nodes when it is JSON. Lua/Python horizon.http.body · HorizonCode Response Body · C++ HE::api::http::body · query
error(ticket: int)
error: string
Why the request failed to reach anybody (no network, bad host, timed out). Empty when Response OK is true. Lua/Python horizon.http.error · HorizonCode Response Error · C++ HE::api::http::error · query
forget(ticket: int)
—
Drops a response you are finished with. Optional: the engine keeps the last 32 and forgets the oldest by itself. Lua/Python horizon.http.forget · HorizonCode Forget Response · C++ HE::api::http::forget · action
available()
available: bool
Does this build have a network stack at all? False only on a Linux build made without libcurl, where every request would fail — worth saying out loud once instead of failing per request. Lua/Python horizon.http.available · HorizonCode HTTP Available · C++ HE::api::http::available · query
Multiplayer: one machine hosts and is the authority,
the others join as clients and play in the host's
world. The rows fall into five parts: the session (host, join, leave,
status), the players, who owns an entity, replicated variables, and
remote calls. Session events, onRep_<var> and the
remote-call targets are script methods, listed under
Callbacks. The examples below ran word for word
in Lua and Python, host and client in both languages, over a real
connection on one machine.
Hosting.host(port, name) answers
true once the socket is open; the status is 1 at once and
the host's scripts get onSessionStarted. Port 0 takes the
project's Default port (Project Settings, Game, Multiplayer, 47824 in
a new project). If that is 0 as well the system picks one, and no row
reads it back, so a game that tells joiners its port hosts on a fixed
one. Hosting again while a session runs ends it first
(onSessionEnded, then onSessionStarted, a new
session id and join code). It goes through leave(), so
players who had joined are dropped; that part is read from the code,
the re-host was measured with nobody connected.
Joining.joinDirect(address, port, code,
name) answers true when the attempt has started
(status 2); false for port 0 or above 65535, and for
joinLan with no LAN entry at that index. The client's
scripts get onConnected once the whole world has arrived
(status 3), the host's get onPlayerJoined(player).
A wrong join code never fails: the host refuses the
handshake, but the client stays at status 2 with an empty
lastError (measured for 8 seconds). Give up yourself, as
the lobby example does.
LAN browsing needs the system's permission to use the
local network. Without it (macOS asks per application) the host logs
“the system refused to send an announcement” and every list stays
empty; joinDirect keeps working. The LAN rows were not
measured with the permission granted.
Player numbers. The host is player 1, joiners are
2, 3, … in join order, and a number is never reused. That is the
number playerName, ping, kick,
callClient, rpcSender and
onPlayerJoined/onPlayerLeft use. It is
not the connection number the
anticheat rows take.
Ownership. Scene entities belong to nobody:
ownerOf is 0. isLocallyControlled is
true offline and for any entity without Replicates; for a
replicated entity nobody owns it is true on the host and
false on a client.
Row
Offline
Host
Client
status
0
1
2 while connecting, 3 joined, 4 after losing the host
isAuthority / isClient
true / false
true / false
false / true
localPlayer
1
1
its own number (2, 3, …); 0 while connecting
playerCount / playerAt
1 / playerAt(0) = 1
everybody, in join order
1: a client knows only itself
playerName
""
every player's
only its own
ping
0
round trip to each client, ms
always 0
sessionId / joinCode
""
8 / 26 characters
"" / ""
Replicated variables
Declare, then write on the host.declareVar…(entity, name, initial, notify) works on any
entity and also offline; a declaration made before the session starts
is carried into it (the health
example declares in onStart). Only entities with
Replicates on are sent to clients: on any other the variable exists
on this machine only. Declaring a name a second time answers
true and changes nothing (measured offline).
What a client sees. A write on the host reaches every
client a moment later (well within half a second on one
machine); with notify the client's script gets
onRep_<name>(old), never the host's (it made the
change) and never for a variable declared without notify. A
client's own setVar… changes only its copy; the host sends
its value again within a moment, even an unchanged one, and
onRep_ fires once more with old = the
client's value.
Types are not checked on write. A getter of another
type answers zero (false, 0, "", 0,0,0). A
setter of another type answers true and changes the
variable's type, after which the declared getter reads zero; during a
session the host then re-sends the entity's whole variable table every
tick. Ints are 32-bit: 3000000000 is stored as −1294967296.
After the session ends, a client keeps the last values it received.
▲
Known issue (build 7d49d44f): in Lua, old of a
Vector variable is the number 0. The value handed to
onRep_<name>(old) goes through a converter that has
no case for vectors, so Lua always receives 0 (measured
with old values 0,0,0 and 1,2,3). Python receives a list
[x, y, z]. Read the new value with
getVarVec3, and keep the previous one yourself if you need
it. A Vector argument of a remote call from a HorizonCode graph goes
through the same converter.
Remote calls
Who runs what.callServer on a client
sends the call to the host, which runs the method of that name on the
entity: its HorizonCode class first, else its Lua or Python script,
else the C++ module (first one that has it). callClient
and callAllClients go from the host to the clients'
copies of the entity. The arguments after the name arrive with their
types: integers, other numbers, booleans and strings as they are, a
Lua table or Python list as a list, a Python dict as a table with
__keys. Inside the method rpcSender() is the
caller's player number (on a client: 1, the host).
Nothing runs locally in a script. Offline and on the
host, callServer, callClient to the host's
own player and the host's own share of callAllClients
reach only a HorizonCode class. A Lua or Python method is not called
and the row answers false, for callAllClients
even though the clients were sent the call. On the host, call your own
method directly (lever example).
Who may call. A client may call the entities it owns,
plus functions opened with allowAnyClient (or Any Client
on a HorizonCode function header). Anything else the host drops
without a log line, while the client's callServer still
answered true; with anti-cheat on it is a
Hard report and the caller is removed at once (measured).
allowAnyClient lasts for one session and answers
false offline, so call it in
onSessionStarted. It answers true on a
client, where it changes nothing, and for an entity without a network
id, which is refused with a log line.
A call needs a replicated entity: for any other the row answers
false. The host accepts up to 60 calls per second from
one client, averaged over 2 seconds; more are dropped (default, the
Multiplayer page can change it).
Leaving and being removed
leave() ends the session on this machine, sends
onSessionEnded and puts the status back to 0.
A client that is removed or loses the host gets
onDisconnected(reason) and status 4, and no
onSessionEnded. net.kick with anti-cheat off
only closes the connection, so the client sees reason 1 and “Lost the
connection to the host.”; with anti-cheat on the client is told first
and sees reason 2 and “Removed from the session.”. The host's scripts
get onPlayerLeft(player), by which time
playerName of that player is already empty.
▲
Known issue (build 7d49d44f): a second session in the same
scene replicates no scene entity. Hosting walks the scene
for entities with Replicates on and skips those that already carry a
network id; leave() does not clear it. After
host → leave → host, or hosting
twice, the log says “Session walk: 0 replicated scene entities”, and
clients that join the new session get none of those entities: their
variables and remote calls do not arrive.
Example: net.joinDirect
lobby.lua
local M = {}
local PORT = 47824 -- the project's Default port. Fixed on purpose: no row reads back a port the OS picked.
function M.onInputPressed(self, action)
if action == "Host" and horizon.net.status() == 0 then
if horizon.net.host(PORT, "Host") then
horizon.log("Hosting. Join code: " .. horizon.net.joinCode())
else
horizon.log("Could not host: " .. horizon.net.lastError())
end
end
end
-- Called by the join screen with what the player typed in.
function M.join(self, address, code)
horizon.net.leave() -- a new attempt replaces an old one
if horizon.net.joinDirect(address, PORT, code, "Guest") then
self.waited = 0
end
end
function M.onUpdate(self, dt)
if self.waited == nil then return end
-- A wrong join code never fails: the attempt stays at 2 (Connecting). Give up yourself.
self.waited = self.waited + dt
if horizon.net.status() == 2 and self.waited > 10 then
horizon.net.leave()
self.waited = nil
horizon.log("No answer from the host. Check the address and the join code.")
end
end
function M.onConnected(self)
self.waited = nil
horizon.log("Joined as player " .. horizon.net.localPlayer())
end
function M.onDisconnected(self, reason) -- 1 lost, 2 kicked, 3 refused
self.waited = nil
horizon.log("Out of the session (" .. reason .. "): " .. horizon.net.lastError())
end
function M.onPlayerJoined(self, player) -- host only
horizon.log(horizon.net.playerName(player) .. " joined")
end
return M
lobby.py
import horizon
PORT = 47824 # the project's Default port. Fixed on purpose: no row reads back a port the OS picked.
class Lobby(horizon.Behavior):
waited = None
def on_input_pressed(self, action):
if action == "Host" and horizon.net.status() == 0:
if horizon.net.host(PORT, "Host"):
horizon.log("Hosting. Join code: " + horizon.net.joinCode())
else:
horizon.log("Could not host: " + horizon.net.lastError())
def join(self, address, code):
"""Called by the join screen with what the player typed in."""
horizon.net.leave() # a new attempt replaces an old one
if horizon.net.joinDirect(address, PORT, code, "Guest"):
self.waited = 0.0
def on_update(self, dt):
if self.waited is None:
return
# A wrong join code never fails: the attempt stays at 2 (Connecting). Give up yourself.
self.waited += dt
if horizon.net.status() == 2 and self.waited > 10:
horizon.net.leave()
self.waited = None
horizon.log("No answer from the host. Check the address and the join code.")
def on_connected(self):
self.waited = None
horizon.log("Joined as player %d" % horizon.net.localPlayer())
def on_disconnected(self, reason): # 1 lost, 2 kicked, 3 refused
self.waited = None
horizon.log("Out of the session (%d): %s" % (reason, horizon.net.lastError()))
def on_player_joined(self, player): # host only
horizon.log(horizon.net.playerName(player) + " joined")
Example: net.declareVarInt
health.lua
local M = {}
function M.onStart(self)
-- The same script runs on every machine; each declares the variable.
-- The host's value is the one every client ends up with.
horizon.net.declareVarInt(self.entityId, "health", 100, true)
end
-- Only the host decides what a hit does.
function M.onCollisionEnter(self, other)
if not horizon.net.isAuthority() then return end
local hp = horizon.net.getVarInt(self.entityId, "health") - 10
horizon.net.setVarInt(self.entityId, "health", math.max(hp, 0))
self:showHealth() -- onRep_health never runs on the host
end
-- On the clients: a new value arrived. `old` is the one before it.
function M.onRep_health(self, old)
self:showHealth()
end
function M.showHealth(self)
horizon.log("health " .. horizon.net.getVarInt(self.entityId, "health"))
end
return M
health.py
import horizon
class Health(horizon.Behavior):
def on_start(self):
# The same script runs on every machine; each declares the variable.
# The host's value is the one every client ends up with.
horizon.net.declareVarInt(self.entity_id, "health", 100, True)
def on_collision_enter(self, other):
# Only the host decides what a hit does.
if not horizon.net.isAuthority():
return
hp = horizon.net.getVarInt(self.entity_id, "health") - 10
horizon.net.setVarInt(self.entity_id, "health", max(hp, 0))
self.show_health() # on_rep_health never runs on the host
def on_rep_health(self, old):
# On the clients: a new value arrived. `old` is the one before it.
self.show_health()
def show_health(self):
horizon.log("health %d" % horizon.net.getVarInt(self.entity_id, "health"))
Example: net.allowAnyClient
lever.lua
local M = {}
function M.onStart(self)
horizon.net.declareVarBool(self.entityId, "open", false, true)
end
-- Per session: the list of calls any client may make starts empty with every session.
function M.onSessionStarted(self)
-- The lever belongs to nobody. Without this only its owner could pull it,
-- and a refused call counts against the caller as cheating.
horizon.net.allowAnyClient(self.entityId, "pull")
end
function M.onInputPressed(self, action)
if action ~= "Interact" then return end
if horizon.net.isAuthority() then
self:pull() -- host or offline: callServer would not reach a script method here
else
horizon.net.callServer(self.entityId, "pull")
end
end
-- Runs on the host, whoever asked.
function M.pull(self)
local who = horizon.net.rpcSender() -- 0: the host pulled it itself
local open = not horizon.net.getVarBool(self.entityId, "open")
horizon.net.setVarBool(self.entityId, "open", open)
local name = who ~= 0 and horizon.net.playerName(who) or "The host"
horizon.log(name .. (open and " opened" or " closed") .. " the gate")
end
function M.onRep_open(self, old)
horizon.log(horizon.net.getVarBool(self.entityId, "open") and "gate opens" or "gate closes")
end
return M
lever.py
import horizon
class Lever(horizon.Behavior):
def on_start(self):
horizon.net.declareVarBool(self.entity_id, "open", False, True)
def on_session_started(self):
# Per session: the list of calls any client may make starts empty with every session.
# The lever belongs to nobody. Without this only its owner could pull it,
# and a refused call counts against the caller as cheating.
horizon.net.allowAnyClient(self.entity_id, "pull")
def on_input_pressed(self, action):
if action != "Interact":
return
if horizon.net.isAuthority():
self.pull() # host or offline: callServer would not reach a script method here
else:
horizon.net.callServer(self.entity_id, "pull")
def pull(self):
"""Runs on the host, whoever asked."""
who = horizon.net.rpcSender() # 0: the host pulled it itself
is_open = not horizon.net.getVarBool(self.entity_id, "open")
horizon.net.setVarBool(self.entity_id, "open", is_open)
name = horizon.net.playerName(who) if who else "The host"
horizon.log(name + (" opened" if is_open else " closed") + " the gate")
def on_rep_open(self, old):
horizon.log("gate opens" if horizon.net.getVarBool(self.entity_id, "open") else "gate closes")
Function
Returns
Description
host(port: int, displayName: string)
ok: bool
Opens a multiplayer session on this machine and makes it joinable. Port 0 takes the project's Default port (Project Settings, Game, Multiplayer), which is where the seats, the tick rate and the rest come from too. If that is 0 as well the system picks one, and no node reads it back, so a game that tells joiners its port hosts on a fixed one; hand them the Join Code too. Hosting again ends the running session first. Everyone who joins plays in YOUR world: the host decides what really happened. Lua/Python horizon.net.host · HorizonCode Host Session · C++ HE::api::net::host · action
Joins a session by address and port, with the host's Join Code. The code is not a password you can guess past: without the right one the connection never completes at all. Answers true when the attempt has started. With a wrong join code the status stays 2 and lastError empty (measured for 8 seconds): time the attempt out yourself, as the example does. Lua/Python horizon.net.joinDirect · HorizonCode Join By Address · C++ HE::api::net::joinDirect · action
Joins the session at this index in the LAN list (Refresh LAN Sessions first). Same Join Code as a direct join. false when there is no entry at that index, which without the system's Local Network permission is always. Lua/Python horizon.net.joinLan · HorizonCode Join LAN Session · C++ HE::api::net::joinLan · action
leave()
—
Leaves the session, or closes it if you are the host. Says goodbye rather than vanishing, so the others learn the seat is free now instead of in thirty seconds. Sends onSessionEnded on this machine. Offline it does nothing. Lua/Python horizon.net.leave · HorizonCode Leave Session · C++ HE::api::net::leave · action
status()
status: int
0 Idle, 1 Hosting, 2 Connecting, 3 Joined, 4 Failed. Joined means the whole world has arrived, not just the connection — a lobby screen may wait for it before it hands over to the game. After a kick or a lost host it stays 4 until the next leave or join. Lua/Python horizon.net.status · HorizonCode Session Status · C++ HE::api::net::status · query
lastError()
error: string
Why the last host or join attempt failed, in words meant for a player. Empty when nothing has failed. After a removal: “Removed from the session.” (anti-cheat on) or “Lost the connection to the host.”. Lua/Python horizon.net.lastError · HorizonCode Session Error · C++ HE::api::net::lastError · query
sessionId()
sessionId: string
This session's short id, for showing to somebody who is about to join. Only the host has it: empty on a client and outside a session. Lua/Python horizon.net.sessionId · HorizonCode Session Id · C++ HE::api::net::sessionId · query
joinCode()
code: string
The secret a joiner needs. Only the HOST gets it — on a client this is deliberately empty, so a client's own UI cannot hand out seats to a session it does not own. 26 characters. Lua/Python horizon.net.joinCode · HorizonCode Join Code · C++ HE::api::net::joinCode · query
refreshLan()
—
Starts listening for sessions announced on the local network, or starts over. Only game sessions are listed; an editor collaboration session on the same network is not one you can join. Needs the system's Local Network permission, see the group notes above. Lua/Python horizon.net.refreshLan · HorizonCode Refresh LAN Sessions · C++ HE::api::net::refreshLan · action
lanSessionCount()
count: int
How many sessions the LAN browser is currently hearing. Zero right after Refresh is normal: announcements arrive a moment later. Lua/Python horizon.net.lanSessionCount · HorizonCode LAN Session Count · C++ HE::api::net::lanSessionCount · query
lanSessionName(index: int)
name: string
The host's display name for the session at this index. Lua/Python horizon.net.lanSessionName · HorizonCode LAN Session Name · C++ HE::api::net::lanSessionName · query
lanSessionPlayers(index: int)
players: int
How many players are in the session at this index, as it last announced itself. Lua/Python horizon.net.lanSessionPlayers · HorizonCode LAN Session Players · C++ HE::api::net::lanSessionPlayers · query
isAuthority()
isAuthority: bool
Is THIS machine the one that decides? True on the host and true with no session at all — a single-player game is its own authority. Ask this before anything that changes the world: simulation only with Is Authority, display everywhere, intentions through the server. Lua/Python horizon.net.isAuthority · HorizonCode Is Authority · C++ HE::api::net::isAuthority · query
isClient()
isClient: bool
Are we a client in somebody else's session? False offline, unlike Is Authority, because offline there is no host to be a client of. Lua/Python horizon.net.isClient · HorizonCode Is Client · C++ HE::api::net::isClient · query
localPlayer()
player: int
Our own player number. 1 on the host and 1 offline; the host hands out 2 and up in join order. A number is minted once and never reused, so a score kept under it stays the right player's. 0 on a client that is still connecting. Lua/Python horizon.net.localPlayer · HorizonCode Local Player · C++ HE::api::net::localPlayer · query
playerCount()
count: int
How many players the session are known HERE. On the host that is all of them; on a client it is 1 — itself — because nothing sends a client the list yet. 1 offline as well. Lua/Python horizon.net.playerCount · HorizonCode Player Count · C++ HE::api::net::playerCount · query
playerAt(index: int)
player: int
The player number of the index'th player, in join order. 0 past the end, so a loop can stop on it. Only the host sees more than itself (see Player Count). On a client playerAt(0) is its own number. Lua/Python horizon.net.playerAt · HorizonCode Player At · C++ HE::api::net::playerAt · query
playerName(player: int)
name: string
The display name that player joined with. Empty for a player this machine does not know about — on a client that is everybody but itself. Inside onPlayerLeft the name is already gone. Lua/Python horizon.net.playerName · HorizonCode Player Name · C++ HE::api::net::playerName · query
ping(player: int)
ms: float
Round trip to that player in milliseconds, as the host measures it. 0 when there is nothing to measure: ourselves, a player nobody has timed yet, and every player on a client, which knows only itself. Lua/Python horizon.net.ping · HorizonCode Player Ping · C++ HE::api::net::ping · query
kick(player: int)
—
Host only: remove a player from the session. With anti-cheat on it takes the anti-cheat's path: the player is told and sees reason 2, Kicked. Without, the connection is simply closed and the player sees reason 1, a lost connection. Does nothing on a client or offline. Lua/Python horizon.net.kick · HorizonCode Kick From Session · C++ HE::api::net::kick · action
ownerOf(entity: int)
player: int
Which player this entity belongs to, or 0 for the host's own and for anything nobody owns (a door, a crate). Lua/Python horizon.net.ownerOf · HorizonCode Owner Of · C++ HE::api::net::ownerOf · query · entity is Self in HorizonCode
isLocallyControlled(entity: int)
local: bool
Do WE drive this entity? True for our own character, false for everybody else's, and true for anything not replicated at all. This is the question a character's graph asks before it reacts to input. Entity 0 or an id nothing answers to: false. Lua/Python horizon.net.isLocallyControlled · HorizonCode Is Locally Controlled · C++ HE::api::net::isLocallyControlled · query · entity is Self in HorizonCode
localCharacter()
entity: int
The entity this machine drives — our own character — or 0 before the host has given us one. 0 in every state measured here (no host assigned a character). Lua/Python horizon.net.localCharacter · HorizonCode Local Character · C++ HE::api::net::localCharacter · query
Declares a replicated Bool on this entity: the host owns it, every client is sent its value. Call it in On Init. With Notify on, the clients get On Rep when a value arrives. A HorizonCode class does not need this — it ticks Replicated in the variable list instead. Entity 0 or an empty name: false. A missing notify is false in Lua and an IndexError in Python. See Replicated variables. Lua/Python horizon.net.declareVarBool · HorizonCode Declare Replicated Bool · C++ HE::api::net::declareVarBool · action · entity is Self in HorizonCode
Declares a replicated Int on this entity. See Declare Replicated Bool. 32-bit: 3000000000 is stored as −1294967296. Lua/Python horizon.net.declareVarInt · HorizonCode Declare Replicated Int · C++ HE::api::net::declareVarInt · action · entity is Self in HorizonCode
Declares a replicated Float on this entity. See Declare Replicated Bool. Lua/Python horizon.net.declareVarFloat · HorizonCode Declare Replicated Float · C++ HE::api::net::declareVarFloat · action · entity is Self in HorizonCode
Declares a replicated String on this entity. See Declare Replicated Bool. Lua/Python horizon.net.declareVarString · HorizonCode Declare Replicated String · C++ HE::api::net::declareVarString · action · entity is Self in HorizonCode
Declares a replicated Vector on this entity. See Declare Replicated Bool. In Lua the old value of onRep_<name> is the number 0 (known issue, see Replicated variables). Lua/Python horizon.net.declareVarVec3 · HorizonCode Declare Replicated Vector · C++ HE::api::net::declareVarVec3 · action · entity is Self in HorizonCode
Writes a declared Bool. On the host every client is sent the new value; on a client it takes effect HERE and the host's next value replaces it, which is what makes a prediction possible. False = never declared. On a client the write is its own prediction until the host's value comes back. A variable of another type is overwritten too: the row answers true and the variable changes type. See Replicated variables. Lua/Python horizon.net.setVarBool · HorizonCode Set Replicated Bool · C++ HE::api::net::setVarBool · action · entity is Self in HorizonCode
setVarInt(entity: int, name: string, value: int)
ok: bool
Writes a declared Int. See Set Replicated Bool. Lua/Python horizon.net.setVarInt · HorizonCode Set Replicated Int · C++ HE::api::net::setVarInt · action · entity is Self in HorizonCode
Writes a declared Float. See Set Replicated Bool. Lua/Python horizon.net.setVarFloat · HorizonCode Set Replicated Float · C++ HE::api::net::setVarFloat · action · entity is Self in HorizonCode
Writes a declared String. See Set Replicated Bool. Lua/Python horizon.net.setVarString · HorizonCode Set Replicated String · C++ HE::api::net::setVarString · action · entity is Self in HorizonCode
Writes a declared Vector. See Set Replicated Bool. Lua/Python horizon.net.setVarVec3 · HorizonCode Set Replicated Vector · C++ HE::api::net::setVarVec3 · action · entity is Self in HorizonCode
getVarBool(entity: int, name: string)
value: bool
Reads a declared Bool. False when the name was never declared here, which Has Replicated Variable tells apart from a value that is simply false. A variable declared with another type reads as false. Lua/Python horizon.net.getVarBool · HorizonCode Get Replicated Bool · C++ HE::api::net::getVarBool · query · entity is Self in HorizonCode
getVarInt(entity: int, name: string)
value: int
Reads a declared Int. 0 when the name was never declared here. Lua/Python horizon.net.getVarInt · HorizonCode Get Replicated Int · C++ HE::api::net::getVarInt · query · entity is Self in HorizonCode
getVarFloat(entity: int, name: string)
value: float
Reads a declared Float. 0 when the name was never declared here. Lua/Python horizon.net.getVarFloat · HorizonCode Get Replicated Float · C++ HE::api::net::getVarFloat · query · entity is Self in HorizonCode
getVarString(entity: int, name: string)
value: string
Reads a declared String. Empty when the name was never declared here. Lua/Python horizon.net.getVarString · HorizonCode Get Replicated String · C++ HE::api::net::getVarString · query · entity is Self in HorizonCode
getVarVec3(entity: int, name: string)
value: vec3
Reads a declared Vector. Zero when the name was never declared here. Lua/Python horizon.net.getVarVec3 · HorizonCode Get Replicated Vector · C++ HE::api::net::getVarVec3 · query · entity is Self in HorizonCode
callServer(entity, name, …)
ok: bool
Ask the HOST to run a function on this entity. The ordinary way to say "I pulled the lever": a client may not change the world, so it asks. Nothing comes back — a remote call has no return value. Allowed for the entity you own, or for a function whose header has Any Client ticked; anything else the host drops, and with anti-cheat on it is a Hard report that removes the caller. Offline and on the host it runs here, on the entity's HorizonCode class, so a graph works in single player; a Lua or Python method is not reached that way. In Lua and Python the arguments after name are passed on as the remote function's arguments, as many as it takes (see Remote calls). On a client true means sent, also for a call the host then refuses. Offline and on the host a Lua or Python method is not reached: the row answers false and nothing runs, so call the method directly (see Remote calls). Lua/Python horizon.net.callServer · HorizonCode Call Server · C++ HE::api::net::callServer · action · entity is Self in HorizonCode
callClient(player, entity, name, …)
ok: bool
Host only: run a function on ONE player's machine — the hit marker, the message only they should see. Addressed by PlayerId. Nothing comes back. In Lua and Python the arguments after name are passed on as the remote function's arguments, as many as it takes (see Remote calls). Answers false and runs nothing in a script on a client, for the host's own player and for a player number nobody has. Lua/Python horizon.net.callClient · HorizonCode Call Client · C++ HE::api::net::callClient · action
callAllClients(entity, name, …)
ok: bool
Host only: run a function on EVERY machine, including this one — the round-over horn, the explosion everybody sees. Nothing comes back. Here on the host only the entity's HorizonCode class runs it (a Lua or Python method is not reached, and the result is then false although the clients were sent the call). In Lua and Python the arguments after name are passed on as the remote function's arguments, as many as it takes (see Remote calls). Answers false on a client, and on the host for an entity without a HorizonCode class, although the clients were sent the call and ran it; the host's own script method does not run. Lua/Python horizon.net.callAllClients · HorizonCode Call All Clients · C++ HE::api::net::callAllClients · action · entity is Self in HorizonCode
allowAnyClient(entity: int, function: string)
ok: bool
Let any client call that function on this entity, not just its owner. For Lua, Python and C++ classes, which have no function header to tick. A door belongs to nobody, so without this nobody could open it. Lasts one session: call it in onSessionStarted. false offline or for an empty name; true on a client and for an entity without a network id, where it does nothing. See Remote calls. Lua/Python horizon.net.allowAnyClient · HorizonCode Allow Any Client · C++ HE::api::net::allowAnyClient · action · entity is Self in HorizonCode
rpcSender()
player: int
Which player asked for the call being handled right now: a player number, as Player Name and Kick From Session take it. 0 at any other moment. Not a connection number, so it is not what the anti-cheat rows take (Report Player gives that one). On a client, inside a call the host made: 1. Lua/Python horizon.net.rpcSender · HorizonCode Call Sender · C++ HE::api::net::rpcSender · query
hasVar(entity: int, name: string)
declared: bool
Is this name declared on this entity at all — by a script or by the entity's HorizonCode class? The one row that tells a missing declaration apart from a value still at its default. Also true for a variable a client declared on its own copy. Lua/Python horizon.net.hasVar · HorizonCode Has Replicated Variable · C++ HE::api::net::hasVar · query · entity is Self in HorizonCode
The host's anti-cheat scores what clients send and decides what to do
about it. It runs on the host of a multiplayer session whose project has
anti-cheat switched on (Project Settings, Game, Anti-Cheat). Each report
reaches every script as onCheatDetected(reportId); the
report… rows read it, respond overrules the
host's reaction. A client gets the same callback once, when the host
tells it that it is being removed.
▲
Known issue (build 7d49d44f): anti-cheat never runs, in the
exported game and in Play as Host alike. The project setting
does not reach the session: net.host, --host
and Play as Host all open it without the anti-cheat settings. And the
anti-cheat that scripts, graphs and C++ modules talk to is a second
object that no session is connected to. So in every frontend
isEnabled is false, checktrue, report, respond,
kick and setPlayerLabel do nothing, the
readers answer 0 or "", and
onCheatDetected never fires. The rest of this section
was measured with the anti-cheat connected by hand, the way the
engine intends it.
player is a connection number, not a player
number. Every row here that takes a player, and
reportPlayer, counts the host's connections, which are
numbered separately from the players of net. The
first joiner was player 2 on connection 1; after a failed join attempt
had used connection 1, the next joiner was player 2 on connection 2.
No row converts one into the other, so take the number from
reportPlayer. A report filed under
net.rpcSender() (player 2) went to connection 2, which
was nobody, and the real caller's score stayed 0. A connection
nobody holds is scored and reported all the same.
Score and level.report(player, rule, weight,
detail) adds weight to that connection's score,
which halves every 30 seconds (the defaults on the Anti-Cheat page).
Reaching 5 files a Suspect report (level 1), 20 a
Confirmed one (level 2). Hard (level 3) is a single
observation no honest build sends, for example a
remote call to an entity the client may not
call. reportDetail lists the observations behind the
score (“Custom 6.00 (Aimbot: through wall)”).
Reactions. Unless a handler says otherwise the host
logs Suspect and Confirmed reports and kicks on Hard.
respond(reportId, response) replaces that, but only
inside onCheatDetected, in the frame the report fired:
respond(id, 1 + 8) made the host log and flag,
1 + 16 log and kick. A later call is ignored with the log
line “no such pending report”. Log is always kept.
check always answers true.
The rules on the Anti-Cheat page are saved with the project but
nothing reads them yet (measured with a rule “Damage” from 0 to 100
and a value of 500), so code that applies a value only when
check agrees keeps working when they arrive.
What the kicked client learns. Its scripts get
onCheatDetected with reportPlayer 0 (“you”),
then onDisconnected(2) with “Removed from the session.”.
After kick(player, code) it reads
reportReason = code, level 2 and an empty
rule; after a kick by the host's policy or a respond, the
rule's name and reason 0. Score and detail are never sent to a client.
In the editor's Play mode nobody is kicked.
Labels. A client's join name is its label from the
start; setPlayerLabel replaces it in the log and in later
reports (“conn 1 [Anna#1]”).
expectDisplacement was not measured here: it needs a
moving character.
Example: anticheat.respond
cheat_log.lua
local M = {}
local AC = horizon.anticheat
function M.onCheatDetected(self, id)
if AC.reportPlayer(id) == 0 then
-- On a client: the host is removing us. Say why before the link drops.
horizon.log("Removed from the session: " .. AC.reportRule(id) .. " (code " .. AC.reportReason(id) .. ")")
return
end
-- On the host. reportPlayer is a CONNECTION number, not a net player number.
horizon.log(string.format("[anti-cheat] %s, connection %d, score %.1f: %s",
AC.reportRule(id), AC.reportPlayer(id), AC.reportScore(id), AC.reportDetail(id)))
if AC.reportLevel(id) == 2 then
AC.respond(id, 1 + 16) -- Confirmed: log and kick (the default policy only logs)
end
end
return M
cheat_log.py
import horizon
AC = horizon.anticheat
class CheatLog(horizon.Behavior):
def on_cheat_detected(self, rid):
if AC.reportPlayer(rid) == 0:
# On a client: the host is removing us. Say why before the link drops.
horizon.log("Removed from the session: %s (code %d)" % (AC.reportRule(rid), AC.reportReason(rid)))
return
# On the host. reportPlayer is a CONNECTION number, not a net player number.
horizon.log("[anti-cheat] %s, connection %d, score %.1f: %s" % (
AC.reportRule(rid), AC.reportPlayer(rid), AC.reportScore(rid), AC.reportDetail(rid)))
if AC.reportLevel(rid) == 2:
AC.respond(rid, 1 + 16) # Confirmed: log and kick (the default policy only logs)
Function
Returns
Description
check(rule: string, value: float, player: int)
ok: bool
Asks the host's anti-cheat whether a value a client claimed (damage, a pickup, a currency delta) is allowed by the rule of that name. True = apply it. The rule table is not built yet: the rules on the project's Anti-Cheat page are saved but never read, so every check answers true. That is also the answer on a client, with anti-cheat off and for a rule nobody declared: a check the engine cannot make never blocks the game. Player is a connection number, like Report Player's. Lua/Python horizon.anticheat.check · HorizonCode Check Rule · C++ HE::api::anticheat::check · action
Tells the anti-cheat that this entity is about to move a long way on purpose (respawn, portal, dash), up to Max Distance. One shot: the next move is allowed, then the usual speed check is back. Without it a respawn would count as a teleport against the player. Lua/Python horizon.anticheat.expectDisplacement · HorizonCode Expect Displacement · C++ HE::api::anticheat::expectDisplacement · action · entity is Self in HorizonCode
Reports something the engine cannot see for itself — a shot through a wall, an impossible pickup — against this player, under a rule name of your choosing. Weight is what it adds to the player's score (5 is the default Suspect threshold, 20 Confirmed); the detail goes into the log. Filed at the end of the frame; onCheatDetected fires then. See the group notes on connection numbers. Lua/Python horizon.anticheat.report · HorizonCode Report Observation · C++ HE::api::anticheat::report · action
setPlayerLabel(player: int, label: string)
—
Gives a player a name for the anti-cheat's log, reports and telemetry. Without one a player is only a connection number. Also what a Ban holds on to for the rest of the session. Lua/Python horizon.anticheat.setPlayerLabel · HorizonCode Set Player Label · C++ HE::api::anticheat::setPlayerLabel · action
respond(reportId: int, response: int)
—
Inside On Cheat Detected: replaces what the host would do for this report. Response is a sum of 1 log, 2 event, 4 telemetry, 8 flag, 16 kick, 32 ban; 0 means log only. The host acts at the END of the frame, so this only counts in the frame the event fired. Lua/Python horizon.anticheat.respond · HorizonCode Respond To Report · C++ HE::api::anticheat::respond · action
kick(player: int, reasonCode: int)
—
Removes a player from the session at the end of this frame, whatever the score says. The client is told the Reason Code first, so it can show why instead of a dead connection. Ignored in the editor's play mode. The client reads reasonCode back through reportReason; its ticket has level 2 and an empty rule. Lua/Python horizon.anticheat.kick · HorizonCode Kick Player · C++ HE::api::anticheat::kick · action
reportLevel(reportId: int)
level: int
How serious the report is: 0 Info, 1 Suspect, 2 Confirmed, 3 Hard. Hard means a single thing no honest client can send; the other two are the decaying score crossing a threshold. Lua/Python horizon.anticheat.reportLevel · HorizonCode Report Level · C++ HE::api::anticheat::reportLevel · query
reportRule(reportId: int)
rule: string
The name of what was violated: an engine rule like DtBudget or Displacement, or the name your own Report Observation used. On a client this is the one thing the host tells you about a kick. Empty on the client after an explicit kick, which sends a reason code instead. Lua/Python horizon.anticheat.reportRule · HorizonCode Report Rule · C++ HE::api::anticheat::reportRule · query
reportPlayer(reportId: int)
player: int
The connection the report is about — the number Check Rule, Report Observation, Set Player Label, Kick Player and Player Score take. Not a multiplayer player number: players are numbered 1, 2, … in join order, connections separately (the first joiner is usually player 2 on connection 1). 0 on a client, where the report is about you. Lua/Python horizon.anticheat.reportPlayer · HorizonCode Report Player · C++ HE::api::anticheat::reportPlayer · query
reportEntity(reportId: int)
entity: int
The network id of the entity involved, when the report names one (a movement report names the character). 0 when it does not. A network id, not an entity id. 0 in every report measured here. Lua/Python horizon.anticheat.reportEntity · HorizonCode Report Entity · C++ HE::api::anticheat::reportEntity · query
reportScore(reportId: int)
score: float
The player's score when the report was made. 0 on a client: the host never tells a player their score. Lua/Python horizon.anticheat.reportScore · HorizonCode Report Score · C++ HE::api::anticheat::reportScore · query
reportDetail(reportId: int)
detail: string
The observations behind the report as one readable line, for a log or an admin overlay. On a client only the rule name — nothing about how the host decides. Lua/Python horizon.anticheat.reportDetail · HorizonCode Report Detail · C++ HE::api::anticheat::reportDetail · query
reportReason(reportId: int)
reasonCode: int
The reason code a kick carried (Kick Player's Reason Code), read on the client to show the right message. 0 when the host's policy kicked. Lua/Python horizon.anticheat.reportReason · HorizonCode Report Reason · C++ HE::api::anticheat::reportReason · query
playerScore(player: int)
score: float
The player's current anti-cheat score, decaying over time. For a diagnostic overlay; 0 with anti-cheat off or on a client. Halves every 30 seconds with the default settings. Lua/Python horizon.anticheat.playerScore · HorizonCode Player Score · C++ HE::api::anticheat::playerScore · query
isEnabled()
enabled: bool
Is the anti-cheat running in this session? True only on a host that has it enabled in the project settings. Useful to hide an admin overlay everywhere else. false on a client. Lua/Python horizon.anticheat.isEnabled · HorizonCode Anti-Cheat Enabled · C++ HE::api::anticheat::isEnabled · query