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:
Gets stuck on corners - Random points don't account for obstacles between current position and destination
No pathfinding - Entities walk in straight lines, hitting walls
Inefficient - Tries 10 random points, may still fail
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:
Generating random offset
Checking if it's inside obstacles (if so, try again)
Checking if it's on walkable ground
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
Create a new entity definition for a different enemy type
Add multiple spawners of the same type to a scene
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.