PVG
24. Trigger Volumes — Program Video Games

24. Trigger Volumes

Every hidden area you've discovered, every trap you've accidentally triggered, every cutscene that played when you entered a room - all rely on triggers.

Triggers are invisible volumes in the game world that fire events when entities move through them.

The Fractal Code Cycle Applied to Triggers

Recall our fundamental pattern:

Input → Processing → Output

For our trigger system:

  • Input: Entity positions, trigger volumes, layer masks
  • Processing: Check if entities enter or exit trigger bounds
  • Output: Events fired for game systems to respond to

What Are Triggers Good For?

  • Triggering cutscenes or dialogue
  • Invisible boundaries that load new scenes
  • Checkpoint systems
  • Tutorial prompts at specific locations

Trigger System in the System Stack

Triggers sit in the World Layer alongside collisions:

  • Base Layer: Entities, Input, Time, Assets, Save/Load
  • Presentation: Camera, Shaders, Animation, UI, Particles
  • Interaction: Player, Collisions, AI, Scenes, Triggers
  • Game Systems: Stats, Combat, Inventory, Abilities
  • Content: Dialogue, Quests, NPC Schedules

Triggers depend on:

  1. Entity system (for positions and handles)
  2. Event system (for firing notifications)
  3. Physics system (for collision checks)

The Trigger Structure

Trigger :: struct {
    pos, size: Vec3,
    use_count: int,
    layers: Layers,
    entities_inside: [dynamic]Entity_Handle,
    flags: Trigger_Flags,
    on_enter: Maybe(Event),
    on_exit: Maybe(Event),
}

Layer System

Triggers use layers to filter which entities they respond to:

Layer :: enum {
    Player,
    NPC,
    Item,
}

Layers :: bit_set[Layer]

This allows triggers to respond only to specific entity types. A dialogue trigger might only care about the player, whereas traps might be triggered by either NPCs or the Player.

Entity Tracking

The entities_inside slice tracks which entities are currently within the trigger volume. This enables:

  • Firing exit events when entities leave
  • Preventing duplicate enter events
  • Supporting multi-entity triggers

Optional Events

Both on_enter and on_exit are Maybe(Event) types, meaning they're optional. A checkpoint trigger might only need on_enter, whilst a zone that tracks player presence might use both.

Trigger Flags

Trigger_Flag :: enum {
    Single_Use,
}

Trigger_Flags :: bit_set[Trigger_Flag]

Flags modify trigger behaviour. Single_Use would make a trigger fire once then disable itself - useful for one-time events like cutscenes.

The Update Algorithm

triggers_update :: proc(ws: ^World_State, es: ^Events_State) {
    for entity, i in ws.entities {
        for &trigger in ws.triggers {
            if entity.layer not_in trigger.layers {
                continue
            }

            entity_handle := Entity_Handle{i, ws.generations[i]}
            aabb := aabb_from_pos_size(trigger.pos, trigger.size)
            is_inside := check_point_vs_aabb(entity.position, aabb)
            index, was_inside := slice.linear_search(trigger.entities_inside[:], entity_handle)

            if is_inside && !was_inside {
                if on_enter, on_enter_ok := trigger.on_enter.?; on_enter_ok {
                    events_enqueue(es, on_enter)
                }
                append(&trigger.entities_inside, entity_handle)
            }

            if !is_inside && was_inside {
                if on_exit, on_exit_ok := trigger.on_exit.?; on_exit_ok {
                    events_enqueue(es, on_exit)
                }
                unordered_remove(&trigger.entities_inside, index)
            }
        }
    }
}

The Algorithm Steps

  1. Layer Filtering: Skip entities not on the trigger's layer mask
  2. State Detection: Check if entity is inside trigger volume
  3. Change Detection: Compare current state with previous state
  4. Event Firing:
    • Fire on_enter if entity just entered
    • Fire on_exit if entity just left
  5. Tracking Update: Add or remove entity from tracking list

Integration with Entity Layers

Entities now have a layer field:

Entity :: struct {
    // ... existing fields
    layer: Layer,
}

This must be set when creating entities:

player.layer = .Player
npc.layer = .NPC

Collision Detection Helpers

Two new physics procedures support trigger detection:

check_sphere_vs_aabb :: proc(sphere_center: Vec3, sphere_radius: f32, box: AABB) -> bool

check_point_vs_aabb :: proc(position: Vec3, box: AABB) -> bool

The point check uses a tiny sphere radius (0.01) to treat entity positions as points for trigger detection.

Creating Triggers

trigger_spawn(&gs.world, Trigger{
    pos = Vec3{2, 0.5, 2},
    size = Vec3{1, 2, 1},
    layers = {.Player},
    on_enter = Event{
        type = .Game_Start,
        debug_level = .Info,
        debug_message = "Entered Player trigger!",
    },
    on_exit = Event{
        type = .Game_Start,
        debug_level = .Info,
        debug_message = "Exited Player trigger!",
    },
})

Triggers are spawned into the world state and persist across frames.

Debug Visualisation

Triggers render as yellow bounding boxes:

for t in gs.world.triggers {
    aabb := aabb_from_pos_size(t.pos, t.size)
    rl.DrawBoundingBox(aabb, rl.YELLOW)
}

This makes it easy to see trigger placement and size during development.

Design Considerations

Performance

With dozens of entities and triggers, the double loop in triggers_update is acceptable. For hundreds of entities, consider:

  • Spatial partitioning (only check nearby triggers)
  • Separate active/inactive trigger lists

Single Use Triggers

The Single_Use flag isn't implemented yet but would work like this:

if .Single_Use in trigger.flags && trigger.use_count > 0 {
    continue  // Skip this trigger
}

// After firing on_enter
trigger.use_count += 1

Multi-Entity Triggers

The current system supports multiple entities in one trigger. You could extend this to require a specific count:

required_entity_count: int,

Then only fire events when len(entities_inside) == required_entity_count.

The Big Idea

Triggers allow us to fire events based on spatial positions of entities.

By providing a set of trigger events, levels can be created and modified without touching the compiled code.

The layer system ensures triggers respond to the correct entities.