Enigma Engine is a C++26 game engine built around modular architecture, runtime extensibility, and an Unreal-style API. It includes runtime DLL module loading with dependency resolution, a subsystem architecture for pluggable engine services, a composition-based scene and component framework with deterministic tick scheduling, and an Enhanced Input plugin with a modifier and trigger pipeline. A companion build toolchain written in C# .NET handles project scaffolding, CMake generation, Visual Studio solutions, and multi-configuration builds from Debug through Shipping.
EnigmaArcade demo
Engine architecture overview
The engine is organized into layered runtime modules, each compiled as a separate DLL with explicit dependency declarations. FEngineLoop drives the frame loop, FEngine owns the global FSubsystemCollection, and FGameInstance hosts the user-programmable game loop and 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 an ordered 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, and PostUpdate), and FGameInstance drives scene updates and rendering.
Module and plugin system
The cross-DLL module system follows Unreal Engine’s module architecture. Each module (Core, Engine, Launch, AsciiRenderer, game modules, and plugins) compiles into its own DLL with API export macros. Modules self-register during static initialization through the IMPLEMENT_MODULE macro, which links a factory function to a global module list at load time without needing a central manifest file.
FModuleManager manages the lifecycle: it calls LoadLibrary to load each DLL, locates the registered factory, instantiates IModuleInterface, and calls StartupModule(). Shutdown runs in reverse order (LIFO). Five loading phases (EarliestPossible through PostEngineInit) allow plugins to declare when they initialize during engine boot.
Plugins use .eplugin JSON descriptors to declare module names, types, loading phases, and dependencies. At build time, the BuildTool resolves the dependency graph and generates CMake link order and DLL copy rules.
BuildTool (C# .NET 9)
The build toolchain is a standalone C# CLI for project scanning, dependency resolution, CMake generation, compilation, 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) |
Scaffolding commands (create-project, create-module, create-plugin) generate boilerplate projects, module descriptors, export headers, and CMake files.
Subsystem architecture
The subsystem architecture adapts Unreal’s USubsystem model for a non-UObject, cross-DLL environment. The base interface is ISubsystem, which provides lifecycle hooks (Initialize, PostInitialize, Tick, Deinitialize) and per-frame ticking through IsTickable() and GetTickPriority().
FSubsystemCollection manages subsystem lifecycles using string keys from each subsystem’s static GetStaticName(). This avoids std::type_index, which can disagree across DLL boundaries on Windows. Initialization runs in four steps:
- Filter subsystems with
ShouldCreateSubsystem()for conditional creation - Call
Initialize()on each, allowingInitializeDependency<T>()for ordered setup - Call
PostInitialize()after all subsystems are ready - Build a priority-sorted tick list for per-frame dispatch
Shutdown runs in reverse initialization order (LIFO) so dependent services remain available during teardown. The engine includes two built-in subsystems: FInputSubsystem (priority 1000, runs before gameplay) and FTickTaskManager (priority 500, runs after input to dispatch component ticks).
Scene, GameObject, and component framework
The gameplay framework uses a pure composition model. FGameObject is marked final and acts strictly as an entity container, with behavior implemented entirely through attached FComponent subclasses. Every FGameObject includes a built-in FTransformComponent that stores position, rotation, and scale in an FTransform.
Component lifecycle
Components follow a two-phase initialization pattern:
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 an FGameObject, handling self-contained setup that does not depend on sibling components. BeginPlay() runs once before the first Update(), when all sibling components are attached and can be queried with GetComponent<T>(). This two-step sequence avoids ordering bugs during entity assembly.
FScene object container
FScene owns all FGameObject instances and drives their lifecycle. It registers render components during FRenderComponent::OnAttach, separating render traversal from the update loop. Objects marked for destruction are cleaned up at the end of the frame through deferred deletion, preventing iterator invalidation during gameplay logic.
FSceneManager handles transitions through double buffering: LoadScene() sets the incoming scene as pending, and the swap occurs at the next frame boundary so the active scene is not replaced mid-tick.
Tick system
Component updates run through FTickTaskManager, which sorts work into three tick groups separated by synchronization barriers:
| 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 a tick group, components can declare ordering constraints with AddPrerequisite(). The manager runs DFS cycle detection as prerequisites are added, catching circular dependencies during registration instead of hanging at runtime. Cross-group prerequisites are ignored because the tick groups already enforce that order.
Parallel dispatch
The tick system connects to the engine task graph (FThreadPool and FTaskGraph). In multithreaded mode, tick functions within a group run as tasks on the FTaskGraph, with prerequisites mapped directly to task dependencies. In single-threaded mode, a fallback uses Kahn’s algorithm for topological sorting, keeping execution order deterministic.
FComponentTickFunction connects the tick system to components. When a component sets bCanEverTick = true, OnAttach creates and registers a tick function. The tick function runs BeginPlay() once, then calls Update() on subsequent frames. Components can also specify a TickInterval if they do not need to tick every frame.
Enhanced Input system
The Enhanced Input plugin (EnhancedInput.eplugin) loads during the PostEngineInit phase. Modeled after Unreal Engine 5’s input architecture, it processes raw input through 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 action with a value type (Boolean, Axis1D, Axis2D, or Axis3D) along with optional action-level modifiers and triggers. FInputMappingContext groups key-to-action bindings that can be activated or deactivated at runtime, with each mapping supporting its own modifiers and triggers.
FInputSubsystem manages the pipeline. When mapping contexts change, it flattens the active contexts and actions into a lookup table. Every frame, it reads the resolved mappings, applies modifier chains to transform raw input values, evaluates trigger state machines, and dispatches events through TDelegate callbacks.
Built-in modifiers and triggers
Modifiers alter input values before triggers evaluate them:
FInputModifierNegate: negates selected axesFInputModifierScalar: scales axes by a vectorFInputModifierDeadZone: applies axial or radial dead zonesFInputModifierSwizzleAxis: remaps axesFInputModifierScaleByDeltaTime: scales input by delta time for frame-rate independence
Triggers evaluate whether an action has fired, transitioning between None, Ongoing, and Triggered states:
FInputTriggerDown: fires every frame while a key is heldFInputTriggerPressed: fires once on pressFInputTriggerReleased: fires once on releaseFInputTriggerHold: fires once the input is held past a threshold duration
ASCII renderer
The AsciiRenderer module provides character-cell rendering for the terminal. IAsciiRenderBackend defines the interface for console output, backed by two implementations: ClassicConsoleBackend (Win32 WriteConsoleOutput with a 16-color palette) and VTConsoleBackend (ANSI escape sequences with 256 colors). An Auto mode checks for VT sequence support at startup and falls back to the Classic backend on older terminals.
FAsciiSpriteComponent inherits from FRenderComponent to draw filled character rectangles. It reads positions from the parent FGameObject transform and submits draw commands to the renderer each frame, supporting DisplayChar, foreground and background colors (FColor), dimensions, and a ZOrder for depth sorting. FScene maintains a dedicated render component registry, so rendering visits only registered visual components instead of traversing the whole scene graph.
EnigmaArcade sample game
EnigmaArcade is an example project that ties these systems together. It implements a small arcade demo where a player character moves and resizes in the terminal using keyboard input across two hot-swappable input modes.
The scene uses an FGameObject with three components:
FAsciiSpriteComponentrenders the player as a character rectangleFArcadeMovementComponent(running inTG_Update) applies velocity to the transform each frame, then clears itFArcadeBoundsClampComponent(running inTG_PostUpdate) clamps the transform to the console window after movement finishes
Two FInputMappingContext assets define the controls. The Move context maps WASD to continuous movement with FInputTriggerDown, using FInputModifierNegate and FInputModifierSwizzleAxis to turn key presses into 2D directional vectors in a Y-up coordinate space. The Resize context binds the same keys with FInputTriggerPressed for single-step size adjustments. Pressing TAB swaps the active context in FInputSubsystem at runtime.
Design decisions
Composition over inheritance
FGameObject is marked final and cannot be subclassed. Attaching components provides all behavior, which avoids deep inheritance hierarchies and allows dynamic changes to entity behavior at runtime.
Cross-DLL type safety
The engine avoids std::type_index and RTTI for cross-module type identification because virtual tables and type metadata can mismatch across DLL boundaries. Subsystems, components, and modules register with a static string name instead.
Deterministic frame ordering
Three tick groups (PreUpdate, Update, PostUpdate) enforce ordering across systems, and intra-group prerequisites order components within the same phase. Input is handled before gameplay logic, and post-processing like camera follow and bounds clamping runs after movement.
Composable input pipelines
Enhanced Input treats modifiers and triggers as reusable units. A single key binding can change from raw button presses to clamped, scaled, or swizzled directional vectors by changing the modifier chain, without rewriting callback handlers.
Conditional ticking
Components that leave bCanEverTick = false register no tick function and incur no per-frame dispatch overhead, participating only in BeginPlay and OnDetach.