Skip to content

Core: application, game, scenes and nodes

The application and its frame loop, the game and its scenes, nodes and their transforms, events, timers and the package’s memory model.

Files: app.odin, game.odin, scene.odin, node.odin, transform.odin, event.odin, node_events.odin, timer.odin, ticker.odin, layers.odin, custom_nodes.odin.

Node events for tools and scene files. Every event worth listening to is an EventSubject field of a node type, tagged with what it means and how to read its data:

onFinished: EventSubject `event:"It finished" data:"index := data.(int)"`,

NodeType_Events lists a type’s tagged subjects (its own, those of the types it embeds, and those of struct fields such as an AnimatedSprite’s animation, named “animation.onAnimationEnded”), so an editor shows them and scene files name them.

Scene files connect them to scripts: <listener event target handler/> inside a node (NodeListenerData). A script registers its handlers by name with Script_Listener; just before the scene’s scripts get ready, each listener’s handler is subscribed (bindListeners), and is called with the listening script, the node whose event it is, and the event’s data. The listening node is looked up by id on each call: a removed one is skipped.

Hero_onCoinCollected :: proc(self: ^Hero, source: ^e.Node, data: e.EventData) {
self.coins += 1
}
@(init)
register_Hero_onCoinCollected :: proc "contextless" () {
e.Script_Listener(Hero, "onCoinCollected", Hero_onCoinCollected)
}

Custom node types: a struct of the game’s own that starts with a built-in node type, registered under its name, and used in scene files as a kind of its own (kind="Player").

Player :: struct {
using sprite: e.Sprite, // the base kind: everything a Sprite is and does
speed: f32 `export:"default=200"`, // its own settings, set from scene files
lives: int `export:"default=3"`,
}
@(init)
register_Player :: proc "contextless" () {
e.NodeType_Register(Player, .Sprite, {
update = proc(self: ^Player, events: []sdl.Event, dt: f64) {
self.position.x += self.speed * f32(dt) // self is the node
},
})
}
  • What it is: a node of the base kind, built from the scene file’s attributes as usual (a Sprite’s texture, a body’s shapes…), allocated as the bigger struct; node.type is the custom type (Node_Is(node, Player), cast(^Player)node), and Node_Is with the base type is false: test with SceneNodeKind_IsType or typeEmbeds for “a kind of Sprite”.
  • Its hooks and fields are a script’s (ScriptHooks, export tags: script.odin), with the node itself as self, run before any scripts attached to it (scripts attach as usual). Its exported fields’ values are <prop name value/> children of the node.
  • Scene files say kind="Player" baseKind="Sprite": where Player isn’t registered (an editor, a player without the game’s scripts) the node is built as its base kind.
  • The base type must be the struct’s first field (using sprite: e.Sprite); one that isn’t, or whose base kind’s type differs, isn’t registered (printed on stderr).
Application :: struct {
// The window opened by `Application_Setup`
window: ^sdl.Window,
// The window's renderer, shared by every game the application runs
renderer: ^sdl.Renderer,
// The audio mixer from `Audio_Init`
mixer: ^mix.Mixer,
// True while the frame loop runs; false once the game sets `shouldQuit`
running: bool,
// The game being played. nil until the first frame after `App_SetGame`
game: ^Game,
// The game `App_SetGame` asked for; the next frame switches to it
queuedGame: ^Game,
}

The program’s window, renderer and audio device, and the game they run.

Set it up with Application_Setup, run it with Application_Run and free it with Application_Cleanup. One application runs one Game at a time; App_SetGame swaps it.

struct. Source: app.odin:19

ScaleMode :: enum {
// Keep the aspect ratio, fill the rest with black bars
Letterbox,
// Fill the window, distorting the aspect ratio
Stretch,
// Scale by whole multiples only, for crisp pixel art
IntegerScale,
// Keep the aspect ratio, crop the edges that don't fit
Overscan,
// No scaling: game coordinates are window coordinates
None,
}

How the game’s fixed resolution (Game.size) is fit into the window

enum. Source: game.odin:13

Game :: struct {
// The audio mixer (from the application)
mixer: ^mix.Mixer,
// The window the game draws into (from the application)
window: ^sdl.Window,
// The renderer the game draws with (from the application)
renderer: ^sdl.Renderer,
// Set it to end the game: the application stops after the current frame. Closing the window
// sets it
shouldQuit: bool,
// Quit when Escape is pressed. Set to false in build if the game uses Escape itself
quitOnEscape: bool,
// Developer keys, for games run from the editor (the player's --dev; never in exports):
// F9 shows / hides the physics shapes (DEV_KEY_PHYSICS_DEBUG), F10 the debug overlay
// (DEV_KEY_DEBUG_OVERLAY)
devKeys: bool,
// Show the debug overlay (debug.odin): frame rate, frame times, the scene's counts
debugOverlay: bool,
// Recent frame times, for the overlay (`Game_GetFrameStats`)
frameStats: FrameStats,
// Translation tables and the language in use (localization.odin; in the game's arena)
localization: Localization,
// Set by `Game_Init`
initialized: bool,
// The color the window is cleared to each frame (black by default)
backgroundColor: sdl.Color,
// Resolution the game is designed for. All drawing and event positions are in this
// coordinate space. Leave as zero to use the window size at init
size: [2]f32,
// How `size` is fit into the window. Change it with `Game_SetScaleMode`
scaleMode: ScaleMode,
// Current size of the window, kept up to date as it is resized
windowSize: [2]f32,
// Textures, sounds and fonts loaded by path (resource_manager.odin)
resourceManager: ResourceManager,
// Scenes are heap allocated so pointers to them stay valid as more scenes are added
scenes: map[string]^Scene,
// The scene `Game_SwitchScene` asked for; entered at the start of the next update
nextScenePtr: ^Scene,
// The scene being played. nil before the first switch
currentScene: ^Scene,
// The game's name (the project's name when loaded from one); names its save folder (save.odin)
name: string,
// Global scripts, in their start order (globals.odin)
globals: [dynamic]GlobalInstance,
// Data resources loaded so far, by path (data.odin)
data: map[string]DataInstance,
// Memory that lives as long as the game: globals, data resources (Game_GetAllocator)
arena: MemoryArena,
// Whether `arena` has been set up (on the first `Game_GetAllocator`)
arenaReady: bool,
// How its scenes' physics behave (physics.odin); zero means DEFAULT_PHYSICS. Set before
// scenes create physics nodes
physics: PhysicsSettings,
// The keyboard, mouse, gamepads and the project's actions (input.odin)
input: InputState,
// Mix buses, Master first, and every sound node (audio_nodes.odin)
buses: [dynamic]AudioBus,
// Achievements, stats and cloud files (steam.odin)
steam: SteamState,
// Every sound player of every scene (audio_nodes.odin); the players belong to their scenes
audioPlayers: [dynamic]^AudioPlayer,
// Components read so far, by path (nil: couldn't be read); in the game's memory (project_loader.odin)
components: map[string]^SceneData,
// Animation sets read so far, by path (nil: couldn't be read); in the game's memory
// (animation_sets.odin)
animationSets: map[string]^AnimationSetData,
// Atlases read so far, by path (nil: couldn't be read); in the game's memory (atlas.odin)
atlases: map[string]^AtlasData,
// Dialogues read so far, by path (nil: couldn't be read; dialogue_box.odin), and the
// conversations' variables every DialogueBox shares
dialogues: map[string]^DialogueData,
dialogueVariables: DialogueVariables,
// Scene changes with transitions, scenes over scenes, the loading screen (scene_flow.odin)
flow: SceneFlow,
// The live link with an editor (live.odin; nil unless the player runs with `--live`)
live: ^LiveLink,
// How long the parts of recent frames took (live.odin)
profile: GameProfile,
// UI themes read so far, by path (nil: couldn't be read); in the game's memory (ui_theme.odin)
themes: map[string]^ThemeData,
// Physics materials read so far, by path (nil: couldn't be read); in the game's memory
// (physics_material.odin)
physicsMaterials: map[string]^PhysicsMaterial,
// The project's UI theme (project.xml's `uiTheme`; "": the default), used by controls with none
uiTheme: string,
// Aseprite files read for their slices and events, by full path (aseprite_nodes.odin; each
// in its own arena)
aseprites: map[string]AsepriteEntry,
// LDtk projects read so far, by path (nil: couldn't be read); each in its own arena (ldtk_nodes.odin)
ldtk: map[string]^LDtkProject,
// Development runs: the LDtk files' modification times when read, by path
ldtkTimes: map[string]i64,
// Development runs: LDtk projects replaced by a reload (kept until the end: entity nodes
// point into them)
ldtkRetired: [dynamic]^LDtkProject,
// Development runs: seconds until the LDtk files are looked at again
ldtkWatchTimer: f64,
// Seconds to the next look at the texture files (development runs)
textureWatchTimer: f64,
// The folder asset paths are relative to (owned; set by Game_LoadProject, "" for the working directory)
projectDir: string,
// Seconds left of a hitstop (Game_HitStop): scenes and globals get a delta time of 0
hitStop: f64,
// Seconds the game has run (scaled like scenes' time; stopped by hitstops): shaders' `time`
time: f64,
// The shaders loaded, by path (shaders.odin; in the game's arena)
shaders: map[string]^ShaderProgram,
// GPU particles' pipelines (gpu_particles.odin)
gpuParticles: GPUParticleResources,
// A hot reload's snapshot whose nodes are still to restore (hot_reload.odin; the game's arena)
pendingSnapshot: string,
// Multiplies the time scenes and globals see: 0.5 is slow motion, 2 double speed
// (`Game_SetTimeScale`). 0 counts as 1, so a game that never sets it runs normally; to stop
// time, pause the scene (`Scene_SetPaused`)
timeScale: f32,
// Each scene's file, by scene name (Game_LoadProject; in the game's arena): a scene entered
// again is rebuilt from it
sceneFiles: map[string]string,
// Called every frame by the application (`Game_Update`, set by `Game_Init`)
update: proc(game: ^Game, events: []sdl.Event, deltaTime: f64),
// Called every frame after `update` (`Game_Draw`, set by `Game_Init`)
draw: proc(game: ^Game),
// `Game_CreateScene`, set by `Game_Init`: `game->createScene("Level")`
createScene: proc(game: ^Game, name: string) -> ^Scene,
// `Game_SwitchScene`, set by `Game_Init`: `game->switchScene("Level")`
switchScene: proc(game: ^Game, name: string) -> bool,
// Set it before init: `Game_Init` calls it once to create the game's scenes and switch to
// the first one
build: proc(game: ^Game),
}

A game: its resolution and scaling, its scenes by name, and what lives as long as the game (resources, input, mix buses, globals, data resources, its memory arena).

Allocate it (new(e.Game)), set size, scaleMode and build, and hand it to Application_Run (or call Game_Init). The update, draw, createScene and switchScene procedures are set by Game_Init and called with ->. Game_Destroy frees what the game owns, but not the Game itself.

struct. Source: game.odin:41

Scene :: struct {
// Unique id
id: EntityId,
// Its name in the game (`Game_CreateScene`); "" for scenes made another way
name: string,
// The game it belongs to (set by `Game_CreateScene`; nil for scenes made another way)
game: ^Game,
// Every node in the scene, children included, in the order they were added (updates run in
// this order)
nodes: [dynamic]^Node,
// Nodes added since the last update; they join the scene at the start of the next one
addQueue: [dynamic]^Node,
// Ids of nodes removed since the last update; they're freed at the start of the next one
removeQueue: [dynamic]EntityId,
// Nodes made with `CreateNode(..., addToScene = false)` and not added yet: the scene still
// owns them, so destroying the scene frees them
detached: [dynamic]^Node,
// Every node the scene owns (in it, queued, detached) by id: Scene_GetNode and removals
index: map[EntityId]^Node,
// Its running tweens (tween.odin), updated after the nodes
tweens: [dynamic]^Tween,
// Debug shapes queued to draw over it (debug.odin)
debugShapes: [dynamic]DebugShape,
// The scene's memory (`Scene_GetAllocator`), freed by `Scene_Destroy`
arena: MemoryArena,
// Optional camera, nil by default. When set, every layer not in `screenLayers` is drawn
// through it, and its pans and bounds are applied after each update. Create with `Scene_CreateCamera`
camera: ^Camera2D,
// Layers drawn in screen space, ignoring `camera`. Put HUDs and menus on these layers
screenLayers: bit_set[RenderLayer],
// The color the screen is cleared with while it plays (nil: the game's `backgroundColor`)
background: Maybe(sdl.Color),
// Its physics world's gravity, px/s² (nil: the game's `physics.gravity`). Set before the world
// is made; afterwards use `Physics_SetGravity`
gravity: Maybe([2]f32),
// The physics world (physics.odin), nil until the scene's first physics node is created
physics: ^PhysicsWorld,
// Don't step the physics (bodies still exist and can be queried). Editors set it: their
// previews update scenes for animations, and bodies mustn't fall there
physicsPaused: bool,
// Paused (`Scene_SetPaused`): physics doesn't step, and only nodes whose process mode allows it
// update (`Node_CanProcess`); everything still draws
paused: bool,
// Not built yet: a project with a loading scene builds scenes when they're entered (scene_flow.odin)
unbuilt: bool,
// UI controls' focus and layout passes (ui_controls.odin)
ui: SceneUI,
// IK chains (skeleton.odin), solved after the nodes update, in the order they were made
ikChains: [dynamic]^IKChain,
// ScreenEffect nodes (screen_effects.odin), in the order they were added, and their pictures
screenEffects: [dynamic]^ScreenEffect,
effects: ScreenEffectTargets,
// 2D lighting (lighting.odin): `Scene_SetLighting`
lighting: SceneLighting,
// Camera zones' bounds (camera_tools.odin)
cameraZones: CameraZoneState,
// It has been the current scene before: entering it again rebuilds it from its file
// (Game.sceneFiles), so every visit starts fresh
played: bool,
// `Scene_AddNode`: `scene->addNode(node)`
addNode: proc(scene: ^Scene, node: ^Node),
// `Scene_RemoveNode`: `scene->removeNode(node.id)`
removeNode: proc(scene: ^Scene, nodeId: EntityId),
// `Scene_GetNode`: `scene->getNode(id)`
getNode: proc(scene: ^Scene, nodeId: EntityId) -> ^Node,
// `Scene_ProcessQueues`: `scene->processQueues()`
processQueues: proc(scene: ^Scene),
// `Scene_Update`, called by the game every frame while it's the current scene
update: proc(scene: ^Scene, events: []sdl.Event, deltaTime: f64),
// `Scene_Draw`, called by the game every frame while it's the current scene
draw: proc(scene: ^Scene, renderer: ^sdl.Renderer),
// Notified when the game switches to the scene (source: the scene)
onEnter: EventSubject,
// Notified when the game leaves the scene or is destroyed (source: the scene)
onExit: EventSubject,
}

A scene: a tree of nodes, the queues that add and remove them between frames, and what the scene shares (camera, physics world, tweens, its memory arena).

Make scenes with Game_CreateScene (game->createScene(name)), which owns them, and fill them with CreateNode. Each update applies the queues first, so nodes added or removed during a frame join or leave at the start of the next one. The procedures are called with ->.

struct. Source: scene.odin:18

TransformMode :: enum {
// Position, rotation and scale (parents' and the camera's included)
Full,
// Only the position: drawn upright and unscaled at the node's screen position. Used by UI
// nodes (Label, Menu), whose input handling works in unrotated screen space
Position_Only,
}

How a node’s world transform applies when it draws.

enum. Source: node.odin:12

ProcessMode :: enum u8 {
// Its parent's mode (a top-level node's: Pausable)
Inherit,
// Updates while the scene runs, stops while it's paused (gameplay)
Pausable,
// Updates only while the scene is paused (a pause menu)
WhenPaused,
// Updates whether the scene is paused or not
Always,
// Never updates (it still draws)
Disabled,
}

Whether a node updates (its update procedure, its script’s update), depending on whether its scene is paused (Scene_SetPaused). Drawing isn’t affected.

enum. Source: node.odin:24

BlendMode :: enum u8 {
// Normal alpha blending
Blend,
// Adds its colors (glows, fire, light)
Add,
// Multiplies the colors under it (shadows, tinting)
Multiply,
// Multiplies the colors under it by its own; its alpha is ignored
Modulate,
// Replaces what's under it, alpha included
None,
}

How a node’s drawing combines with what’s under it. Not inherited by children.

enum. Source: node.odin:40

NodeArena :: runtime.Arena

A node’s memory: an arena of heap blocks (Odin’s runtime.Arena) holding the node itself and whatever it allocates with Node_GetAllocator, freed in one step with the node. Heap blocks, not virtual memory: making and freeing a node then costs no system calls (scenes, games and LDtk projects, which are few and large, keep MemoryArena).

type. Source: node.odin:63

Node :: struct {
// Unique id, set by `CreateNode`. Pass it to `Scene_RemoveNode` and `Scene_GetNode`
id: EntityId,
// The type `CreateNode` made it as (`Sprite`, `DynamicBody`, ...); see `Node_Is`
type: typeid,
// The name from the scene file ("" for nodes made in code unless set). Used by
// `Scene_FindNode` and `Node_FindChild`; owned by the node's arena when set by `Scene_Build`
name: string,
// The root of a component instance: the component's file (`Scene_AddComponent`,
// `Node_GetComponent`; in the node's arena). "" for any other node
component: string,
// The layer it draws on (Layer1 first, Layer8 on top). Not inherited by children
layer: RenderLayer,
// The scene that made it (`CreateNode`)
scene: ^Scene,
// Optional. Called every frame while the node is in its scene and may process
// (`Node_CanProcess`); `deltaTime` is in seconds
update: proc(node: ^Node, events: []sdl.Event, deltaTime: f64),
// Optional. Draws the node in its local space: (0, 0) is its position, and the scene applies
// its world transform, the camera and its tint to every Draw* call
draw: proc(node: ^Node, renderer: ^sdl.Renderer),
// Optional. Called just before the node's memory is freed, to release anything that is
// not in the node's arena (e.g. memory from another allocator, SDL resources it owns)
destroy: proc(node: ^Node),
// Optional. The rectangle the node draws, in its local space. Editors use it for picking and
// selection boxes; see `Node_GetLocalBounds`
getBounds: proc(node: ^Node) -> (bounds: sdl.FRect, ok: bool),
// Transform relative to the parent (or the world, for top-level nodes)
position: [2]f32,
// Degrees, clockwise
rotation: f32,
// Scale on each axis ({1, 1} by default; a negative x mirrors it)
scale: [2]f32,
// UI nodes use .Position_Only: they follow their world position but aren't rotated or scaled
transformMode: TransformMode,
// Whether it updates while its scene is paused or running (`Node_CanProcess`)
processMode: ProcessMode,
// How its drawing combines with what's under it
blendMode: BlendMode,
// Its shader and its parameters (shaders.odin; nil none): `Node_SetShader`
shader: ^ShaderMaterial,
// Draw order within its layer: higher draws later (on top). Equal values keep tree order
zIndex: i32,
// Its children draw sorted by their world y (lower first: things further down the screen
// draw on top), for top-down games
ySort: bool,
// Groups it belongs to (`Node_AddToGroup`, `Scene_GetGroup`); names in the node's arena
groups: [dynamic]string,
// A hidden node and its children aren't drawn. They still update; check
// `Node_IsVisibleInTree` in update code that should pause while hidden
visible: bool,
// Multiplies the colors of everything the node and its children draw
tint: sdl.Color,
// Its parent, nil for a top-level node. Set with `Node_AddChild` / `Node_SetParent`
parent: ^Node,
// Its children, in the order they were added (in the node's arena)
children: [dynamic]^Node,
// Set while the node is part of its scene (after it was added, until it's removed)
inScene: bool,
// Set while it waits to be added (Scene_AddNode until the next Scene_ProcessQueues)
queued: bool,
// The script attached to the node, if any (script.odin), and the ones attached after it (each
// a different script; in its arena). Their hooks run in that order
script: NodeScript,
moreScripts: [dynamic]NodeScript,
// A custom node type's hooks and fields (custom_nodes.odin): its data is the node itself
custom: NodeScript,
// Scripts listening to its events, subscribed before the scene's scripts get `ready`
// (`Node_AddListener`, node_events.odin; in its arena)
listeners: [dynamic]NodeListenerData,
// Its listeners were subscribed (`listeners` added later aren't)
listenersBound: bool,
// Notified when the node joins its scene, at the start of the update after it was added
// (source: the node)
onEnter: EventSubject `event:"It joined the scene (the update after it was added)"`,
// Notified when the node leaves its scene (removed, or the scene destroyed), before it's
// freed (source: the node)
onExit: EventSubject `event:"It's leaving the scene (removed, or the scene ended), before it's freed"`,
// Holds this node and everything allocated with Node_GetAllocator
arena: NodeArena,
}

Base type of everything in a scene. Node types embed it as their first field:

Player :: struct {
using node: e.Node,
...
}

Create nodes with CreateNode. Each node owns a memory arena that holds the node itself and anything allocated with Node_GetAllocator: its event listener lists, animation players, strings, etc. When the node is destroyed (removed from its scene, or the scene is destroyed) the arena is freed in one step, so none of that needs freeing by hand.

Every node has a transform (position, rotation, scale) relative to its parent, set with Node_AddChild / Node_SetParent. A node’s draw procedure draws in its own local space: the scene applies its world transform (and the camera) to every Draw* call. Children also inherit visible (hidden nodes aren’t drawn, but still update) and tint. Removing a node removes its children too.

struct. Source: node.odin:84

Transform2D :: struct {
// Where the local X axis points; its length is the X scale
x: [2]f32,
// Where the local Y axis points; its length is the Y scale
y: [2]f32,
// Where the local (0, 0) lands
origin: [2]f32,
}

A 2D affine transform: maps a point p to x * p.x + y * p.y + origin.

x and y are where the local X and Y axes point (their lengths are the scale) and origin is where the local (0, 0) lands. Composing transforms of nodes with rotation and non-uniform scale can produce skew, which a matrix represents exactly.

Example

t := e.Transform2D_Make({100, 50}, 90, {2, 2})
p := e.Transform2D_Apply(t, {1, 0}) // {100, 52}: one unit along local X, rotated 90° clockwise, doubled

struct. Source: transform.odin:18

EventData :: union {
int,
u32,
u64,
f32,
f64,
bool,
rawptr,
string,
}

The value an event carries to its listeners (Event_Notify). nil when there’s none. A string is only valid during the notification: clone it to keep it

union. Source: event.odin:9

EntityId :: uuid.Identifier

A unique id (a version 4 UUID) for a node or an event listener. Made by NewEntityId

type. Source: event.odin:21

ListenerHandle :: EntityId

Identifies a subscription, returned by Event_Subscribe; pass it to Event_Unsubscribe

type. Source: event.odin:39

EventListener :: struct {
// The handle `Event_Subscribe` returned
id: ListenerHandle,
// Called on each notification with the subject, the notifier's source and data, and
// `userData`. nil while a removal waits for the notification to end
callback : proc(event: ^EventSubject, source: rawptr, data: EventData, userData: rawptr),
// Passed back to `callback` as it was given to `Event_Subscribe`
userData : rawptr,
}

One subscription to an EventSubject

struct. Source: event.odin:42

EventSubject :: struct {
// The subscriptions, in the order they subscribed
listeners : [dynamic]EventListener,
// Notifications running (nested ones count): while one runs, unsubscribing only marks the
// listener, and the list is compacted when the last one ends
notifying: int,
// A listener was marked for removal while notifying
compact: bool,
// `Event_Subscribe`, set by `EventSubject_Init`
subscribe: proc(subject: ^EventSubject, callback: proc(event: ^EventSubject, source: rawptr, data: EventData, userData: rawptr), userData: rawptr) -> ListenerHandle ,
// `Event_Unsubscribe`, set by `EventSubject_Init`
unsubscribe: proc(subject: ^EventSubject, handle: ListenerHandle),
// `Event_Notify`, set by `EventSubject_Init`
notify: proc(subject: ^EventSubject, source: rawptr, data: EventData),
}

An event that listeners subscribe to (the observer pattern). Scenes and nodes use them for hooks such as onEnter and onExit.

Set it up with EventSubject_Init; the procedures are called with ->: subject->subscribe(callback, userData), subject->notify(source, data), subject->unsubscribe(handle). Free it with Event_Destroy.

struct. Source: event.odin:60

NodeEventInfo :: struct {
// Its name in scene files: the field's ("onFinished"), through a struct field
// ("animation.onAnimationEnded")
name: string,
// What it means (the `event` tag)
description: string,
// Code reading its data (the `data` tag: "index := data.(int)"); "" when it carries none
dataHint: string,
// The subject's byte offset in the node
offset: uintptr,
}

An event a node type offers: one of its EventSubject fields tagged event:"..."

struct. Source: node_events.odin:35

ScriptListenerProc :: proc(self: rawptr, source: ^Node, data: EventData)

A script’s handler for an event: the script struct, the node whose event it is, its data

type. Source: node_events.odin:180

Timer :: struct {
// Time run since it (re)started
elapsed: time.Duration,
// Whether `update` advances it
running: bool,
// How long until it expires
duration: time.Duration,
// Start over when it expires (otherwise it stops)
loop: bool,
// Every update while running; the data is the elapsed seconds (f32)
onTick: EventSubject,
// When it expires; the data is the overshoot in seconds (f32)
onExpire: EventSubject,
// `TimerNode_Start`: `timer->start()`
start: proc(timer: ^Timer),
// `TimerNode_SetDuration`: `timer->setDuration(seconds)`
setDuration: proc(timer: ^Timer, durationSec: f32),
// Advances a running timer by `deltaTime` seconds: `timer->update(dt)`. Call it every frame
update: proc(timer: ^Timer, deltaTime: f32),
}

A countdown driven by the delta time it’s given (timer->update(timer, deltaTime), usually from a node’s update): it sends onTick on every update while running, and onExpire when duration has passed. A looping timer starts over (keeping any overshoot, so it doesn’t drift); otherwise it stops. Being driven by delta time, it pauses with a hitstop and anything else that stops time.

Example

package mygame
import e "engine:."
import "core:time"
import sdl "vendor:sdl3"
Spawner :: struct {
using node: e.Node,
timer: e.Timer,
}
CreateSpawner :: proc(scene: ^e.Scene) -> ^Spawner {
spawner := e.CreateNode(scene, Spawner)
e.TimerNode_Init(&spawner.timer, 2 * time.Second, auto_start = true, loop = true, allocator = e.Node_GetAllocator(spawner))
spawner.timer.onExpire->subscribe(proc(_: ^e.EventSubject, _: rawptr, _: e.EventData, user: rawptr) {
spawner := cast(^Spawner)user
_ = spawner // every two seconds: spawn something
}, spawner)
spawner.update = proc(node: ^e.Node, _: []sdl.Event, deltaTime: f64) {
spawner := cast(^Spawner)node
spawner.timer->update(f32(deltaTime))
}
return spawner
}

struct. Source: timer.odin:37

TickerCallBack :: proc(ticker: ^Ticker, currentTick: int, user_data: rawptr)

A tick callback: the ticker, its tick count and user data. (Not used by Ticker, which notifies onTick)

type. Source: ticker.odin:6

Ticker :: struct {
// Ticks per second (0 to 120). Change it with `setFPS`
framesPerSecond: f32,
// Time between ticks (0: never ticks)
secondsPerFrame: time.Duration,
// Time since the last tick
elapsedTime: time.Duration,
// When `process` last measured the time
lastTick: time.Tick,
// Ticks so far
currentTick: int,
// Free for the game's use; the ticker doesn't read it
user_data: rawptr,
// Whether `process` advances it
running: bool,
// `Ticker_SetFPS`
setFPS: proc(ticker: ^Ticker, framesPerSecond: f32),
// `Ticker_Start`
start: proc(ticker: ^Ticker, resetCounter: bool),
// `Ticker_Stop`
stop: proc(ticker: ^Ticker),
// `Ticker_Pause`
pause: proc(ticker: ^Ticker),
// `Ticker_Resume`
resume: proc(ticker: ^Ticker),
// `Ticker_TogglePause`
togglePause: proc(ticker: ^Ticker),
// `Ticker_Reset`
reset: proc(ticker: ^Ticker),
// Measures the time and ticks if a tick is due. Call it every frame
process: proc(ticker: ^Ticker),
// Notified on each tick (source: the ticker; data: the tick count, f32)
onTick: EventSubject,
}

Sends onTick at a fixed rate, measured in real (wall clock) time: unlike Timer, it doesn’t follow the delta time, so the time scale, hitstops and paused scenes don’t slow it.

Set it up with Ticker_Init, then call ticker->process() every frame; it ticks at most once per call. The data sent with onTick is the tick count (f32). The procedures are called with ->.

Example

// Given: ticker: ^e.Ticker
e.Ticker_Init(ticker, allocator = e.Node_GetAllocator(node))
ticker->setFPS(4)
ticker.onTick->subscribe(proc(_: ^e.EventSubject, _: rawptr, data: e.EventData, _: rawptr) {
log.infof("tick %v", data)
}, nil)
// Every frame:
ticker->process()

struct. Source: ticker.odin:26

RenderLayer :: enum {
Layer1,
Layer2,
Layer3,
Layer4,
Layer5,
Layer6,
Layer7,
Layer8
}

The layer a node draws on (Node.layer). A scene draws Layer1 first and Layer8 last (on top); within a layer, nodes draw by zIndex, and in tree order among equal ones. Layers in Scene.screenLayers ignore the camera.

enum. Source: layers.odin:8

CollisionLayers :: bit_set[RenderLayer]

A set of layers. (Not used by the engine: physics bodies use u64 collision layers and masks)

bit_set. Source: layers.odin:20

NodeTypeInfo :: struct {
// Its name, hooks and fields, as a script's (data: the node itself)
using info: ScriptInfo,
// The built-in kind it extends
base: SceneNodeKind,
}

A registered custom node type

struct. Source: custom_nodes.odin:44

Application_Setup :: proc(app: ^Application, windowTitle: string, windowSize: [2]f32, backend := RenderBackend.Auto, window := DEFAULT_WINDOW_SETTINGS, audio := DEFAULT_AUDIO_OUTPUT) -> bool

Starts SDL, opens the window with a renderer, the audio device and SDL_ttf.

The window is resizable; the game’s resolution is scaled into it (see Game_SetScaleMode). Call it once, before creating a game; free everything with Application_Cleanup.

Inputs

  • app: The application to fill in
  • windowTitle: The window’s title
  • windowSize: The window’s size in pixels (ignored when window.fullscreen is set)
  • backend: The renderer to use (RenderBackend). .Auto lets SDL choose
  • window: Fullscreen, vsync and high-DPI settings (vsync is applied later on the web)
  • audio: The sound output’s format and device (Audio_OpenMixer falls back when it won’t open)

Returns

  • false (logged) if SDL, the window, the renderer, the audio device or SDL_ttf couldn’t start

Example

app := e.Application{}
if !e.Application_Setup(&app, "My Game", {1280, 720}) do return
defer e.Application_Cleanup(&app)
e.Application_Run(&app, game)

procedure. Source: app.odin:57

Application_Run :: proc(app: ^Application, game: ^Game = nil)

Runs the game until it quits: Application_Frame in a loop.

The browser can’t block like this; there, the page calls Application_Frame once per animation frame (see Player_StartWeb).

Inputs

  • app: The application, set up with Application_Setup
  • game: The game to play (queued with App_SetGame; initialized on the first frame if needed). nil keeps the game already set

Example

app := e.Application{}
if !e.Application_Setup(&app, "My Game", {1280, 720}) do return
defer e.Application_Cleanup(&app)
e.Application_Run(&app, game)

procedure. Source: app.odin:113

Application_Frame :: proc(app: ^Application)

Runs one frame: switches to a queued game, polls events, updates, draws and presents, and frees the frame’s temporary memory (context.temp_allocator).

Switching destroys the previous game (Game_Destroy) and initializes the new one if it isn’t yet. Event positions are converted to the game’s resolution. Sets app.running to false when the game sets shouldQuit.

Inputs

procedure. Source: app.odin:138

Application_Cleanup :: proc(app: ^Application)

Destroys the current game, then closes audio, SDL_ttf, the renderer, the window and SDL.

A game that is only queued (queuedGame) is not destroyed: the caller owns it.

Inputs

  • app: The application to shut down

procedure. Source: app.odin:223

App_SetGame :: proc(app: ^Application, game: ^Game)

Queues a game to play. The next Application_Frame destroys the current game (if it’s a different one) and switches to this one, initializing it if needed.

Inputs

  • app: The application
  • game: The game to switch to. Setting the current game again only re-applies its scaling

procedure. Source: app.odin:248

Game_Init :: proc(game: ^Game, window: ^sdl.Window, renderer: ^sdl.Renderer, mixer: ^mix.Mixer)

Starts a game in the application’s window: sets up its resources and procedures, fits its resolution into the window, then calls game.build.

Application_Frame calls it for a game set with App_SetGame (or Application_Run) that isn’t initialized yet, so most games don’t call it themselves. Set size (zero: the window’s size), scaleMode and build before.

Inputs

  • game: The game to start
  • window: The application’s window
  • renderer: The application’s renderer
  • mixer: The application’s audio mixer

Example

e.Application
game := new(e.Game)
game.size = {640, 360}
game.scaleMode = .IntegerScale
game.build = proc(game: ^e.Game) {
game->createScene("Title")
_ = game->switchScene("Title")
}
e.Game_Init(game, app.window, app.renderer, app.mixer)

procedure. Source: game.odin:198

Game_GetAllocator :: proc(game: ^Game) -> runtime.Allocator

Memory that lives as long as the game (globals and data resources use it). Freed by Game_Destroy.

Inputs

  • game: The game

Returns

  • The allocator of the game’s arena (set up on the first call)

procedure. Source: game.odin:236

Game_Destroy :: proc(game: ^Game)

Frees what the game owns: the current scene exits, every scene and its nodes are destroyed, then the globals, input, LDtk projects, mix buses, resources and the game’s arena.

Application_Cleanup and switching games (App_SetGame) call it. The Game itself isn’t freed.

Inputs

  • game: The game to destroy

procedure. Source: game.odin:252

Game_CreateScene :: proc(game: ^Game, name: string) -> ^Scene

Creates an empty scene and adds it to the game under a name. Call it as game->createScene(name).

The game owns the scene (freed by Game_Destroy) and keeps its own copy of the name.

Inputs

  • game: The game
  • name: The scene’s name, unique in the game

Returns

  • The new scene, or nil (logged) if the game already has a scene with that name

Example

level := game->createScene("Level")
level.screenLayers = {.Layer8}
_ = game->switchScene("Level")

procedure. Source: game.odin:315

Game_SwitchScene :: proc(game: ^Game, name: string) -> bool

Switches to a scene at the start of the next update. Call it as game->switchScene(name).

The scene being left gets its onExit and its sounds stop; the new one gets onEnter. A scene that has been played before is rebuilt from its file (switching to the current scene restarts it); a scene made by code is entered as it is.

Inputs

  • game: The game
  • name: The scene’s name

Returns

  • false (logged) if the game has no scene with that name

Example

if !game->switchScene("GameOver") do log.error("no GameOver scene")

procedure. Source: game.odin:348

Game_Update :: proc(game: ^Game, events: []sdl.Event, deltaTime: f64)

Updates the game for one frame. The application calls it (as game->update).

In order: input, audio, window and quit events (and the developer keys), a pending scene switch, the time scale and hitstop, the globals, then the current scene.

Inputs

  • game: The game
  • events: This frame’s SDL events, positions in game coordinates
  • deltaTime: Real seconds since the last frame

procedure. Source: game.odin:370

Game_SetWindowIcon :: proc(game: ^Game, path: string) -> bool

Changes the game window’s icon (the taskbar’s, the window list’s): an image in the project (PNG, JPEG…), read from the data pack when the game is packed. Projects set one with icon (Project Settings > General > Icon); games without one show the Crossfire logo. Browsers show the page’s favicon instead: this does nothing there.

Inputs

  • game: The game (its window)
  • path: The image, relative to the project folder

Returns

  • false when there’s no window, the image can’t be read (logged), or the window system doesn’t take icons (Wayland compositors without the xdg-toplevel-icon protocol, which show the .desktop entry’s icon instead)

Example

e.Game_SetWindowIcon(game, "assets/icon.png")

procedure. Source: game.odin:499

Game_SetFullscreen :: proc(game: ^Game, fullscreen: bool) -> bool

Switches the game’s window between fullscreen (the desktop’s resolution, the game scaled into it) and windowed.

Inputs

  • game: The game
  • fullscreen: true for fullscreen, false for windowed

Returns

  • false if the window couldn’t change (logged), or there’s no window

procedure. Source: game.odin:520

Game_IsFullscreen :: proc(game: ^Game) -> bool

Whether the game’s window is fullscreen.

Inputs

  • game: The game

Returns

  • true if it’s fullscreen; false if windowed or there’s no window

procedure. Source: game.odin:538

Game_SetVSync :: proc(game: ^Game, vsync: bool)

Turns vsync on (frames wait for the display’s refresh: no tearing) or off (as fast as it can). Does nothing in the browser, where frames always follow the display, or without a renderer.

Inputs

  • game: The game
  • vsync: true to wait for the display’s refresh

procedure. Source: game.odin:550

Game_SetTimeScale :: proc(game: ^Game, scale: f32)

Sets how fast time runs for scenes and globals (physics, animations, particles, timers, tweens, scripts’ delta time): 1 is normal, 0.5 slow motion, 2 double speed. Hitstops and camera shakes run in real time.

Inputs

  • game: The game
  • scale: The speed (above 0; 0 or less becomes 0.0001. To stop time, pause the scene)

Example

e.Game_SetTimeScale(game, 0.5) // slow motion

procedure. Source: game.odin:566

Game_HitStop :: proc(game: ^Game, seconds: f64)

Freezes the game for a moment: a hitstop, the short pause that makes a hit feel heavy. For seconds of real time, scenes and globals update with a delta time of 0: physics, animations, particles, timers and anything scripts move by the delta stop, while input, drawing and the camera’s shake (Camera2D_Shake) go on. A longer hitstop extends a running one; a shorter one doesn’t cut it.

Inputs

  • game: The game
  • seconds: How long, in real seconds (around 0.05 to 0.12 for a hit)

Example

e.Game_HitStop(game, 0.08)
e.Camera2D_Shake(scene.camera, 3, 0.15)

procedure. Source: game.odin:585

Game_Draw :: proc(game: ^Game)

Draws the game for one frame. The application calls it (as game->draw) after the update.

Draws the current scene, then the globals that draw (in screen space), then the debug overlay when it’s on.

Inputs

  • game: The game

procedure. Source: game.odin:598

Game_ApplyScaling :: proc(game: ^Game)

Tells the renderer to draw at game.size and scale the result into the window with game.scaleMode. The renderer recalculates the scale on its own whenever the window is resized.

Game_SetSize and Game_SetScaleMode call it; call it after changing size or scaleMode directly.

Inputs

  • game: The game

procedure. Source: game.odin:624

Game_SetSize :: proc(game: ^Game, size: [2]f32)

Sets the game’s resolution and applies it (Game_ApplyScaling). Drawing and event positions are in this coordinate space.

Inputs

  • game: The game
  • size: The resolution, in pixels

procedure. Source: game.odin:646

Game_SetScaleMode :: proc(game: ^Game, mode: ScaleMode)

Sets how the game’s resolution is fit into the window and applies it (Game_ApplyScaling).

Inputs

  • game: The game
  • mode: The scale mode

Example

e.Game_SetScaleMode(game, .IntegerScale)

procedure. Source: game.odin:661

Game_WindowToGame :: proc(game: ^Game, point: [2]f32) -> [2]f32

Converts a point in window coordinates to game coordinates (through the scaling).

Events already arrive in game coordinates; this is for positions read from SDL directly.

Inputs

  • game: The game
  • point: The point, in window pixels

Returns

  • The point in game coordinates

procedure. Source: game.odin:678

Game_GetMousePosition :: proc(game: ^Game) -> [2]f32

The mouse position in game coordinates, for nodes that poll instead of using events.

Inputs

  • game: The game

Returns

  • The mouse position in game coordinates (screen space: the camera isn’t applied)

procedure. Source: game.odin:693

Scene_Init :: proc(scene: ^Scene)

Prepares an empty scene: its arena, procedures, node index and onEnter / onExit. Game_CreateScene calls it; call it yourself only for a scene outside a game (editors, tests). Panics if the arena can’t be set up.

Inputs

  • scene: The scene (zeroed)

procedure. Source: scene.odin:103

Scene_GetAllocator :: proc(scene: ^Scene) -> mem.Allocator

Allocator backed by the scene’s arena. Anything allocated with it is freed all at once when the scene is destroyed, so it suits data shared by the whole scene. For data that belongs to one node, prefer Node_GetAllocator so it is freed when that node is removed.

Inputs

  • scene: The scene

Returns

  • The scene arena allocator

procedure. Source: scene.odin:130

Scene_Clear :: proc(scene: ^Scene)

Empties a scene: every node is destroyed and its physics world goes, as Scene_Destroy does, but the scene itself stays (its name, game, screen layers, physicsPaused), ready to be built again. Listeners on its onEnter / onExit are dropped with it. Procedures the game replaced (update, draw…) stay replaced.

Inputs

  • scene: The scene

procedure. Source: scene.odin:143

Scene_Destroy :: proc(scene: ^Scene)

Destroys every node of a scene and frees what the scene owns (its lists, physics world and arena); the Scene struct itself is the caller’s. Nodes in the scene get onExit first, as removed nodes do; then every node it made (in it, waiting to be added, or never added) is destroyed, children before their parents, so a destroy hook can still reach its parent.

Inputs

  • scene: The scene

procedure. Source: scene.odin:164

CreateNode :: proc(scene: ^Scene, $T: typeid, addToScene: bool = true) -> ^T

Creates a node of type T in its own memory arena and (by default) adds it to the scene.

T must be Node or a struct that embeds it as its first field (using node: e.Node). The node is freed, along with everything allocated with Node_GetAllocator(node), when it is removed from the scene or the scene is destroyed.

Inputs

  • scene: The scene the node belongs to
  • T: The node type
  • addToScene: Add it to the scene (at the start of the next update). Pass false to add it later with scene->addNode

Returns

  • The new node, zero-initialized apart from its id, scene and event subjects

Example

package mygame
import e "engine:."
import sdl "vendor:sdl3"
Bullet :: struct {
using node: e.Node,
velocity: [2]f32,
}
Fire :: proc(scene: ^e.Scene, from: [2]f32) {
bullet := e.CreateNode(scene, Bullet)
bullet.position = from
bullet.velocity = {0, -600}
bullet.update = Bullet_update
}
Bullet_update :: proc(node: ^e.Node, events: []sdl.Event, deltaTime: f64) {
bullet := cast(^Bullet)node
bullet.position += bullet.velocity * f32(deltaTime)
if bullet.position.y < 0 {
node.scene->removeNode(node.id) // Freed at the start of the next update
}
}

procedure. Source: scene.odin:243

Scene_AddNode :: proc(scene: ^Scene, node: ^Node)

Adds a node to its scene at the start of the next update (it gets onEnter then). For nodes made with CreateNode(..., addToScene = false); adding a node twice, or one of another scene, does nothing.

Inputs

  • scene: The node’s scene
  • node: The node

procedure. Source: scene.odin:273

Scene_RemoveNode :: proc(scene: ^Scene, nodeId: EntityId)

Removes a node, with its children, at the start of the next update: they get onExit, then their destroy hooks, then their memory is freed. Pointers to them are invalid after that. Removing a node twice, or one already gone, does nothing.

Inputs

  • scene: The node’s scene
  • nodeId: The node’s id

procedure. Source: scene.odin:293

Scene_GetNode :: proc(scene: ^Scene, nodeId: EntityId) -> ^Node

A node of the scene by its id: in the scene, waiting to be added, or made without being added. Also scene->getNode(id).

Inputs

  • scene: The scene
  • nodeId: The node’s id

Returns

  • The node, or nil (never made here, or already removed)

procedure. Source: scene.odin:308

Scene_ProcessQueues :: proc(scene: ^Scene)

Applies queued removals, then queued additions (onEnter for each, in the order they were added). Scene_Update does it first; editors and tests call it to apply changes without updating.

Inputs

  • scene: The scene

procedure. Source: scene.odin:320

Scene_Update :: proc(scene: ^Scene, events: []sdl.Event, deltaTime: f64)

Updates the scene for one frame. The game calls it (as scene->update) for its current scene.

In order: expired debug shapes go, the add and remove queues are applied, new physics nodes get their bodies, scripts get ready, physics steps (unless paused), every node that may process updates (its update, then its script’s), tweens advance, then the camera’s pan and bounds are applied.

Inputs

  • scene: The scene
  • events: This frame’s SDL events
  • deltaTime: Seconds since the last frame (after the game’s time scale and hitstop)

procedure. Source: scene.odin:405

Scene_SetPaused :: proc(scene: ^Scene, paused: bool)

Pauses or resumes a scene: while paused, physics doesn’t step and only nodes that may process then update (process mode WhenPaused or Always, or inheriting one); drawing goes on.

Inputs

  • scene: The scene
  • paused: true to pause, false to resume

Example

e.Scene_SetPaused(scene, !scene.paused) // the pause key

procedure. Source: scene.odin:461

Scene_GetGroup :: proc(scene: ^Scene, group: string) -> []^Node

The scene’s nodes in a group, in the order they were added (nodes waiting to be added included).

Inputs

  • scene: The scene
  • group: The group’s name

Returns

  • The nodes (temp allocated; empty if none)

Example

for enemy in e.Scene_GetGroup(scene, "enemies") do enemy.visible = false

procedure. Source: scene.odin:479

Scene_Draw :: proc(scene: ^Scene, renderer: ^sdl.Renderer)

Draws the scene: layer by layer, and within a layer in tree order (a parent before its children, siblings in the order they were added). Each node draws in its local space; the scene sets the draw transform to the camera view combined with the node’s world transform (only the position, for .Position_Only nodes), and the draw tint to its inherited tint. Hidden nodes and their children are skipped. The game calls it (as scene->draw) for its current scene.

Inputs

  • scene: The scene
  • renderer: The renderer to draw with

procedure. Source: scene.odin:498

Scene_DrawWithView :: proc(scene: ^Scene, renderer: ^sdl.Renderer, view: Transform2D, screenLayersUseView := false, positionOnlyScale: f32 = 1)

Draws the scene through a given view transform (world to screen) instead of its camera. Editors use it to show a scene with their own pan and zoom.

Inputs

  • scene: The scene
  • renderer: The renderer to draw with
  • view: World-to-screen transform for world layers
  • screenLayersUseView: Also draw scene.screenLayers through view (an editor shows HUD nodes where they sit relative to the game area); otherwise they’re drawn untransformed
  • positionOnlyScale: Size multiplier for .Position_Only nodes (labels, menus). The game uses 1; an editor passes its zoom so UI keeps its size relative to the game area

procedure. Source: scene.odin:516

Node_IsVisibleInTree :: proc(node: ^Node) -> bool

Whether a node and all of its ancestors are visible.

Inputs

  • node: The node

Returns

  • false if the node or any ancestor has visible off

procedure. Source: scene.odin:593

Scene_FindNode :: proc(scene: ^Scene, path: string) -> ^Node

Finds a node by its name, or by a path of names from a top-level node: “World/Ship/Flame”.

Inputs

  • scene: The scene
  • path: A name, or names separated by “/”

Returns

  • The first match (nodes waiting to be added included), or nil. A bare name matches any node, in the order they were added; a path starts at the first top-level node with the first name

Example

if flame := e.Scene_FindNode(scene, "Ship/Flame"); flame != nil do flame.visible = false

procedure. Source: scene.odin:703

Node_FindPath :: proc(from: ^Node, path: string) -> ^Node

Finds a node by a path from another, like a file path: child names separated by “/” (“Gun”, “Arm/Hand”), “..” for the parent and “.” for the node itself (“../Camera”: a sibling). From a top-level node “..” leads to the scene’s top level. A path starting with “/” starts at the scene’s top level instead (“/World/Ship”). Script node fields use it.

Inputs

  • from: The node the path starts at
  • path: The path (“” gives nil)

Returns

  • The node, or nil when no node is there

Example

// Given: self: ^e.Script
camera := e.Node_FindPath(self.node, "../Camera")

procedure. Source: scene.odin:733

Node_FindChild :: proc(node: ^Node, path: string) -> ^Node

Finds a descendant of a node by a path of child names: “Ship/Flame”. At each level the first child with the name is followed.

Inputs

  • node: The node to start from
  • path: Child names separated by “/”. “” returns node

Returns

  • The node, or nil

procedure. Source: scene.odin:788

Node_CanProcess :: proc(node: ^Node) -> bool

Whether a node updates now: its process mode (resolving Inherit through its parents; Pausable at the top) against whether its scene is paused. The scene checks it before each update.

Inputs

  • node: The node

Returns

  • true if the node updates this frame

Example

// Given: pauseMenu: ^e.Node
pauseMenu.processMode = .WhenPaused
e.Scene_SetPaused(scene, true) // gameplay stops, the menu runs

procedure. Source: node.odin:203

Node_AddToGroup :: proc(node: ^Node, group: string)

Puts a node in a group (once): groups name sets of nodes to find or act on together, like “enemies” or “pickups”. Find them with Scene_GetGroup.

Inputs

  • node: The node
  • group: The group’s name (copied into the node’s arena)

Example

e.Node_AddToGroup(node, "enemies")
for enemy in e.Scene_GetGroup(scene, "enemies") do enemy.tint = {255, 80, 80, 255}

procedure. Source: node.odin:233

Node_RemoveFromGroup :: proc(node: ^Node, group: string)

Takes a node out of a group (nothing if it isn’t in it).

Inputs

  • node: The node
  • group: The group’s name

procedure. Source: node.odin:245

Node_IsInGroup :: proc(node: ^Node, group: string) -> bool

Whether a node is in a group.

Inputs

  • node: The node
  • group: The group’s name

Returns

  • true if it’s in the group

procedure. Source: node.odin:262

Node_GetAllocator :: proc(node: ^Node) -> mem.Allocator

Allocator backed by the node’s arena. Everything allocated with it is freed when the node is destroyed, so use it for data the node owns: event subjects, animation players, animations, copies of strings.

Inputs

Returns

  • The node’s arena allocator

Example

package mygame
import e "engine:."
Enemy :: struct {
using node: e.Node,
anim: e.AnimationPlayer,
onHit: e.EventSubject,
}
CreateEnemy :: proc(scene: ^e.Scene) -> ^Enemy {
enemy := e.CreateNode(scene, Enemy)
allocator := e.Node_GetAllocator(enemy)
e.AnimationPlayer_Init(&enemy.anim, allocator)
e.EventSubject_Init(&enemy.onHit, allocator)
return enemy
}

procedure. Source: node.odin:297

Node_Destroy :: proc(node: ^Node)

Frees a node and everything in its arena, after calling its destroy callback.

The scene does this for you: for removed nodes at the start of the next update (after onExit), and for all of its nodes when the scene is destroyed. Any pointer to the node is invalid afterwards.

Inputs

procedure. Source: node.odin:311

Node_Is :: proc(node: ^Node, $T: typeid) -> bool

Whether a node was created as type T (CreateNode(scene, T)), so it can be cast to ^T.

Inputs

  • node: The node (nil returns false)
  • T: The node type

Returns

  • true if the node’s type is exactly T

Example

if e.Node_Is(node, e.DynamicBody) do e.DynamicBody_ApplyImpulse(cast(^e.DynamicBody)node, {0, -300})

procedure. Source: node.odin:369

Node_GetLocalBounds :: proc(node: ^Node) -> (sdl.FRect, bool)

The rectangle a node draws, in its local space (before its transform).

Inputs

  • node: The node

Returns

  • The bounds, and false for nodes that draw nothing measurable (no getBounds)

procedure. Source: node.odin:382

Transform2D_Make :: proc(position: [2]f32, rotation: f32, scale: [2]f32) -> Transform2D

Builds a transform from a position, rotation and scale. Scale is applied first, then rotation, then the position.

Inputs

  • position: Where the local origin goes
  • rotation: Degrees, clockwise (screen Y points down)
  • scale: Size multiplier per axis

Returns

  • The transform

procedure. Source: transform.odin:42

Transform2D_Multiply :: proc(a, b: Transform2D) -> Transform2D

Combines two transforms: the result applies b first, then a. A child’s world transform is Transform2D_Multiply(parentWorld, childLocal).

Inputs

  • a: The transform applied second (the parent’s)
  • b: The transform applied first (the child’s)

Returns

  • The combined transform

Example

e.Transform2D
world := e.Transform2D_Multiply(parentWorld, e.Node_GetLocalTransform(node))
_ = world

procedure. Source: transform.odin:70

Transform2D_Apply :: proc(t: Transform2D, p: [2]f32) -> [2]f32

Transforms a point.

Inputs

  • t: The transform
  • p: The point

Returns

  • t.x * p.x + t.y * p.y + t.origin

procedure. Source: transform.odin:88

Transform2D_ApplyVector :: proc(t: Transform2D, v: [2]f32) -> [2]f32

Transforms a direction or size: like Transform2D_Apply, without the translation.

Inputs

  • t: The transform
  • v: The direction or size

Returns

  • t.x * v.x + t.y * v.y

procedure. Source: transform.odin:102

Transform2D_Inverse :: proc(t: Transform2D) -> Transform2D

The transform that undoes t. A transform that collapses space (zero scale) has no inverse; identity is returned for it.

Inputs

  • t: The transform

Returns

procedure. Source: transform.odin:116

Transform2D_Rotation :: proc(t: Transform2D) -> f32

Rotation of the transform’s X axis.

Inputs

  • t: The transform

Returns

  • Degrees, clockwise, from -180 to 180

procedure. Source: transform.odin:137

Transform2D_Scale :: proc(t: Transform2D) -> [2]f32

Length of each axis: the scale, ignoring any mirroring.

Inputs

  • t: The transform

Returns

  • The X and Y scale, both positive

procedure. Source: transform.odin:150

Transform2D_UniformScale :: proc(t: Transform2D) -> f32

How much the transform scales areas, as a single length factor (the square root of the area scale). Used for line widths and text.

Inputs

  • t: The transform

Returns

  • The length factor (1 for no scaling)

procedure. Source: transform.odin:164

Transform2D_IsAxisAligned :: proc(t: Transform2D) -> bool

Whether the transform keeps axis-aligned rectangles axis-aligned (no rotation or skew; mirroring is fine).

Inputs

  • t: The transform

Returns

  • true if its axes are along X and Y

procedure. Source: transform.odin:178

Node_GetLocalTransform :: proc(node: ^Node) -> Transform2D

The node’s transform relative to its parent, built from position, rotation and scale.

Inputs

  • node: The node

Returns

  • The local transform

procedure. Source: transform.odin:194

Node_GetWorldTransform :: proc(node: ^Node) -> Transform2D

The node’s transform in world coordinates: its local transform combined with all of its ancestors’. Computed from the current values, so it’s correct even right after something moved during an update. The camera isn’t included.

Inputs

  • node: The node

Returns

  • The world transform

procedure. Source: transform.odin:209

Node_GetWorldPosition :: proc(node: ^Node) -> [2]f32

The node’s origin in world coordinates.

Inputs

  • node: The node

Returns

  • The world position

Example

// Given: target: ^e.Node
toTarget := e.Node_GetWorldPosition(target) - e.Node_GetWorldPosition(node)
node.rotation = math.to_degrees(math.atan2(toTarget.y, toTarget.x))

procedure. Source: transform.odin:231

Node_SetWorldPosition :: proc(node: ^Node, world: [2]f32)

Moves the node so its origin is at a world position, whatever its parent’s transform is.

Inputs

  • node: The node
  • world: Target position in world coordinates

Example

// Given: target: ^e.Node
e.Node_SetWorldPosition(node, e.Node_GetWorldPosition(target) + {0, -32})

procedure. Source: transform.odin:246

Node_GetScreenPosition :: proc(node: ^Node) -> [2]f32

Where the node’s origin appears on screen (game resolution coordinates): through the scene’s camera, unless the node’s layer is in scene.screenLayers. Use it to hit-test UI nodes against mouse events.

Inputs

  • node: The node

Returns

  • The screen position, in game coordinates

procedure. Source: transform.odin:266

Node_SetParent :: proc(child: ^Node, parent: ^Node, keepWorld := false) -> bool

Makes child a child of parent, or a top-level node when parent is nil. Both must be in the same scene.

Inputs

  • child: The node to move
  • parent: New parent, or nil
  • keepWorld: Keep the node where it is in the world (its local position, rotation and scale are recalculated). Otherwise its local values are kept, so it moves with the new parent. Skew from a non-uniformly scaled parent can’t be kept

Returns

  • false if parent is child or one of its descendants (that would make a loop), or is in another scene (logged)

Example

// Given: hand: ^e.Node, sword: ^e.Node
e.Node_SetParent(sword, hand, keepWorld = true) // picked up where it lay
e.Node_SetParent(sword, nil, keepWorld = true) // dropped where it is

procedure. Source: transform.odin:293

Node_AddChild :: proc(parent, child: ^Node) -> bool

Adds a child to a node. Same as Node_SetParent(child, parent): the child keeps its local values.

Inputs

  • parent: The new parent
  • child: The node to add

Returns

  • false if that would make a loop, or the nodes are in different scenes (see Node_SetParent)

Example

// Given: ship: ^e.Node
flame := e.CreateNode(scene, e.Sprite)
e.Node_AddChild(ship, flame)
flame.position = {0, 40} // 40 below the ship, wherever the ship goes

procedure. Source: transform.odin:338

GetDrawTransform :: proc() -> Transform2D

The transform the Draw* helpers currently apply: from the drawing node’s local space to the screen. Identity outside of scene drawing. Use it in custom drawing code that calls SDL directly.

Example

MyNode_draw :: proc(node: ^e.Node, renderer: ^sdl.Renderer) {
p := e.Transform2D_Apply(e.GetDrawTransform(), {0, 0})
sdl.RenderPoint(renderer, p.x, p.y)
}

procedure. Source: transform.odin:391

SetDrawState :: proc(transform: Transform2D, tint := sdl.Color{255, 255, 255, 255}, blend := BlendMode.Blend)

Sets the transform, tint and blend mode the Draw* helpers apply (on the calling thread). Scenes do this for each node; call it only when drawing outside a scene, and restore it afterwards.

Inputs

  • transform: Local space to screen
  • tint: Multiplies every color drawn (white: unchanged)
  • blend: The blend mode DrawTexture uses (shapes use the renderer’s draw blend mode)

Example

e.SetDrawState(e.Transform2D_Make({200, 100}, 45, {2, 2}))
e.DrawCircle(renderer, {0, 0}, 10, {255, 255, 255, 255})
e.SetDrawState(e.TRANSFORM2D_IDENTITY)

procedure. Source: transform.odin:409

SDLBlendMode :: proc(blend: BlendMode) -> sdl.BlendMode

The SDL blend mode of a BlendMode.

Inputs

  • blend: The engine’s blend mode

Returns

  • The matching SDL blend mode

procedure. Source: transform.odin:424

GetDrawBlend :: proc() -> sdl.BlendMode

The blend mode of the node drawing now, for drawing code that sets its own (geometry)

procedure. Source: transform.odin:436

NewEntityId :: proc() -> EntityId

A new random id (a version 4 UUID) from Odin’s fast per-thread generator. Ids needn’t be secret, and the OS’s cryptographic source (what uuid.generate_v4 insists on) cost a system call per node and per listener.

Returns

  • The new id

procedure. Source: event.odin:31

EventSubject_Init :: proc(subject: ^EventSubject, allocator := context.allocator)

Prepares an event subject for use.

Inputs

  • subject: The subject to initialize
  • allocator: Allocator for the listener list. Use the owner’s allocator (e.g. Node_GetAllocator(node)) so the list is freed along with the owner

procedure. Source: event.odin:84

Event_Subscribe :: proc(subject: ^EventSubject, callback: proc(event: ^EventSubject, source: rawptr, data: EventData, userData: rawptr), userData: rawptr) -> ListenerHandle

Adds a listener to a subject. Call it as subject->subscribe(callback, userData).

A listener added while a notification runs is first called by the next notification.

Inputs

  • subject: The subject to listen to
  • callback: Called on each notification with the subject, the source and data passed to Event_Notify, and userData
  • userData: Any pointer, passed back to callback (nil when not needed)

Returns

Example

// Given: subject: ^e.EventSubject
handle := subject->subscribe(proc(event: ^e.EventSubject, source: rawptr, data: e.EventData, userData: rawptr) {
if score, ok := data.(int); ok do log.infof("score: %d", score)
}, nil)
subject->notify(nil, 42)
subject->unsubscribe(handle)

procedure. Source: event.odin:113

Event_Unsubscribe :: proc(subject: ^EventSubject, handle: ListenerHandle)

Removes a listener from a subject. Call it as subject->unsubscribe(handle).

Safe during a notification: the listener isn’t called again, and the list is compacted when the notification ends. The other listeners keep their order. Logs a warning if the handle isn’t subscribed.

Inputs

procedure. Source: event.odin:135

Event_Notify :: proc(subject: ^EventSubject, source: rawptr, data: EventData)

Calls every listener, in the order they subscribed. A listener may subscribe or unsubscribe (itself or others) while being called: ones that subscribe then are first called by the next notification; ones unsubscribed then aren’t called again, this time included. Call it as subject->notify(source, data).

Inputs

  • subject: The subject
  • source: Who sent it (a node, a scene…), passed to each listener; may be nil
  • data: A value passed to each listener; nil for none

procedure. Source: event.odin:162

Event_Destroy :: proc(subject: ^EventSubject)

Frees the subject’s listener list. Not needed when the list was made with an arena allocator that’s freed anyway (a node’s or scene’s).

Inputs

  • subject: The subject

procedure. Source: event.odin:190

NodeType_Events :: proc(type: typeid, allocator := context.temp_allocator) -> []NodeEventInfo

The events of a node type: its tagged EventSubject fields, those of the types it embeds (Node’s onEnter and onExit last) and of its struct fields.

Inputs

Returns

  • The events, the type’s own first

procedure. Source: node_events.odin:58

SceneNodeKind_Events :: proc(kind: SceneNodeKind, allocator := context.temp_allocator) -> []NodeEventInfo

The events nodes of a scene file’s kind offer (a Component instance: a Node’s; its root’s type decides the rest).

Inputs

  • kind: The kind
  • allocator: For the list

Returns

procedure. Source: node_events.odin:92

Node_FindEvent :: proc(node: ^Node, name: string) -> ^EventSubject

A node’s event by name.

Inputs

  • node: The node
  • name: The event (NodeEventInfo.name)

Returns

  • Its subject, or nil when the node’s type has no such event

Example

// Given: emitter: ^e.Node
if finished := e.Node_FindEvent(emitter, "onFinished"); finished != nil do log.info("it can finish")

procedure. Source: node_events.odin:110

Script_FindEvent :: proc(data: rawptr, info: ^ScriptInfo, name: string) -> ^EventSubject

A script’s event by name: one of its struct’s EventSubject fields tagged event:"...".

Inputs

  • data: The script struct (a node’s script, or a global)
  • info: Its script
  • name: The event, “script.<field>”

Returns

  • Its subject, or nil

procedure. Source: node_events.odin:136

Node_AddListener :: proc(node: ^Node, listener: NodeListenerData)

Adds a listener to a node: before the scene’s scripts next get ready (for a node built from a scene file: before they first do), the target’s script’s handler is subscribed to the event. Scene files’ <listener> elements come here.

Inputs

  • node: The node whose event it is
  • listener: The event, the listening node (a path from node) and the handler’s name (copied)

procedure. Source: node_events.odin:172

Script_Listener :: proc "contextless" ($T: typeid, name: string, handler: proc(self: ^T, source: ^Node, data: EventData))

Registers a script’s event handler by name, for scene files’ listeners (<listener handler=...>) to call. Like Script_Register, call it from an @(init) proc "contextless"; the editor writes these when you attach a listener in the inspector.

Inputs

  • T: The script struct
  • name: The handler’s name, as scene files give it
  • handler: Called with the listening script, the node whose event it is, and the event’s data

Example

Hero :: struct {
using script: e.Script,
coins: int,
}
Hero_onCoinCollected :: proc(self: ^Hero, source: ^e.Node, data: e.EventData) {
self.coins += 1
}
register_Hero_onCoinCollected :: proc "contextless" () {
e.Script_Listener(Hero, "onCoinCollected", Hero_onCoinCollected)
}

procedure. Source: node_events.odin:207

Script_FindListener :: proc(type: typeid, name: string) -> ScriptListenerProc

A script’s handler registered with Script_Listener.

Inputs

  • type: The script struct’s type
  • name: The handler’s name

Returns

  • The handler, or nil

procedure. Source: node_events.odin:224

TimerNode_Init :: proc(timer: ^Timer, duration: time.Duration, auto_start: bool = false, loop: bool = false, allocator := context.allocator)

Prepares a timer: its events and procedures. Its elapsed time starts at zero.

Inputs

  • timer: The timer
  • duration: How long until it expires
  • auto_start: Start running now
  • loop: Start over each time it expires
  • allocator: For the event listener lists; inside a node, pass Node_GetAllocator(node)

procedure. Source: timer.odin:68

TimerNode_Create :: proc(durationSeconds: f32, auto_start: bool = false, loop: bool = false, allocator := context.allocator) -> ^Timer

Makes and prepares a timer.

Inputs

  • durationSeconds: How long until it expires
  • auto_start: Start running now
  • loop: Start over each time it expires
  • allocator: For the timer and its listener lists; inside a node, pass Node_GetAllocator(node)

Returns

  • The timer (freed with the allocator)

procedure. Source: timer.odin:92

TimerNode_Start :: proc(timer: ^Timer)

Starts (or restarts) a timer from zero. Also timer->start().

Inputs

  • timer: The timer

procedure. Source: timer.odin:104

TimerNode_Stop :: proc(timer: ^Timer)

Stops a timer where it is (timer.running = true resumes it).

Inputs

  • timer: The timer

procedure. Source: timer.odin:115

TimerNode_SetDuration :: proc(timer: ^Timer, durationSec: f32)

Sets how long a timer runs before it expires (its elapsed time is kept). Also timer->setDuration(seconds).

Inputs

  • timer: The timer
  • durationSec: The new duration, in seconds

procedure. Source: timer.odin:152

Ticker_Init :: proc(ticker: ^Ticker, auto_start: bool = true, allocator := context.allocator)

Prepares a ticker: its procedures and onTick, at 1 tick per second.

Inputs

  • ticker: The ticker
  • auto_start: Start it now (counter at 0)
  • allocator: Holds the event listener list. Inside a node, pass Node_GetAllocator(node)

procedure. Source: ticker.odin:86

Ticker_Start :: proc(ticker: ^Ticker, resetCounter: bool = true)

Starts the ticker: the time to the next tick starts from now. Also ticker->start(resetCounter).

Inputs

  • ticker: The ticker
  • resetCounter: Set the tick count back to 0

procedure. Source: ticker.odin:109

Ticker_Stop :: proc(ticker: ^Ticker)

Stops the ticker. Also ticker->stop().

Inputs

  • ticker: The ticker

procedure. Source: ticker.odin:121

Ticker_TogglePause :: proc(ticker: ^Ticker)

Pauses a running ticker, or resumes a stopped one. Also ticker->togglePause().

Inputs

  • ticker: The ticker

procedure. Source: ticker.odin:131

Ticker_Pause :: proc(ticker: ^Ticker)

Pauses the ticker (the same as Ticker_Stop). Also ticker->pause().

Inputs

  • ticker: The ticker

procedure. Source: ticker.odin:142

Ticker_Resume :: proc(ticker: ^Ticker)

Resumes the ticker where it was paused: the time it was paused doesn’t count toward the next tick. Also ticker->resume().

Inputs

  • ticker: The ticker

procedure. Source: ticker.odin:152

Ticker_SetFPS :: proc(ticker: ^Ticker, framesPerSecond: f32)

Sets the tick rate. Also ticker->setFPS(rate).

Inputs

  • ticker: The ticker
  • framesPerSecond: Ticks per second, clamped to 0 to 120 (0 never ticks)

procedure. Source: ticker.odin:164

Ticker_Reset :: proc(ticker: ^Ticker)

Sets the tick count and the time since the last tick back to 0. Also ticker->reset().

Inputs

  • ticker: The ticker

procedure. Source: ticker.odin:176

NodeType_Register :: proc "contextless" ($T: typeid, base: SceneNodeKind, hooks: ScriptHooks(T), loc := #caller_location)

Registers a custom node type: call it from an @(init) proc "contextless" in the game’s scripts.

Inputs

  • T: The struct; its first field is the base kind’s type (using sprite: e.Sprite)
  • base: The built-in kind it extends (.Sprite)
  • hooks: Its procedures, as a script’s (self: the node)
  • loc: Where it’s called from (the editor opens the file)

procedure. Source: custom_nodes.odin:67

NodeType_Find :: proc(name: string) -> ^NodeTypeInfo

The custom node type registered under a name, or nil

procedure. Source: custom_nodes.odin:105

NodeType_All :: proc() -> []^NodeTypeInfo

Every registered custom node type, by name (temp allocated)

procedure. Source: custom_nodes.odin:110

MAX_EVENTS :: 1000

Most SDL events Application_Frame collects in one frame; the rest wait for the next frame

constant. Source: app.odin:11

DEV_KEY_PHYSICS_DEBUG :: sdl.Scancode.F9

With Game.devKeys: shows and hides the physics shapes while the game runs

constant. Source: game.odin:28

DEV_KEY_DEBUG_OVERLAY :: sdl.Scancode.F10

With Game.devKeys: shows and hides the debug overlay (debug.odin)

constant. Source: game.odin:30

NODE_ARENA_BLOCK_SIZE :: 4 * mem.Kilobyte

Initial (and minimum) size of each block of a node’s arena. Small, because most nodes only hold a few event listener lists; the arena adds blocks if a node needs more

constant. Source: node.odin:55

TRANSFORM2D_IDENTITY :: Transform2D{x = {1, 0}, y = {0, 1}}

The transform that changes nothing: axes {1, 0} and {0, 1}, origin {0, 0}

constant. Source: transform.odin:28

SCRIPT_EVENT_PREFIX :: "script."

Script events are named with this before their field (“script.onCoinCollected”)

constant. Source: node_events.odin:123