25. Scene Switching
Distinct locations in your game - the town square, wilderness paths, dungeon chambers - each needs its own set of entities, geometry, and triggers. When players move between these locations, the game must efficiently swap out one scene and load another without memory leaks or fragmentation.
This brings us to the Scene System - a structured approach to managing distinct game locations and their associated data.
The Fractal Code Cycle Applied to Scenes
Recall our fundamental pattern:
Input → Processing → Output
For our scene system:
- Input: Scene definition data (from JSON), scene change requests
- Processing: Allocate scene state, populate entities and geometry, manage transitions
- Output: Fully initialised scene ready for gameplay
What Are Scenes Good For?
Discrete Locations
- Town areas with NPCs and shops
- Wilderness regions with enemies
- Indoor locations like houses or dungeons
- Battle arenas
Memory Management
- Load only what's needed for the current location
- Unload everything when leaving
- Prevent memory fragmentation
- Clean slate for each scene
Data Organisation
- Each scene contains its own entities, geometry, triggers
- Scene definitions describe what to load
- Scene state holds the runtime data
- Clear separation between definition and state
Scene Definitions vs Scene State
Scene Definition
A scene definition is the template - static data describing what a scene should contain:
Scene_Definition :: struct {
name: string,
objects: []Object,
static_geometry: []OBB3,
walkable_surfaces: []Walkable_Surface,
triggers: []Trigger,
}
Loaded from JSON files and stored in gs.scenes map. These persist for the entire game session.
Scene State
Scene state is the instance - runtime data for the currently active scene:
Scene_State :: struct {
allocator: mem.Allocator,
scene_name: string,
player_handle: Entity_Handle,
entities: [dynamic]Entity,
generations: [dynamic]int,
active_entities: [dynamic]int,
unused_entity_handles: [dynamic]int,
objects: [dynamic]Object,
static_geometry: [dynamic]OBB3,
walkable_surfaces: [dynamic]Walkable_Surface,
triggers: [dynamic]Trigger,
}
Created when entering a scene, destroyed when leaving. This is the "living" version with entities moving, triggers activating, etc.
Memory Management with Arena Allocators
Why Arena Allocators?
Arena allocators are perfect for scene management:
Fast Allocation: Bump pointer allocation, no searching for free blocks Fast Deallocation: Reset the entire arena at once, no individual frees Cache Friendly: Sequential memory layout No Fragmentation: Each scene gets a clean slate
The Scene Arena
The game state now uses a scene arena instead of individual allocations:
Game_State :: struct {
// ... other fields ...
scene_arena: mem.Dynamic_Arena,
scene: ^Scene_State,
scenes: map[string]Scene_Definition,
}
scene_arena: Memory pool for scene datascene: Pointer to current scene statescenes: Map of scene definitions (persists)
Scene Creation Process
The scene_create procedure handles the full scene initialisation:
scene_create :: proc(arena: ^mem.Dynamic_Arena, def: Scene_Definition) -> ^Scene_State {
// Initialise arena if needed
if arena.current_block == nil {
mem.dynamic_arena_init(arena, alignment = runtime.MAP_CACHE_LINE_SIZE)
}
// Reset arena - destroys previous scene
mem.dynamic_arena_reset(arena)
allocator := mem.dynamic_arena_allocator(arena)
// Use scene allocator for everything
context.allocator = allocator
// Allocate scene state
ss := new(Scene_State)
// Initialise all dynamic arrays
ss.active_entities = make(type_of(ss.active_entities))
ss.entities = make(type_of(ss.entities))
ss.generations = make(type_of(ss.generations))
// ... more arrays ...
// Create player
ss.player_handle = player_init(ss)
// Copy scene definition data
append(&ss.objects, ..def.objects[:])
append(&ss.static_geometry, ..def.static_geometry[:])
append(&ss.walkable_surfaces, ..def.walkable_surfaces[:])
// Initialise triggers with their dynamic arrays
for def_trigger in def.triggers {
trigger := def_trigger
trigger.entities_inside = make(type_of(trigger.entities_inside))
append(&ss.triggers, trigger)
}
ss.scene_name = strings.clone(def.name)
return ss
}
Key Steps
- Reset Arena: Destroys previous scene completely
- Allocate State: Create new scene state structure
- Initialise Arrays: All dynamic arrays use scene allocator
- Copy Data: Transfer definition data to scene state
- Special Handling: Triggers need their internal arrays initialised
Scene Parsing and Definitions
Scene definitions are loaded from JSON and parsed into structures:
scene_parse :: proc(data: []u8, allocator := context.allocator) -> (def: Scene_Definition) {
// Parse into temporary structure
temp_def: Scene_Definition
json.unmarshal(data, &temp_def, .SJSON, context.temp_allocator)
// Use passed allocator for permanent storage
context.allocator = allocator
def.name = strings.clone(temp_def.name)
// Allocate and copy arrays
def.objects = make(type_of(def.objects), len(temp_def.objects))
def.static_geometry = make(type_of(def.static_geometry), len(temp_def.static_geometry))
def.walkable_surfaces = make(type_of(def.walkable_surfaces), len(temp_def.walkable_surfaces))
copy(def.objects, temp_def.objects)
copy(def.static_geometry, temp_def.static_geometry)
copy(def.walkable_surfaces, temp_def.walkable_surfaces)
// Manual parsing for complex types (triggers)
// ...
return
}
This two-stage parsing ensures:
- Temporary parsing uses temp allocator
- Final definition uses specified allocator (usually main allocator)
- Scene definitions persist for game duration
Scene Transitions
Changing scenes is now a single operation:
// Before (old approach)
scene_change(gs, gs.scenes["Town"])
// After (new approach)
gs.scene = scene_create(&gs.scene_arena, gs.scenes["Town"])
The arena reset in scene_create automatically destroys the previous scene - no need for manual cleanup.
Integration with Entity System
All entity functions now take Scene_State instead of World_State:
entity_create :: proc(ws: ^Scene_State) -> Entity_Handle
entity_get :: proc(ws: ^Scene_State, handle: Entity_Handle) -> ^Entity
entity_destroy :: proc(ws: ^Scene_State, handle: Entity_Handle)
entities_update :: proc(ws: ^Scene_State, dt: f32)
This makes it clear that entities belong to a scene and are destroyed with it.
Scene System in the System Stack
The Scene System sits in the World Layer:
- Dependencies: Assets (for models), Time (for updates)
- Provides: Environment for Player, Collisions, AI
- Manages: Entities, geometry, triggers, transitions
It acts as the container for all interactive elements in a location.
Debug Memory Tracking
In debug builds, we now track all allocations:
when ODIN_DEBUG {
permanent_arena: mem.Dynamic_Arena
mem.dynamic_arena_init(&permanent_arena, alignment = runtime.MAP_CACHE_LINE_SIZE)
context.allocator = mem.dynamic_arena_allocator(&permanent_arena)
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
context.allocator = mem.tracking_allocator(&track)
defer {
if len(track.allocation_map) > 0 {
fmt.eprintf("=== %v allocations not freed: ===
", len(track.allocation_map))
for _, v in track.allocation_map {
fmt.eprintf("- %v bytes @ %v
", v.size, v.location)
}
}
}
}
This catches memory leaks during development.
Design Considerations
Why Arena Allocators for Scenes?
Advantages:
- Extremely fast allocation and deallocation
- No fragmentation between scenes
- Simple mental model
- Perfect for data with the same lifetime
Trade-offs:
- Can't free individual objects
- Everything in scene has same lifetime
- May use more memory than necessary
For our RPG with distinct scenes, these trade-offs are worth it.
Why Pointer to Scene State?
scene: ^Scene_State, // Pointer
Using a pointer instead of a value has benefits:
- Arena reset doesn't need to update Game_State
- Clear that scene can be nil
- Easier to pass around
- Smaller copy when passing Game_State
Scene Definition Storage
Scene definitions use the main allocator:
if data, ok := os.read_entire_file(path, context.temp_allocator); ok {
def := scene_parse(data) // Uses main allocator
gs.scenes[def.name] = def
}
They persist for the entire game session so other systems can reference them.
The Big Idea
Scene systems separate what should exist (definitions) from what currently exists (state).
Arena allocators make scene transitions instant - one reset operation destroys everything and provides a clean slate for the next scene.
This approach eliminates entire categories of bugs:
- No dangling entity pointers between scenes
- No forgotten cleanup code
- No memory fragmentation
- No gradual memory leaks
The scene system transforms complex memory management into a simple, predictable operation.