PVG
27. Spawners and Entity Definitions — Program Video Games

27. Spawners and Entity Definitions

Every enemy that ambushes you, every NPC that greets you in town, every creature that roams the wilderness - they all begin their existence through spawners using entity definitions.

In our System Stack, spawners bridge the World and Content layers, creating instances of entities based on reusable definitions.

The Fractal Code Cycle Applied to Spawners

Recall our fundamental pattern:

Input → Processing → Output

For our spawner system:

  • Input: Entity definition data from JSON, spawn position from scene

  • Processing: Create entity instance, apply definition properties, position in world

  • Output: Active entity in the game world

What Are Spawners Good For?

Entity Instantiation

  • Place enemies in specific locations

  • Populate scenes with NPCs

  • Create interactive objects at design-time positions

Data-Driven Design

  • Separate entity properties from placement

  • Reuse definitions across multiple spawners

  • Modify entity behaviour without touching spawn code

Scene Management

  • Load entities appropriate for each scene

  • Unload entities when changing scenes

  • Control population density per area

Entity Definitions vs Entity Instances

It's critical to distinguish between these two concepts:

Entity Definition: The blueprint for an entity type

  • Stored as JSON data

  • Defines properties like movement speed, collision radius, behaviours

  • Loaded once at startup

  • Reusable across multiple instances

Entity Instance: An actual entity in the game world

  • Created from a definition

  • Has position, velocity, state

  • Unique per spawner

  • Destroyed when scene changes

The Entity Definition Structure

Entity_Definition :: struct {
    name:            string,
    movement_speed:  f32,
    collider_radius: f32,
    flags:           Entity_Flags,
    behavior_flags:  Entity_Behaviors,
    layer:           Layer,
}

This contains all the properties that define what type of entity this is, without any instance-specific data like position or current velocity.

JSON Entity Definition

Here's our test enemy definition:

name = "Test Enemy"
collider_radius = 0.5
movement_speed = 2
flags = 2
behavior_flags = 1

Simple JSON (SJSON format) that defines the entity's core properties. Note the clean syntax without braces or quotes - SJSON allows this flexibility.

The Behaviour System

We've introduced a new concept: behaviour flags.

Entity_Behaviors :: bit_set[Entity_Behavior]
Entity_Behavior :: enum {
    Wander,
}

Similar to entity flags, behaviours are a bit set that determines what AI routines run for an entity. Currently we only have Wander, but this will expand to include behaviours like:

  • Patrol

  • Chase

  • Flee

  • Guard

  • Follow

Spawner Structure

Entity_Spawner :: struct {
    pos:  Vec3,
    type: string,
}

A spawner is simply a position and a reference to an entity definition by name. When the scene loads, it looks up the definition and creates an instance at that position.

Scene Definition Integration

Spawners are added to scene JSON files:

entity_spawners: [
    {
        pos: [4, 0.5, -4],
        type: "Test Enemy",
    }
],

The scene definition now includes an array of spawners, each specifying where to spawn and what to spawn.

Loading Entity Definitions

At startup, we load all entity definitions:

dir, dir_err := os.open("data/entities")
// ... error handling ...

file_infos, fi_err := os.read_dir(dir, 0)

for file_info in file_infos {
    data, ok := os.read_entire_file(file_info.fullpath)
    
    entity_def: Entity_Definition
    json_err := json.unmarshal(data, &entity_def, .SJSON)
    
    gs.entity_definitions[entity_def.name] = entity_def
}

We iterate through the data/entities/ directory, parse each JSON file, and store the definitions in a map keyed by name.

Creating Entities from Definitions

entity_create_with_def :: proc(ss: ^Scene_State, def: Entity_Definition) -> Entity_Handle {
    h := entity_create(ss)
    e := entity_get(ss, h)
    
    e.collider_radius = def.collider_radius
    e.movement_speed = def.movement_speed
    e.flags = def.flags
    e.behavior_flags = def.behavior_flags
    
    return h
}

This procedure creates a new entity using our existing API, then copies properties from the definition. The entity still needs its position set separately.

Spawning During Scene Load

When loading a scene, we iterate through spawners:

for spawner in ss.entity_spawners {
    entity_def, ok := entity_defs[spawner.type]
    if !ok {
        log.errorf("Couldn't find entity def: `%s`", spawner.type)
    } else {
        h := entity_create_with_def(ss, entity_def)
        e := entity_get(ss, h)
        e.position = spawner.pos
    }
}

For each spawner, we look up the definition, create an entity instance, and position it at the spawner location.

The Wander Behaviour (Naive Implementation)

Our first behaviour is a simple random wander:

if .Wander in entity.behavior_flags {
    entity.flags += {.Waypoints}
    
    if entity.waypoint_count == 0 {
        ground := ws.walkable_surfaces[:]
        obstacles := ws.static_geometry[:]
        dest := get_nearby_point(entity.position, ground, obstacles)
        dest.y = entity.position.y
        entity.waypoints[0] = dest
        entity.waypoint_count = 1
    } else if rl.Vector3DistanceSqrt(entity.waypoints[0], entity.position) < 0.3 {
        entity.waypoint_count = 0
    }
}

This picks a random nearby point, sets it as a waypoint, and when reached, picks a new random point.

Why This Implementation is Naive

The current wander behaviour has serious problems:

  1. Gets stuck on corners - Random points don't account for obstacles between current position and destination

  2. No pathfinding - Entities walk in straight lines, hitting walls

  3. Inefficient - Tries 10 random points, may still fail

  4. Unrealistic - Real creatures don't walk in perfectly straight lines to random points

This is intentionally simple to get something working. The next lesson will implement nav grids for proper pathfinding.

The get_nearby_point Function

get_nearby_point :: proc(pos: Vec3, ground: []Quad, obstacles: []OBB3) -> Vec3 {
    ATTEMPTS :: 10
    
    for _ in 0..<ATTEMPTS {
        next_x := -2.5 + rand.float32() * 5
        next_z := -2.5 + rand.float32() * 5
        next_pos := pos + Vec3{next_x, 0, next_z}
        
        // Check if point is inside an obstacle
        for obb in obstacles {
            if check_sphere_vs_obb3(next_pos, 0.2, obb) {
                continue
            }
        }
        
        // Check if point is on walkable ground
        hit, hit_normal, ok := sample_ground_height(ground, next_pos, 1)
        if ok {
            return hit
        }
    }
    
    return pos
}

This attempts to find a valid nearby point by:

  1. Generating random offset

  2. Checking if it's inside obstacles (if so, try again)

  3. Checking if it's on walkable ground

  4. Returning current position if all attempts fail

Spawners in the System Stack

Spawners sit between multiple layers:

  • World Layer: Uses entities and positions

  • Content Layer: Driven by scene definitions

  • Base Layer: Depends on asset management (definitions loaded at startup)

The spawner system bridges data-driven content with runtime entity management.

Design Considerations

Spawn Timing

On Scene Load: Our current approach - all entities spawn immediately
Triggered Spawns: Spawn when player enters area or completes quest
Timed Spawns: Respawn after delay (like enemy respawns)

Our simple approach works for now, but can be extended later.

Definition Reusability

The same definition can be used by multiple spawners:

  • Multiple goblins from one definition

  • Different NPCs sharing base properties

  • Consistency across similar entities

Memory Management

Definitions are loaded once and stored in a map. Instances are created per-scene and cleaned up on scene change. This keeps memory usage predictable.

Extending the System

Future additions might include:

Spawn Conditions

  • Only spawn if quest active

  • Only spawn at certain times of day

  • Only spawn if player level >= X

Spawn Groups

  • Multiple entities from one spawner

  • Formation-based spawning

  • Wave-based encounters

Dynamic Properties

  • Randomise some values per instance

  • Scale difficulty based on player level

  • Variation in appearance per instance

Try It Yourself

  1. Create a new entity definition for a different enemy type

  2. Add multiple spawners of the same type to a scene

  3. Experiment with different behaviour combinations (when we have more)

The Big Idea

The spawner and entity definition system separates "what" entities are (definitions) from "where" they appear (spawners) and "when" they exist (instances).

This data-driven approach gives designers control over entity placement without programming knowledge, whilst keeping entity behaviour consistent and maintainable.

Our naive wander behaviour proves the system works, even if it's not production-ready. The next lesson's nav grid implementation will transform this into smooth, obstacle-aware pathfinding.