On this page
- What If a Game World Could Exist in Two Places at Once?
- The Gap Between “Two Levels” and “Two Realities”
- Parallel Worlds, Unified Architecture
- Under the Hood: Architecture and the Reasoning Behind It
- The Stack
- Why These Choices Matter
- The Hardest Problem: Keeping Two Worlds Consistent
- From Code to Conviction
- What Comes Next
What If a Game World Could Exist in Two Places at Once?
Multiversal Consciousness is a custom-built 2D puzzle-platformer engine written in C++23 on top of SDL3. It targets game designers who want to build puzzles around a single core mechanic: two parallel realities that share some state but diverge in others. A player controls agents — switching between them at will — and must strategically flip between Reality A and Reality B to unlock doors, traverse chasms, and satisfy win conditions that span both worlds. The engine provides the Entity-Component System, the physics pipeline, the reality synchronization layer, and a tile-based level format so that designers can focus entirely on puzzle authoring.
The Gap Between “Two Levels” and “Two Realities”
Most engines make it easy to load a different level. Teleport the player, swap the tilemap, reset the state. But that’s not what dual-reality gameplay demands. The challenge is subtler: two versions of the same world must coexist simultaneously, with some elements shared between them and others diverging. A door unlocked in Reality A must also be unlocked in Reality B. But the key that opened it only exists in one reality, and the agent who picked it up may have a completely different position in the other.
This is the gap that motivated the project. Off-the-shelf engines treat the game world as a single, authoritative state. Bolting dual-reality mechanics onto that assumption means fighting the framework at every turn — duplicating scene graphs, manually mirroring state, and writing brittle synchronization code that breaks the moment a new shared element is introduced. I needed to build the engine from the ground up with reality duality as a first-class architectural concern.
Parallel Worlds, Unified Architecture
The engine delivers four capabilities that work together to make dual-reality puzzle design practical:
-
Reality-Aware State Management — Every piece of game state is explicitly classified as shared (doors, switches, water levels synchronize across both realities) or reality-specific (inventories, abilities, and optionally agent positions are tracked independently per reality). When a designer adds a new environmental element, the system’s classification determines its synchronization behavior automatically.
-
Universal Possession — A single player can transfer consciousness to any agent in the world using the number keys. The camera, input routing, HUD, and visual indicators all re-bind atomically. This allows puzzle designs where solving a challenge requires coordinating multiple agents, each potentially standing in a different reality.
-
Quantum Loadouts — Abilities are not global. They’re assigned per-reality through quantum nodes scattered across levels. An agent might carry an Axe in Reality A and a Keycard in Reality B. Switching realities changes what the agent can do, and cooldowns persist across the switch to prevent trivial exploitation.
-
Declarative Level Authoring — Levels are plain-text files using tile coordinates. A designer specifies agents, obstacles, quantum nodes, and win conditions in a human-readable format. The engine handles entity instantiation, component wiring, and validation. No code changes are needed to create new puzzles.
Under the Hood: Architecture and the Reasoning Behind It
The Stack
| Layer | Technology | Purpose |
|---|---|---|
| Language | C++23 | Memory control, zero-cost abstractions, move semantics |
| Graphics & Input | SDL3 / SDL3_ttf / SDL3_image | Cross-platform rendering, text, and image loading |
| Build System | CMake 3.20+ | Multi-platform builds, dependency fetching |
| Testing | Catch2 v3.4 | Behavior-driven test suite with tag filtering |
| Deployment | Emscripten (optional) | WebAssembly target for browser-based play |
Why These Choices Matter
I chose C++23 over a managed language because the ECS at the heart of this engine stores components in contiguous, cache-friendly arrays and uses swap-and-pop deletion to avoid memory fragmentation. These decisions only pay off when you control the memory layout, which rules out garbage-collected runtimes. C++23 specifically brings structured bindings, constexpr improvements, and tighter type deduction that reduce boilerplate without sacrificing performance.
I built a custom ECS rather than using an off-the-shelf framework because this engine needed dual-reality semantics baked into the component registry. The registry uses a two-level type-erasure pattern — a polymorphic base interface for runtime dispatch, backed by templated containers that store components in parallel arrays indexed by entity. This gives the reality system direct, typed access to component vectors for bulk state save-and-restore during reality switches, something that would require fighting the abstraction layer in a generic ECS.
SDL3 over a higher-level engine because I wanted to own the rendering pipeline, input routing, and frame timing — not inherit opinions about scene graphs or physics from a framework designed for a different kind of game. SDL3 provides hardware-accelerated 2D rendering, cross-platform windowing, and event handling without imposing architectural constraints.
The Hardest Problem: Keeping Two Worlds Consistent
The most architecturally demanding subsystem in the engine is reality switching — the moment a player presses the switch key and the entire game world pivots from one reality to the other.
The challenge isn’t the visual flip. It’s the state transaction. At the instant of a switch, the engine must save every reality-specific component for the current reality, load the corresponding components for the target reality, leave all shared components untouched, and do it all atomically so that no system observes a half-switched world during a single frame.
The solution is a three-tier state stratification model baked into the reality manager. Every component type falls into one of three categories. Shared geometry — transforms for linked agents, static platforms — is never touched during a switch. Reality-specific state — inventories, quantum node activations, unlinked agent positions — is maintained in parallel storage indexed by reality, and the switch operation is a bulk swap between the two indices. Cross-reality transactional state — doors, water levels, environmental switches — is synchronized bidirectionally after every switch to ensure consistency.
The key design insight is the concept of position linking. When agents are first created, their position is shared across realities — both realities see the agent in the same spot. But the moment an agent acquires a reality-specific ability from a quantum node, their position unlinks, and from that point forward each reality tracks the agent’s transform independently. This hybrid approach optimizes the common case while cleanly supporting the complex case. It emerged from playtesting: forcing designers to choose “linked or unlinked” at level-creation time was too rigid, so I made it a runtime transition triggered by gameplay instead.
From Code to Conviction
Classification is architecture. The single most impactful design decision in the engine wasn’t a data structure or an algorithm. It was deciding that every piece of game state must be explicitly classified as shared, reality-specific, or cross-reality transactional. That classification drives synchronization behavior, serialization boundaries, and system update order. In any domain where state has multiple scopes — multi-tenant SaaS, distributed caches, collaborative editing — forcing an explicit scope classification early prevents an entire category of consistency bugs later.
Composition scales; inheritance does not. The interactive obstacle system went through two iterations. The first used a deep inheritance tree where each obstacle type extended a base class with overrides for interaction, rendering, and collision. Adding a new obstacle type meant understanding the entire hierarchy. The second iteration — the one that shipped — uses a flat strategy pattern. Each obstacle is a small, independent implementation of an interaction interface. Adding a new obstacle type means writing one class that answers four questions: what ability is required, what happens on interaction, what prompt to display, and what feedback to give.
Design for the level designer, not the engine programmer. The tile-based level format exists because the first version used pixel coordinates, and every level file was a wall of inscrutable numbers. Switching to tile coordinates made levels human-readable and dramatically reduced authoring errors. The best engine feature is one that makes the person using the engine more productive, even if it means more work inside the engine itself.
What Comes Next
Puzzle validation integration. The level loader currently parses win conditions from level files, but the runtime validation pipeline isn’t yet wired to the puzzle validator for dynamic condition checking. Connecting these systems would allow levels to define richer completion criteria — multi-agent coordination goals, timed challenges, and conditional branching — evaluated automatically by the engine rather than hardcoded per level.
An audio system. Sound design is one of the strongest tools for communicating reality state to the player. A system that ties ambient audio, interaction feedback, and transition effects to the reality manager would deepen the gameplay experience without requiring changes to the existing architecture — audio fits cleanly into the ECS as another system with its own components.
A visual level editor. The tile-based text format lowers the barrier to level authoring, but a graphical editor with real-time preview would make it accessible to non-technical designers. The ECS architecture already separates data from logic cleanly enough that a GUI tool could manipulate component data directly and serialize back to the level format, closing the loop between design and playtesting.
Try It Out
Check out the live demo or explore the source code on GitHub.