On this page
- What You Need Before Managing Parallel Worlds
- A Hotel with Two Lobbies and a Shared Basement
- Three Tiers of State, One Data Structure
- The Switch Itself Is Trivial (Wrapping It Isn’t)
- Position Linking: The Most Subtle Piece
- The Save-Switch-Load Protocol
- Shared State Stays In Sync Every Frame
- What This Costs in Memory and Time
- Entity Destruction, Stale State, and the Unlink Timing Trap
Imagine a game where a door unlocked in Reality A must also appear unlocked in Reality B — but the key that opened it only exists in Reality A’s inventory, and the agent who picked it up is standing in a completely different position in Reality B. Some state is shared. Some state is independent. And when the player presses one button to switch realities, the entire world must pivot atomically — no system should ever observe a half-switched state during a frame.
This tutorial dissects the Multiversal Consciousness engine’s three-tier state stratification model: the architecture that classifies every piece of game state as shared, reality-specific, or cross-reality transactional, and the save-switch-load protocol that ensures consistency during reality transitions. No locks, no queues, no eventual consistency. Just a carefully ordered sequence of operations that the single-threaded game loop guarantees will complete before any system reads the new state.
What You Need Before Managing Parallel Worlds
You need a thorough understanding of the ECS pattern from Tutorial 2 (EntityManager, ComponentRegistry, ComponentContainer), plus familiarity with std::unordered_map as parallel state storage, enum classes indexed into std::array, and std::chrono for performance measurement. Most importantly, you need to be comfortable with the concept of transactional state changes — operations that must either fully complete or not happen at all.
On the environment side: C++17 or later, the engine’s ECS layer, and SDL3 for input handling.
A Hotel with Two Lobbies and a Shared Basement
The reality system is a hotel with two lobbies (Reality A and B) and a shared basement.
Each lobby has its own guest register (inventories), its own room assignments (agent positions), and its own event board (quantum node activations). Guests in Lobby A can’t see Lobby B’s register. But the basement is shared — the building’s doors, water pipes, and electrical switches serve both lobbies. When someone flips a switch in the basement, both lobbies are affected immediately.
Now imagine a lobby swap: every guest is instantly teleported from their current lobby to the other one. Room assignments change, event boards change, but the basement stays the same. The swap has to be atomic — no guest should be half in Lobby A and half in Lobby B.
Three Tiers of State, One Data Structure
The Reality enum maps directly to array indices. That’s the whole trick. The RealityManager maintains three categories of storage: reality-indexed maps for per-reality state, a flat map for shared geometry, and dedicated maps for cross-reality environmental state.
enum class Reality : uint8_t {
A = 0,
B = 1
};
class RealityManager {
private:
Reality current_reality_{Reality::A};
// TIER 1: Reality-specific state (indexed by Reality enum)
std::array<std::unordered_map<EntityID, Inventory>, 2> reality_inventories_;
std::array<std::unordered_map<EntityID, QuantumNode>, 2> reality_quantum_nodes_;
std::array<std::unordered_map<EntityID, Transform>, 2> reality_transforms_;
// TIER 2: Shared geometry (same in both realities)
std::unordered_map<EntityID, Transform> shared_geometry_;
// TIER 3: Cross-reality transactional state
std::unordered_map<EntityID, Door> shared_doors_;
std::unordered_map<EntityID, WaterLevel> shared_water_levels_;
std::unordered_map<EntityID, EnvironmentalSwitch> shared_switches_;
};
The std::array<Map, 2> pattern is the foundation. Indexing by static_cast<size_t>(Reality::A) gives index 0; Reality::B gives index 1. No branching, no string lookups — just direct array access.
The Switch Itself Is Trivial (Wrapping It Isn’t)
Flipping a reality flag is one line. What the engine wraps around it matters more — a timing measurement that enforces a hard 100ms performance SLA. If the switch takes longer due to excessive state synchronization, the engine logs a warning.
bool RealityManager::switch_reality() {
auto switch_start = std::chrono::steady_clock::now();
current_reality_ = (current_reality_ == Reality::A) ? Reality::B : Reality::A;
auto switch_end = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
switch_end - switch_start);
last_switch_time_ = switch_end;
bool within_limit = duration <= MAX_SWITCH_TIME; // 100ms
if (!within_limit) {
std::cerr << "Reality switch took " << duration.count()
<< "ms, exceeding limit of " << MAX_SWITCH_TIME.count()
<< "ms" << std::endl;
}
return within_limit;
}
Position Linking: The Most Subtle Piece
Agents start with linked positions — shared across both realities. When an agent acquires a reality-specific ability, their position unlinks, and from that point forward, each reality tracks the agent’s transform independently.
void RealityManager::unlink_agent_position(EntityID entity,
const Transform& current_transform) {
shared_geometry_.erase(entity);
// Seed BOTH realities with the current position as a starting point.
// This prevents a jarring teleport when the player first switches after acquiring an ability.
reality_transforms_[static_cast<size_t>(Reality::A)][entity] = current_transform;
reality_transforms_[static_cast<size_t>(Reality::B)][entity] = current_transform;
}
bool RealityManager::is_position_linked(EntityID entity) const {
return reality_transforms_[static_cast<size_t>(Reality::A)]
.find(entity) == reality_transforms_[static_cast<size_t>(Reality::A)].end();
}
Seeding both realities with the same position at unlink time is a deliberate design choice. Without it, the first reality switch after acquiring an ability would teleport the agent to wherever their position happened to be stored in the other reality’s uninitialized slot — which is nothing.
The Save-Switch-Load Protocol
RealitySystem orchestrates the actual switch. It wraps RealityManager and coordinates with the ComponentRegistry to save and load per-reality state in the correct order.
void RealitySystem::update(float delta_time) {
if (input_manager_ &&
input_manager_->is_action_just_pressed(InputAction::SWITCH_REALITY)) {
// PHASE 1: Save current reality's state FROM the live components
save_current_reality_transforms();
save_current_reality_inventories();
save_current_reality_quantum_nodes();
// PHASE 2: Flip the reality flag
switch_reality();
// PHASE 3: Load the NEW reality's state INTO the live components
load_current_reality_transforms();
load_current_reality_inventories();
load_current_reality_quantum_nodes();
}
synchronize_all_entities();
}
The ordering is not negotiable. Save before flip, load after flip. Any system that reads component state between these phases is guaranteed to see a consistent snapshot. There’s no frame where half the agents are in Reality A’s positions and half are in Reality B’s.
Each save/load pair follows the same pattern. Only unlinked agents participate — linked agents use shared geometry and are never touched during the switch.
void RealitySystem::save_current_reality_transforms() {
const auto* agents = component_registry_->get_all_components<Agent>();
if (!agents) return;
Reality current = reality_manager_->get_current_reality();
for (EntityID entity : agents->get_entities()) {
const Agent* agent = component_registry_->get_component<Agent>(entity);
if (agent && !agent->position_linked) {
const Transform* t = component_registry_->get_component<Transform>(entity);
if (t) {
reality_manager_->set_reality_transform(entity, *t, current);
}
}
}
}
void RealitySystem::load_current_reality_transforms() {
const auto* agents = component_registry_->get_all_components<Agent>();
if (!agents) return;
Reality current = reality_manager_->get_current_reality();
for (EntityID entity : agents->get_entities()) {
const Agent* agent = component_registry_->get_component<Agent>(entity);
if (agent && !agent->position_linked) {
const Transform* stored =
reality_manager_->get_reality_transform(entity, current);
if (stored) {
Transform* live = component_registry_->get_component<Transform>(entity);
if (live) *live = *stored;
}
}
}
}
Shared State Stays In Sync Every Frame
Shared elements — doors, water, switches — are written to the RealityManager every frame, regardless of which reality is active. When a system opens a door in Reality A, the door is also open when the player switches to Reality B because the door state was already synchronized.
void RealitySystem::synchronize_entity(EntityID entity) {
if (component_registry_->has_component<Agent>(entity)) {
const Agent* agent = component_registry_->get_component<Agent>(entity);
if (agent && !agent->position_linked) {
Transform* t = component_registry_->get_component<Transform>(entity);
if (t) reality_manager_->set_reality_transform(
entity, *t, reality_manager_->get_current_reality());
} else {
Transform* t = component_registry_->get_component<Transform>(entity);
if (t) reality_manager_->sync_shared_geometry(entity, *t);
}
}
if (component_registry_->has_component<Door>(entity)) {
const Door* door = component_registry_->get_component<Door>(entity);
if (door) reality_manager_->sync_shared_door(entity, *door);
}
if (component_registry_->has_component<WaterLevel>(entity)) {
const WaterLevel* wl = component_registry_->get_component<WaterLevel>(entity);
if (wl) reality_manager_->sync_shared_water_level(entity, *wl);
}
if (component_registry_->has_component<EnvironmentalSwitch>(entity)) {
const EnvironmentalSwitch* sw =
component_registry_->get_component<EnvironmentalSwitch>(entity);
if (sw) reality_manager_->sync_shared_switch(entity, *sw);
}
}
The engine synchronizes shared state every frame rather than only on change. An event-driven alternative would save CPU cycles but introduce coupling between the door-opening system and the reality system. I chose simplicity: shared state is always consistent, even if nothing changed.
What This Costs in Memory and Time
The three-tier model trades memory for correctness. Every unlinked agent’s transform is stored three times: once in the live ComponentRegistry, once in reality_transforms_[0], and once in reality_transforms_[1]. For inventories and quantum nodes, the same triplication applies.
This is deliberate. The alternative — reconstructing reality-specific state on demand — would require either a command log (expensive to replay) or a diff-and-patch system (complex to implement correctly). Full-copy duplication keeps the switch operation simple: write to slot A, read from slot B.
The save-switch-load protocol is O(n) per component type, where n is the entity count. For a level with 9 agents, 20 inventory items, and 10 quantum nodes, that’s under 100 iterations total — well within the 100ms SLA even on modest hardware.
Entity Destruction, Stale State, and the Unlink Timing Trap
When an entity is destroyed, it must be removed from all reality-tracking maps — both reality-indexed stores for Reality A and B, and all shared stores. Missing even one map means a dangling entry that could be loaded into a newly recycled entity ID. The engine’s remove_entity() explicitly erases from all nine maps:
void RealityManager::remove_entity(EntityID entity) {
shared_geometry_.erase(entity);
reality_transforms_[0].erase(entity);
reality_transforms_[1].erase(entity);
reality_inventories_[0].erase(entity);
reality_inventories_[1].erase(entity);
reality_quantum_nodes_[0].erase(entity);
reality_quantum_nodes_[1].erase(entity);
shared_doors_.erase(entity);
shared_water_levels_.erase(entity);
shared_switches_.erase(entity);
}
There’s also an unlink timing trap worth knowing: if unlink_agent_position() is called during a reality switch — between save and load — the agent’s position will be seeded from a potentially stale transform. The engine avoids this by only allowing unlinking during the normal update phase, never during the switch protocol.
Every get_* method in RealityManager returns a pointer that may be null. The RealitySystem always null-checks before dereferencing. Pointers from the reality manager are always optional.
On level transitions, reset() clears all eleven internal maps and resets to Reality A. Any system holding a stale pointer into one of these maps has a dangling pointer after reset. The engine handles this by flushing and rebuilding all systems during level transitions — no cross-level state survives.