Actor System
Actor System Overview
GB Studio manages up to 21 actor slots (MAX_ACTORS), of which 12 can be active simultaneously (MAX_ACTORS_ACTIVE). The actor system is implemented in C — the actor_t struct, flag definitions, and linked-list management are all C code. Active actors are rendered, updated, and participate in collisions; inactive actors exist in memory but are dormant.
actors[]— global array of all actor slots.actors[0]is always the player (accessed via thePLAYERmacro).- Active actors are linked on a doubly-linked list headed by
actors_active_head/actors_active_tail. - Inactive actors sit on a singly-linked list headed by
actors_inactive_head.
Actor Data Structure (actor_t)
Each actor slot is an actor_t struct. Key fields:
| Field | Type | Description |
|---|---|---|
pos | upoint16_t | Position in subpixels (12.5 fixed-point: pixel × 32) |
dir | direction_e | Current facing direction (DIR_DOWN, DIR_UP, DIR_LEFT, DIR_RIGHT) |
bounds | rect16_t | Collision bounding box (left, right, top, bottom offsets) |
base_tile | UINT8 | First tile index in VRAM for this actor's sprite sheet |
frame | UINT8 | Current animation frame index |
frame_start | UINT8 | First frame of the current animation range |
frame_end | UINT8 | Last frame of the current animation range |
anim_tick | UINT8 | Animation timer; decrements each frame. 255 = ANIM_PAUSED |
animations[8] | UINT8[8] | Animation state table — maps state index to frame range |
sprite | far_ptr_t | Far pointer to the sprite sheet data (bank + address) |
move_speed | UINT8 | Movement speed in subpixels per frame |
collision_group | UINT8 | Collision group bitmask for filtering collisions |
next / prev | actor_t* | Linked list pointers (active or inactive list) |
script | far_ptr_t | Interact script (runs when player interacts with this actor) |
script_update | far_ptr_t | Update script (runs every frame via script_runner_update) |
Actor Flags
Actor behavior is controlled by a flags bitmask on each actor_t:
| Flag | Value | Purpose |
|---|---|---|
ACTOR_FLAG_PINNED | 0x01 |
Fixed to screen coordinates — does not scroll with the camera. Pinned actors are auto-activated during load_scene. |
ACTOR_FLAG_HIDDEN | 0x02 |
Not rendered by actors_render, but remains on the active list. Still receives update scripts and participates in collisions. |
ACTOR_FLAG_ANIM_NOLOOP | 0x04 |
Animation plays once and stops on the last frame instead of looping. |
ACTOR_FLAG_COLLISION | 0x08 |
Actor participates in collision detection with the player. |
ACTOR_FLAG_PERSISTENT | 0x10 |
Survives scene transitions — not removed when a new scene loads. Also auto-activated during load_scene. |
ACTOR_FLAG_ACTIVE | 0x20 |
Currently on the active linked list. Set/cleared by activation/deactivation routines. |
ACTOR_FLAG_DISABLED | 0x40 |
Prevents re-activation by the streaming system. Set by VM_ACTOR_DEACTIVATE. Only VM_ACTOR_ACTIVATE clears it. |
ACTOR_FLAG_INTERRUPT | 0x80 |
Scripts running on this actor can be interrupted by new interactions. |
Actor Indexing
Actor indices in C code are off by one from scene file indices:
actors[0]= player (always).- Scene actors with
_index Nin.gbsresfiles load intoactors[N + 1]. load_scenecopies scene actors intoactors + 1viamemcpy.
_index when accessing actors from C code.
// Scene file defines actor with _index 2
// In C code, access it as:
actor_t *npc = &actors[2 + 1]; // actors[3]
// Player is always actors[0]
actor_t *player = &actors[0]; // or use PLAYER macro
UINT16 px = PLAYER.pos.x;
Animation System
Each actor has 8 animation states (down, up, left, right × idle/moving). The engine auto-advances frames based on anim_tick.
How Animation Works
actor->animations[state]maps a state index to a frame range (frame_start/frame_end).- Each frame,
anim_tickdecrements. When it reaches zero,frameadvances. - If
frame > frame_end, it wraps toframe_start(unlessANIM_NOLOOPis set).
Manual Frame Control
To control frames directly from C code (e.g., in a scene type plugin), disable auto-animation:
// Stop the engine from auto-advancing frames
actor_t *actor = &actors[1];
actor->anim_tick = 255; // ANIM_PAUSED
// Now set frames directly
actor->frame = 3; // show frame 3
anim_tick = 255 to freeze animation, then write to actor->frame directly. This is the standard approach for scene type plugins that need precise frame control (used by Flight, DrinkingGame, AirHockey, etc.).
Animation from GBVM
; Set specific frame (pauses auto-animation)
VM_ACTOR_SET_ANIM_FRAME .ARG0 ; struct: [actorId, frame]
VM_ACTOR_SET_ANIM_TICK .ARG0, 255 ; prevent auto-advance
; Change animation set/state
VM_ACTOR_SET_ANIM_SET .ARG0 ; switch to different animation set
Streaming Activation
The engine dynamically activates and deactivates actors based on their distance from the camera to stay within the 12-actor active limit.
Lifecycle
- Scene load (
load_scene): Pinned and persistent actors are auto-activated. All others start on the inactive list. - Each frame (
actors_update): Inactive actors within the activation zone (on-screen + padding) are moved to the active list. Active actors that move outside the zone are deactivated. - Script deactivation (
VM_ACTOR_DEACTIVATE): SetsACTOR_FLAG_DISABLEDand moves the actor to the inactive list. The streaming system will not re-activate a disabled actor. - Script re-activation (
VM_ACTOR_ACTIVATE): ClearsACTOR_FLAG_DISABLEDand moves the actor back to the active list. Required after moving an unpinned actor off-screen.
VM_ACTOR_SET_POS), the streaming system will deactivate it. Use VM_ACTOR_ACTIVATE after repositioning to force it back onto the active list if needed.
Actor Rendering
actors_render() iterates the active linked list and writes each actor's metasprite data to shadow OAM.
- Only actors on the active list are rendered.
- Actors with
ACTOR_FLAG_HIDDENorACTOR_FLAG_DISABLEDare skipped. - Output goes to shadow OAM (double-buffered:
shadow_OAM2[40]). - The hardware OAM holds 40 sprite entries (8×16 mode). Large actors consume multiple entries.
- The metasprite system splits actor sprite sheets into individual 8×16 hardware sprites.
actors_render() runs during the scene loading exception handler — before the On Init script executes. Pinned actors are auto-activated during load_scene and will be visible for one frame. To prevent this, set hide_sprites = TRUE in state_init() and clear it on the first state_update() call.
Common Actor Operations from C
Getting / Setting Position
actor_t *actor = &actors[1];
// Read position (subpixels)
UINT16 x_subpx = actor->pos.x;
UINT16 y_subpx = actor->pos.y;
// Convert to pixels
UINT16 x_px = SUBPX_TO_PX(actor->pos.x); // pos.x >> 5
UINT16 y_px = SUBPX_TO_PX(actor->pos.y);
// Set position (convert pixels to subpixels)
actor->pos.x = PX_TO_SUBPX(80); // 80 << 5 = 2560
actor->pos.y = PX_TO_SUBPX(64);
Changing Animation State
// Let the engine handle animation (auto-advance)
actor_set_anim(actor, ANIM_STATE_MOVING);
// Manual frame control
actor->anim_tick = 255; // pause auto-animation
actor->frame = desired_frame;
Hiding / Showing Actors
// Hide (still active, still runs scripts)
actor->flags |= ACTOR_FLAG_HIDDEN;
// Show
actor->flags &= ~ACTOR_FLAG_HIDDEN;
Checking Flags
if (actor->flags & ACTOR_FLAG_PINNED) {
// Actor is pinned to screen
}
if (actor->flags & ACTOR_FLAG_ACTIVE) {
// Actor is on the active list
}
Moving Actors
// Direct position set (teleport)
actor->pos.x = PX_TO_SUBPX(100);
actor->pos.y = PX_TO_SUBPX(50);
// Incremental movement (per-frame in state_update)
actor->pos.x += actor->move_speed;
// Direction-based movement
switch (actor->dir) {
case DIR_UP: actor->pos.y -= actor->move_speed; break;
case DIR_DOWN: actor->pos.y += actor->move_speed; break;
case DIR_LEFT: actor->pos.x -= actor->move_speed; break;
case DIR_RIGHT: actor->pos.x += actor->move_speed; break;
}
GBVM Actor Commands
These are the GBVM opcodes for manipulating actors from scripts. Many use a pseudo-struct pattern where arguments are packed into consecutive stack slots.
| Opcode | Stack Layout | Description |
|---|---|---|
VM_ACTOR_SET_POS |
[actorId, X, Y] (3 words) |
Set actor position. X and Y are in subpixels. |
VM_ACTOR_GET_POS |
[actorId, X, Y] (3 words) |
Read actor position into the struct slots. |
VM_ACTOR_MOVE_TO |
Varies (INIT + XY steps) | Start a scripted move. Uses VM_ACTOR_MOVE_TO_INIT then VM_ACTOR_MOVE_TO_XY. |
VM_ACTOR_SET_ANIM_FRAME |
[actorId, frame] (2 words) |
Set the current animation frame. Pair with VM_ACTOR_SET_ANIM_TICK to prevent auto-advance. |
VM_ACTOR_GET_ANIM_FRAME |
[actorId, frame] (2 words) |
Read the current animation frame into the struct. |
VM_ACTOR_SET_ANIM_TICK |
actorVar, tick |
Set animation tick speed. Use 255 to pause animation. |
VM_ACTOR_SET_ANIM_SET |
actorVar, animSet |
Switch to a different animation set (sprite state). |
VM_ACTOR_ACTIVATE |
actorVar |
Re-activate a deactivated actor. Clears ACTOR_FLAG_DISABLED. |
VM_ACTOR_DEACTIVATE |
actorVar |
Deactivate actor and set ACTOR_FLAG_DISABLED to prevent streaming re-activation. |
VM_ACTOR_SET_FLAGS |
actorVar, flags, mask |
Set or clear specific actor flags using a bitmask. |
Stack Struct Pattern
Several GBVM actor commands use a pseudo-struct — consecutive stack words that the opcode reads/writes as a group. The base variable points to the first word.
; Position struct: 3 words at consecutive stack slots
; offset +0 = actorId
; offset +1 = X (subpixels)
; offset +2 = Y (subpixels)
; In event plugin JS:
const posStruct = helper._declareLocal("pos", 3, true);
helper.setActorId("actorId", "pos"); // writes actorId at +0
helper._addCmd("VM_ACTOR_GET_POS", posStruct); // fills +1 (X) and +2 (Y)
; Access individual fields in appendRaw:
; ^/(${posStruct} + 0)/ = actorId
; ^/(${posStruct} + 1)/ = X
; ^/(${posStruct} + 2)/ = Y
; Frame struct: 2 words
; offset +0 = actorId
; offset +1 = frame
const frameStruct = helper._declareLocal("frame", 2, true);
helper.setActorId("actorId", "frame");
helper._addCmd("VM_ACTOR_SET_ANIM_FRAME", frameStruct);
; After setting frame, pause auto-animation:
helper._addCmd("VM_ACTOR_SET_ANIM_TICK", frameStruct, "255");