---
mvp: no
subsystem: animation
---

# Animation

GameMaker 5.3a has no separate animation system — animation IS sprite playback. Every Sprite resource has N frames; the engine advances `image_index` by `image_speed` per step and wraps. There is no skeleton, no skinning, no tween system. BNO compensates by **swapping `sprite_index`** at state transitions: the player object owns 8+ sprite resources (Hexport, HexportIn, HexportOut, HexportMask, NaviStand, NaviWalk, BattleIdle, ...) and the Step + Other events choose which one is current.

The player Create event sets the initial frame rate:

```gml
// auto-transcompiled — see *.dnd.json for canonical truth
image_speed = 0.8;

//global.p_created[pid] = 1;
```

`image_speed = 0.8` means "advance the sprite frame by 0.8 every step" — at 30 fps that's 24 frames per second of sprite playback, fast enough for a smooth walk. Idle states use lower values; jump/teleport states crank it to 1.0 for snap.

The sprite-index-swap idiom appears all over `extracted/client-5-8/objects/0042-player/events/Step.gml`:

```gml
if(sprite_index != Hexport && sprite_index != HexportIn && sprite_index != HexportOut)
{ depth_set(0,43); ... }
else if(sprite_index == HexportIn || sprite_index == HexportOut)
{ ... }
else if(sprite_index == Hexport)
{ ... }
```

Each branch is a state. Hexport (in transit), HexportIn (entering hexport-mode animation), HexportOut (exiting). The transitions happen by direct `sprite_index = HexportOut` writes, often coupled with `image_index = 0` to restart the new animation from frame 0.

**Alarm events** are GM 5.x's only timer mechanism. `alarm[N] = K` sets alarm-N to fire in K steps; the corresponding Alarm-N event runs once and the alarm clears. BNO uses these for animation-step timeouts ("hexport-out animation completes in 24 steps; trigger Alarm-3 to actually leave the room").

The player object's Alarm.gml (event slot 0) is non-trivial — it owns the multi-frame teleport / hexport finalisation. Other.gml-7 (event slot 7 = "Game End" in DnD numbering) cleans up sprite state on disconnect.

The pcode_* scripts (`0290 pcode_lrloop`, `0291 pcode_udloop`, `0292 pcode_slookat`, `0293 pcode_wander`, `0319 pcode_facedir`, `0355 pcode_mover`, `0356 pcode_spin`) are the BNO "P-code" mini-VM for scripted NPC animations: each is a small state machine that updates `direction` and `sprite_index` based on a per-NPC behaviour code stored in `global.npc_pcode[i]`. P-code 0 is "wander", 1 is "look at player", 2 is "spin in place", etc. The mini-VM is called each step from the NPC's Step event.

## Key idioms

- **`sprite_index = NewSprite`** swaps the entire sprite resource. Combined with `image_index = 0` to restart. No tween, no crossfade.
- **`image_speed`** is a per-step delta, not a duration. To pause an animation, set `image_speed = 0`.
- **`alarm[N] = K`** is the only built-in timer. BNO uses alarms 0-3 on the player heavily.
- **Direction → sprite mapping** lives inside per-state branches. There is no general-purpose `setSpriteForDirection(dir)` helper; each state encodes its own directional sprite picks.
- **P-code mini-VM for NPCs.** A handful of scripts implement `lrloop`, `udloop`, `slookat`, `wander`, `mover`, `spin`, `facedir`. These are a poor man's behaviour tree.

## Scripts referenced in this subsystem

<!-- AUTOGEN:scripts:start -->
| Script ID | Name | Lines | Used in objects |
|-----------|------|-------|------------------|
| 0319 | pcode_facedir | 28 | — |
| 0354 | depth_set | 9 | — |
| 0355 | pcode_mover | 48 | — |
| 0356 | pcode_spin | 48 | — |
<!-- AUTOGEN:scripts:end -->

## Objects referenced in this subsystem

<!-- AUTOGEN:objects:start -->
| Object ID | Name | Sprite | Mask | Events |
|-----------|------|--------|------|--------|
<!-- AUTOGEN:objects:end -->

## Engine functions used

<!-- AUTOGEN:gml-functions:start -->
| GML function | Call sites | Sample script | Wiki link |
|--------------|------------|---------------|-----------|
| `image_speed` | 1 | 0272-rq_new | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `image_index` | 0 | — | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `sprite_index` | 17 | 0008-ChtCmdRec | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
<!-- AUTOGEN:gml-functions:end -->

## Rebuild guidance

For the Phase 6 client:

- **Use a state machine.** Encode `PlayerAnimState = 'idle' | 'walking' | 'hexport-in' | 'hexport-out' | 'teleporting' | ...`; each state owns its sprite key, frame rate, and exit conditions. Finite state machine library or hand-rolled — both fine.
- **Phaser AnimationManager** (or Pixi spine-equivalent) instead of frame-by-frame `image_index`. Convert sprite frames to a sprite-sheet at the AST-01 boundary; play with `play('hexport-in')`.
- **Animation duration in ms, NOT steps.** `image_speed = 0.8` at 30 fps is a step-rate hangover; modern animation libraries take duration in ms.
- **NPC behaviours move to data + a runtime VM**, NOT per-NPC GML scripts. A small scripting layer (or just a switch over enum) resolves the P-code idiom cleanly.

## See also

- [collision.md](collision.md) — `mask_index` swap accompanies `sprite_index` swap
- [input.md](input.md) — the input that triggers state transitions
- [decomp/wiki/07-gml-core-functions.md](../../decomp/wiki/07-gml-core-functions.md) — `image_*`, `sprite_*`, `alarm[]` reference
