Enigma Engine is a C++26 game engine I built from the ground up, focusing on modular architecture, runtime extensibility, and Unreal-inspired API design. The engine features runtime DLL module loading with dependency resolution, a Subsystem architecture for pluggable engine services, a composition-based Scene/GameObject/Component framework with deterministic tick scheduling, and an Enhanced Input plugin with a full modifier/trigger pipeline. I also built the entire build toolchain in C# .NET, covering project scaffolding, CMake generation, Visual Studio solution output, and multi-configuration compilation from Debug through Shipping.
EnigmaArcade Demo
Engine Architecture Overview
The engine is organized into layered runtime modules, each compiled as an independent DLL with explicit dependency declarations. FEngineLoop drives the top-level frame loop, FEngine owns the global FSubsystemCollection, and FGameInstance provides the user-programmable game loop with integrated scene management.
graph TD
subgraph EngineLoop["FEngineLoop (Entry Point)"]
EL[PreInit / Init / Tick / Exit]
end
subgraph Engine["FEngine (Global Singleton)"]
SC[FSubsystemCollection]
IS[FInputSubsystem - Priority 1000]
TM[FTickTaskManager - Priority 500]
end
subgraph GameInstance["FGameInstance (User Game Logic)"]
SM[FSceneManager]
SI[SetupInput]
end
subgraph Scene["FScene (Object Container)"]
GO[FGameObject]
TC[FTransformComponent]
CC[Custom Components]
RC[FRenderComponent]
end
subgraph TickSystem["Tick Dispatch"]
TG1[TG_PreUpdate]
TG2[TG_Update]
TG3[TG_PostUpdate]
end
subgraph ModuleManager["FModuleManager"]
EM[Engine Modules]
GM[Game Modules]
PM[Plugin Modules]
end
EL --> Engine
SC --> IS
SC --> TM
Engine --> GameInstance
SI --> IS
GameInstance --> SM
SM --> Scene
GO --> TC
GO --> CC
GO --> RC
TM --> TickSystem
TG1 --> TG2 --> TG3
EL --> ModuleManager
Each frame follows a deterministic sequence: the message pump processes OS events, FInputSubsystem evaluates the input pipeline and fires callbacks, FTickTaskManager dispatches component ticks across three ordered groups (PreUpdate, Update, PostUpdate), and finally FGameInstance drives scene updates and rendering.
Module and Plugin System
I implemented a cross-DLL module system inspired by Unreal Engine’s module architecture. Each module (Core, Engine, Launch, AsciiRenderer, game modules, plugins) compiles into its own DLL with explicit API export macros. Modules self-register through static initialization using the IMPLEMENT_MODULE macro, which creates a factory function linked to a global module list at load time, requiring no central manifest file.
FModuleManager handles the full lifecycle: LoadLibrary to load the DLL, locate the self-registered factory, instantiate the IModuleInterface, and call StartupModule(). Shutdown proceeds in reverse order (LIFO). The system supports five loading phases (EarliestPossible through PostEngineInit) so plugins can declare when they need to initialize relative to the engine boot sequence.
Plugins are described by .eplugin JSON descriptors that declare module names, types, loading phases, and dependencies. The BuildTool resolves the full dependency graph at build time, generating correct CMake link orders and DLL copy rules.
BuildTool (C# .NET 9)
The build toolchain is a standalone C# CLI that handles project scanning, dependency resolution, CMake generation, compilation orchestration, and packaging. It supports five build configurations:
| Configuration | Optimization | Link Mode |
|---|---|---|
| Debug | /Od | Modular (DLL) |
| DebugGame | /O1 | Modular (DLL) |
| Development | /O1 | Modular (DLL) |
| Shipping | /O2 | Monolithic |
| Test | /O2 | Modular (DLL) |
Project scaffolding commands (create-project, create-module, create-plugin) generate complete template code with proper module descriptors, API export headers, and CMake integration.
Subsystem Architecture
I designed a Subsystem architecture modeled after Unreal’s USubsystem hierarchy, adapted for a non-UObject, cross-DLL environment. The core abstraction is ISubsystem, a lightweight interface with lifecycle hooks (Initialize, PostInitialize, Tick, Deinitialize) and opt-in per-frame ticking controlled by IsTickable() and GetTickPriority().
FSubsystemCollection manages the full subsystem lifecycle. It uses string-based registration keys (each subsystem provides a static GetStaticName()) rather than std::type_index, which is unreliable across DLL boundaries. The initialization sequence follows four phases:
- Filter subsystems via
ShouldCreateSubsystem()(conditional creation) - Call
Initialize()on each, allowingInitializeDependency<T>()for ordered resolution - Call
PostInitialize()after all subsystems are ready - Build a priority-sorted tick list for per-frame dispatch
Shutdown proceeds in reverse initialization order (LIFO), ensuring dependencies are torn down safely. Two subsystems ship with the engine: FInputSubsystem (priority 1000, processes input before gameplay) and FTickTaskManager (priority 500, dispatches component ticks after input).
Scene, GameObject and Component Framework
The gameplay framework follows a pure composition model. FGameObject is a final class (not inheritable) that serves as an entity container. All behavior differences are achieved by attaching FComponent subclasses. Every FGameObject automatically owns a built-in FTransformComponent providing position, rotation, and scale through the engine’s FTransform math type.
Component Lifecycle
Components follow a two-phase initialization pattern inspired by both Unity and Unreal:
stateDiagram-v2
[*] --> OnAttach: AddComponent
OnAttach --> BeginPlay: Scene has begun play
BeginPlay --> Update: Each frame if enabled
Update --> OnDetach: RemoveComponent / Destroy
OnAttach --> OnDetach: Removed before BeginPlay
OnDetach --> [*]
note right of Update: Called every frame by TickSystem
OnAttach() fires immediately when a component is added to a FGameObject, used for self-initialization with no cross-component dependencies. BeginPlay() fires once before the first Update(), at which point sibling components are guaranteed to be attached and safe to query via GetComponent<T>(). This separation prevents initialization order bugs that commonly arise in composition-based architectures.
FScene: Object Container
FScene owns all FGameObject instances and drives their lifecycle. It maintains a render component registry (populated automatically by FRenderComponent::OnAttach) that decouples the render traversal from the update traversal. Objects marked for destruction are cleaned up at frame end through deferred destruction, preventing iterator invalidation during gameplay logic.
FSceneManager handles safe scene transitions using double-buffering: LoadScene() stores the new scene as pending, and the actual swap happens at the next frame boundary, ensuring no mid-tick disruption.
Tick System
I implemented a deterministic tick scheduling system as an engine subsystem (FTickTaskManager). The system organizes component updates into three ordered tick groups with hard synchronization barriers between them:
| Tick Group | Purpose | Example |
|---|---|---|
| TG_PreUpdate | Input results, AI decisions | Input processing |
| TG_Update | Gameplay logic, movement | FArcadeMovementComponent |
| TG_PostUpdate | Camera follow, UI sync, cleanup | FArcadeBoundsClampComponent |
Prerequisite Dependencies
Within each tick group, components can declare ordering constraints via AddPrerequisite(). The system performs DFS cycle detection when prerequisites are added, rejecting circular dependencies at registration time rather than deadlocking at runtime. Cross-group prerequisites are ignored since group ordering already provides the guarantee.
Parallel Dispatch
The tick system integrates with the engine’s async task infrastructure (FThreadPool + FTaskGraph). In multi-threaded mode, tick functions within a group are submitted as tasks to the FTaskGraph, with prerequisite relationships mapped to task dependencies for automatic parallel scheduling. A single-threaded fallback uses Kahn’s algorithm for topological sort, ensuring deterministic behavior on all platforms.
FComponentTickFunction bridges the tick system to the component framework. When a component sets bCanEverTick = true, a tick function is automatically created and registered on attach. The tick function calls BeginPlay() (if not yet called) then Update(), supporting configurable TickInterval for components that do not need per-frame updates.
Enhanced Input System
The Enhanced Input plugin (EnhancedInput.eplugin) is the engine’s first plugin module, loaded at PostEngineInit phase. I built it as a complete input processing pipeline modeled after Unreal Engine 5’s Enhanced Input system, featuring composable modifiers, stateful triggers, and runtime-switchable mapping contexts.
Pipeline Flow
flowchart LR
A[Physical Key Event] --> B[FInputMessageBridge]
B --> C[FInputSubsystem SetKeyState]
C --> D[Process Resolved Mappings]
D --> E[Apply Modifiers]
E --> F[Evaluate Triggers]
F --> G[State Machine]
G --> H[Emit Events]
H --> I[Fire Bound Callbacks]
Core Abstractions
FInputAction defines an abstract action with a value type (Boolean, Axis1D, Axis2D, Axis3D) and optional action-level modifiers and triggers. FInputMappingContext groups key-to-action mappings that can be activated or deactivated at runtime, each mapping carrying its own per-mapping modifiers and triggers.
FInputSubsystem (an ISubsystem at priority 1000) manages the full pipeline. When mapping contexts change, it rebuilds a flattened mapping table that merges context-level and action-level modifiers/triggers. Each frame, it iterates resolved mappings, applies the modifier chain to transform raw input values, evaluates the trigger state machine, and dispatches events to bound callbacks through the engine’s TDelegate system.
Built-in Modifiers and Triggers
Modifiers transform input values in the pipeline: FInputModifierNegate (selective axis negation), FInputModifierScalar (scale by vector), FInputModifierDeadZone (axial/radial dead zones), FInputModifierSwizzleAxis (axis remapping), and FInputModifierScaleByDeltaTime (frame-rate independent scaling).
Triggers implement a state machine that transitions between None, Ongoing, and Triggered states: FInputTriggerDown (fires every frame while held), FInputTriggerPressed (fires once on press), FInputTriggerReleased (fires once on release), and FInputTriggerHold (fires after a configurable hold duration).
ASCII Renderer
The AsciiRenderer module provides a character-cell rendering backend using the Strategy pattern. IAsciiRenderBackend defines the abstract interface for console output, with two concrete implementations: ClassicConsoleBackend (Win32 WriteConsoleOutput, 16-color palette) and VTConsoleBackend (ANSI escape sequences, 256-color support). The Auto mode detects VT capability at startup and falls back to Classic on older terminals.
FAsciiSpriteComponent extends FRenderComponent to draw filled rectangles of ASCII characters. It reads position from the owning FGameObject’s transform and submits draw commands to the active renderer each frame. Properties include DisplayChar, foreground/background colors (FColor), dimensions, and ZOrder for draw ordering. The render traversal is decoupled from the update loop through FScene’s render component registry, which only iterates registered render components and skips inactive owners.
EnigmaArcade: Putting It All Together
EnigmaArcade is the example game project that demonstrates the full engine stack working in concert. It implements a simple arcade game where a player character (rendered as an ASCII sprite) can be moved and resized using keyboard input, with two switchable input modes.
The game creates a FScene containing a single FGameObject with three components attached:
FAsciiSpriteComponentrenders the player as a colored character rectangleFArcadeMovementComponent(ticking inTG_Update) applies velocity to the transform each frame, then resets velocity to zeroFArcadeBoundsClampComponent(ticking inTG_PostUpdate) clamps position to framebuffer bounds after movement has been applied
Two FInputMappingContext instances define the input schemes. The Move context maps WASD to continuous movement using FInputTriggerDown, with FInputModifierNegate and FInputModifierSwizzleAxis to convert individual key presses into proper 2D directional vectors (Y-up coordinate convention). The Resize context maps the same keys but uses FInputTriggerPressed for discrete, one-shot size adjustments. TAB switches between contexts at runtime by removing one and adding the other to FInputSubsystem, demonstrating hot-swappable input configurations.
Design Philosophy
Composition Over Inheritance — FGameObject is final and cannot be subclassed. All behavior differences are achieved through component attachment. This eliminates deep inheritance hierarchies and makes entity capabilities fully dynamic at runtime.
Cross-DLL Safety by Design — The engine avoids std::type_index and RTTI for cross-module type identification. Instead, every subsystem, component, and module provides a static string name used as the registry key. This ensures reliable type lookup across DLL boundaries where virtual tables and type info may differ.
Deterministic Frame Ordering — The tick group system (PreUpdate, Update, PostUpdate) with intra-group prerequisite dependencies guarantees a predictable execution order. Input is always processed before gameplay logic, and post-processing (camera, UI, bounds clamping) always runs after movement, eliminating an entire class of frame-ordering bugs.
Pipeline Composability — The Enhanced Input system treats modifiers and triggers as composable building blocks. The same physical key can produce different logical behaviors by swapping modifier chains (negate, scale, swizzle, dead zone) and trigger types (down, pressed, released, hold) without changing any callback code.
Zero-Cost Opt-In — Components that do not set bCanEverTick = true incur zero tick overhead. No tick function is created, no registration occurs, and the component only participates in the BeginPlay/OnDetach lifecycle. This keeps the tick system lean even with hundreds of non-ticking components in a scene.