Scripting
defineBehavior is the authoring sugar for the gameplay loop. One call gives you two
things at once:
- A component that holds this behavior’s per-entity
state— attachable to any entity and tunable in the editor’s Details panel. - An auto-registered system that drives a
start/update/destroylifecycle for every entity carrying that component.
There is no second scripting runtime: a behavior desugars entirely onto Estella’s existing ECS — the same components, systems, and scheduling you already use — so it stays fast and fully inspectable.
Your first behavior
Section titled “Your first behavior”defineBehavior(name, def) returns the backing component, so you can spawn or insert
it like any other. The state object becomes the component’s per-entity data:
import { defineBehavior, Transform } from 'esengine';
export const Patrol = defineBehavior('Patrol', { state: { speed: 60 }, // per-entity data, editable in the Details panel update(ctx, dt) { ctx.get(Transform).position.x += ctx.self.speed * dt; },});That single call registers the update system. To put the behavior on an entity, insert
its component — optionally overriding the starting state:
import { defineSystem, Commands, Transform } from 'esengine';
const spawn = defineSystem([Commands()], (cmds) => { cmds.spawn() .insert(Transform, { position: { x: 0, y: 0, z: 0 } }) .insert(Patrol, { speed: 120 }); // override the default per instance});The lifecycle
Section titled “The lifecycle”Each hook is optional; define only the ones you need.
| Hook | When it runs |
|---|---|
start(ctx) |
Once, the frame the behavior’s component first appears on an entity. |
update(ctx, dt) |
Every frame, for each entity carrying the behavior. dt is the frame delta in seconds (same as ctx.time.delta). |
destroy(ctx) |
Once when the component is removed or the entity is despawned. |
export const Enemy = defineBehavior('Enemy', { state: { hp: 100 }, start(ctx) { // one-time setup: seed state, cache lookups, spawn a health bar… }, update(ctx, dt) { if (ctx.self.hp <= 0) ctx.commands.despawn(ctx.entity); }, destroy(ctx) { // teardown: runs whether the entity was killed or the component was detached },});The context
Section titled “The context”Every hook receives a BehaviorContext — the handle to the entity it runs for and to
the world around it:
| Member | Type | Description |
|---|---|---|
ctx.self |
S |
This behavior’s own state. Mutate it freely — changes persist. |
ctx.entity |
Entity |
The entity this instance is attached to. |
ctx.time |
TimeData |
Frame timing: delta, elapsed, frameCount, fixedDelta. |
ctx.input |
InputState |
Keyboard / mouse / touch / gamepad (see Input). |
ctx.commands |
Commands |
Deferred spawn / despawn / insert — safe to call mid-update. |
ctx.world |
World |
Full world access for cross-entity reads and writes. |
ctx.get(Comp) |
ComponentData |
Read another component on this entity. |
ctx.set(Comp, data) |
void |
Write a component on this entity. |
ctx.has(Comp) |
boolean |
Whether this entity has Comp. |
import { defineBehavior, Transform } from 'esengine';
export const PlayerController = defineBehavior('PlayerController', { state: { speed: 200, facing: 1 }, update(ctx, dt) { const move = (ctx.input.isKeyDown('KeyD') ? 1 : 0) - (ctx.input.isKeyDown('KeyA') ? 1 : 0); if (move !== 0) ctx.self.facing = move; // mutating self persists ctx.get(Transform).position.x += move * ctx.self.speed * dt; },});Editable state & metadata
Section titled “Editable state & metadata”The state object defines the component’s fields and their defaults. Pass metadata
to control how those fields present in the editor — ranges, steps, units, enums — the
same field metadata any component uses:
export const Turret = defineBehavior('Turret', { state: { range: 300, fireRate: 2, target: 0 }, metadata: { fields: { range: { min: 0, unit: 'px', category: 'Targeting' }, fireRate: { min: 0, unit: '/s', category: 'Targeting', tooltip: 'Shots per second.' }, }, }, update(ctx, dt) { /* … */ },});Because the state is a real serialized component, per-entity overrides are saved with the scene and edited live in the Details panel.
Choosing a schedule
Section titled “Choosing a schedule”By default the lifecycle system runs in Schedule.Update, once per frame. Pass a
different schedule to run elsewhere in the frame — for example
Schedule.FixedUpdate to advance in lockstep with physics:
import { defineBehavior, Schedule } from 'esengine';
export const Thruster = defineBehavior('Thruster', { schedule: Schedule.FixedUpdate, state: { force: 500 }, update(ctx) { // fixed cadence — read ctx.time.fixedDelta for the fixed step },});Available phases include Startup, PreUpdate, Update, PostUpdate, and the fixed
trio FixedPreUpdate / FixedUpdate / FixedPostUpdate.
Timers
Section titled “Timers”For “in 2 seconds…” / “every 0.5 s…” logic, the built-in timer resource beats
hand-rolled elapsed-time bookkeeping. It ticks with the engine loop (so it pauses
with the game, unlike setTimeout):
import { defineSystem, Res, TimerRes } from 'esengine';
const arm = defineSystem([Res(TimerRes)], (timers) => { timers.delay(2, () => explode()); // once, in 2 s const h = timers.interval(0.5, (t) => spawnWave()); // repeating timers.interval(1, (t) => tick(), 3); // exactly 3 times h.pause(); h.resume(); h.cancel(); h.reset(); // handle controls});| Member | Description |
|---|---|
delay(seconds, cb) |
Run once after seconds; returns a TimerHandle. |
interval(seconds, cb, maxRepeat?) |
Run every seconds; maxRepeat 0 = forever. |
handle pause() / resume() / cancel() / reset() |
Control one timer (chainable). |
handle isActive / elapsed / repeatCount |
Inspect it. |
cancelAll() · activeCount |
Manage the whole set. |
timeScale |
Slow / speed every timer at once (0 freezes). |
Timers advance in play mode only — in the editor’s edit mode they hold, like the rest of game time.
Constant motion: the Velocity component
Section titled “Constant motion: the Velocity component”For “just keep moving” motion — projectiles, drifting debris, spinners — attach the
builtin Velocity component instead of writing a movement system. The engine
integrates it into Transform every update in play mode:
import { Velocity } from 'esengine';
world.spawn() .insert(Transform, { position: { x: 0, y: 0, z: 0 } }) .insert(Sprite, { texture: bulletTex }) .insert(Velocity, { linear: { x: 240, y: 0, z: 0 }, // units per second angular: { x: 0, y: 0, z: Math.PI }, // radians per second (z = 2D spin) });Entities with a RigidBody are skipped — the physics solver owns their transform,
so the two never fight over one entity. Velocity is also replication-aware: its
fields are replicated, so networked clients can dead-reckon between snapshots.
How it fits together
Section titled “How it fits together”A behavior is not a special case — it compiles to exactly one defineComponent plus
one defineSystem. Reach for defineBehavior when your logic is naturally
per-entity (a patrol, a projectile, a pickup); reach for a plain
system when you’re processing a whole query at once or
coordinating across entities.
The hook signature — entity, world, commands, plus self — is also the same
programming model used by the Gameplay AI layer’s actions and
behavior-tree leaves, so moving logic between a behavior and an AI action is
straightforward.
Hot reload
Section titled “Hot reload”While the editor plays, saving your code triggers a state-preserving hot swap when it can: if your components’ shapes are unchanged and no system was added, removed, or renamed, the live world is kept and only your function bodies are replaced. Otherwise the editor falls back to a full reload. What that asks of your code:
- Component and behavior names are identity. Renaming one is a structural change — expect a full reload and fresh state for that data.
- On any reload, per-entity
startruns again for live entities — matching Estella’s fast-restart semantics, so treatstartas idempotent setup.
The mechanism (probe context, schema fingerprint, App.hotSwapSystems) is
documented in App Setup & Lifecycle.
See also
Section titled “See also”- Systems — the full query vocabulary, filters, and schedules.
- ECS Architecture — entities, components, and how systems iterate them.
- Input — read keyboard, pointer, and gestures from a system.