Game Framework¶
Summary: Project Eternal uses a hierarchical game framework with GameMode for session configuration, GameInstance for subsystem management, and GameState for replicated game-wide data. The architecture emphasizes subsystem-based services organized by scope.
Table of Contents¶
- Framework Philosophy
- Three-Tier Hierarchy
- Persistence Provider Selection
- Subsystem Architecture
- Initialization Flow
- Source Reference
- Related Systems
- Recent Changes
Framework Philosophy¶
Why This Structure?¶
The game framework separates concerns by lifetime and scope:
+------------------------------------------------------------------+
| LIFETIME HIERARCHY |
+------------------------------------------------------------------+
| |
| UEternalGameInstance |
| +-- Lives: Entire game session |
| +-- Scope: All levels, all players |
| +-- Use for: Subsystems, cross-level state |
| |
| AEternalGameMode (Server Only) |
| +-- Lives: Per level |
| +-- Scope: Server-side level configuration |
| +-- Use for: Spawning rules, class defaults |
| |
| AEternalGameState (Replicated) |
| +-- Lives: Per level |
| +-- Scope: All clients see this |
| +-- Use for: Game-wide managers, shared state |
| |
+------------------------------------------------------------------+
Design Decisions¶
| Decision | Rationale |
|---|---|
| Minimal GameMode | Most logic belongs in GameState (replicated) or Subsystems (persistent) |
| Server-authoritative item spawning | Prevents duplication exploits |
| GameInstance subsystems | Services persist across level transitions |
| Components on GameState | Quest/POI managers are game-wide, need replication |
Three-Tier Hierarchy¶
GameInstance (Session Lifetime)¶
Purpose: Persists across level transitions. Manages subsystem lifecycle and loading screens.
UEternalGameInstance
|
+-- Manages loading screen transitions
| +-- BeginLoadingScreen() on map load start
| +-- EndLoadingScreen() on map load complete
|
+-- Owns GameInstance Subsystems (auto-created)
+-- UEternalApiSubsystem
+-- UEternalUISubsystem
+-- UCraftingSubsystem
+-- UItemRegistrySubsystem
+-- UModifierSubsystem
+-- UCameraObstructionSubsystem
+-- UBalanceSubsystem
+-- UDungeonSubsystem
+-- UWorldMapSubsystem
+-- UEnvironmentSubsystem
| Responsibility | Implementation |
|---|---|
| Loading screens | Delegates bound to PreLoadMap / PostLoadMapWithWorld |
| Subsystem lifecycle | Automatic via UE's subsystem framework |
| Session persistence | Survives level transitions |
| Pending preset carrier | Holds a menu-selected curated character preset (and its start-node override) across OpenLevel() |
Pending Preset Carrier¶
When the menu selects a curated character preset (demo / "play as" build), that choice must survive the
OpenLevel() into the gameplay map. The GameInstance is the only object that outlives the level change, so it
carries the selection until the new pawn's persistence component picks it up.
| Member | Type | Purpose |
|---|---|---|
PendingPreset |
UPROPERTY(Transient) TObjectPtr<UPresetCharacterAsset> |
Menu-selected preset awaiting load on the next gameplay map |
SetPendingPreset(Preset) |
setter | Menu stores the selection before travel |
GetPendingPreset() |
getter | Consumed by UPersistenceComponent::InitializeProvider() |
PendingStartNodeOverride |
FName |
World-map node the run should start at, overriding the preset's own start node |
SetPendingStartNodeOverride(NodeID) / GetPendingStartNodeOverride() |
setter / getter | Set by the main-menu controller alongside the preset; read by UPersistenceComponent when resolving the start node |
A null PendingPreset means "use the normal save" (local / remote). When set, it is the highest-priority input
to provider selection. A NAME_None override means "use the preset's own start node". See
Persistence for how the preset becomes a read-only provider.
Both fields are GameInstance-lifetime run state, so both need explicit clearing — nothing else outlives the
travel to reset them. That clearing lives in the return-to-preset-select flow:
UEternalUISubsystem::ReturnToPresetSelect() resets PendingPreset and PendingStartNodeOverride, and
clears the world map, before travelling back to the menu map. The travel itself is non-seamless, so
PC/PS/pawn/ASC — and therefore abilities, equipment, attributes and inventory — reset by construction.
| Member | Type | Purpose |
|---|---|---|
bRouteToCharacterSelectOnMenuLoad |
bool on UEternalUISubsystem |
Set by ReturnToPresetSelect(); the flag that lands the player on preset select rather than the main menu root |
ConsumeRouteToCharacterSelect() |
read-and-clear | Consumed once by MenuGameMode after the menu map loads |
GameMode (Per-Level, Server Only)¶
Purpose: Minimal configuration for player spawning and character class defaults.
AEternalGameMode
|
+-- CharacterClassInfo
+-- Default attributes per class
+-- Startup abilities per class
+-- Animation configurations
| Property | Type | Purpose |
|---|---|---|
CharacterClassInfo |
UCharacterClassInfo* |
Character class configurations |
Why so minimal? GameMode runs only on server and doesn't replicate. Gameplay logic that clients need belongs in GameState.
Auto-load Travel Gate¶
The first player's world-map travel must not start until persistence has settled, because an auto-load (a
read-only preset, or bLoadOnPIEStart) restores the saved/preset world-map node that travel reads as its target.
HandleStartingNewPlayer therefore consults the pawn's UPersistenceComponent and gates TravelToCurrentNode()
on the scheduled load:
HandleStartingNewPlayer
|
+-- Persistence->WillAutoLoad() && authority?
| |
| +-- no -> TravelToCurrentNode() immediately (start node)
| |
| +-- yes -> HasCompletedInitialLoad()?
| +-- yes -> TravelToCurrentNode() now
| +-- no -> bind OnLoadCompleted -> OnPlayerPersistenceLoaded -> TravelToCurrentNode()
An abandoned auto-load (persistence disabled, no character id, or no provider) still releases the gate: the
component fires OnLoadCompleted(false), so OnPlayerPersistenceLoaded runs TravelToCurrentNode() and startup
never stalls waiting for a load that will never happen. Full provider/scheduling detail lives in
Persistence.
GameState (Per-Level, Replicated)¶
Purpose: Server-authoritative game-wide state. Owns managers that all clients can query.
AEternalGameState
|
+-- QuestManager (Component)
| +-- Tracks all active quests
| +-- Manages quest state transitions
|
+-- WorldPOIManager (Component)
| +-- Points of interest for automap
| +-- Discovery state per player
|
+-- Server RPCs
+-- Server_RequestDropItem()
+-- Server_RequestSpawnItem()
| Method | Purpose |
|---|---|
Server_RequestDropItem() |
Server-authoritative item dropping |
Server_RequestSpawnItem() |
Server-authoritative item creation |
GetQuestManagerComponent() |
Access to quest system |
GetWorldPOIManagerComponent() |
Access to POI/automap system |
Persistence Provider Selection¶
Concept-level touchpoint only. The provider classes, save/load lifecycle, identity, and preset assets are documented in Persistence.
The persistence component on the player picks its provider from EPersistenceProviderMode (Project Settings >
Game > Persistence (Dev)) combined with the GameInstance's pending preset:
| Mode | Behaviour |
|---|---|
Auto |
Local file provider in Standalone, Remote server provider on a server |
Local |
Always the local file provider |
Remote |
Always the remote server provider |
Preset |
Always load the configured read-only DefaultPreset |
Selection precedence (first match wins):
PendingPreset (menu selection) -> read-only preset provider
ProviderMode == Preset -> read-only preset provider (DefaultPreset)
packaged build && bAutoLoadPresetInPackagedBuild && DefaultPreset valid
-> read-only preset provider
otherwise -> normal Local / Remote save provider
Preset providers are read-only (IPersistenceProvider::IsReadOnly() == true): Save/Delete are no-ops and the
auto-save timer is disabled, so a curated demo build can never overwrite its source data. The GameInstance's
PendingPreset is the runtime override consumed by UPersistenceComponent::InitializeProvider().
Subsystem Architecture¶
Subsystem Lifetime Model¶
+-------------------------------------------------------------------+
| SUBSYSTEM LIFETIMES |
+-------------------------------------------------------------------+
| |
| GameInstance Subsystems LocalPlayer Subsystems |
| (One per game session) (One per local player) |
| |
| +-------------------------+ +-------------------------+ |
| | UEternalApiSubsystem | | UEternalInputSubsystem | |
| | UEternalUISubsystem | +-------------------------+ |
| | UCraftingSubsystem | |
| | UItemRegistrySubsystem | |
| | UModifierSubsystem | |
| | UCameraObstructionSub. | |
| | UBalanceSubsystem | |
| | UDungeonSubsystem | |
| | UWorldMapSubsystem | |
| | UEnvironmentSubsystem | |
| +-------------------------+ |
+-------------------------------------------------------------------+
GameInstance Subsystems¶
| Subsystem | Purpose | Key Methods |
|---|---|---|
UEternalApiSubsystem |
Backend API communication | GetAccountService(), GetCharacterService(), GetPersistenceService() |
UEternalUISubsystem |
MVVM UI management | GetHUDController(), GetInventoryController() |
UCraftingSubsystem |
Recipe matching | FindRecipeByID(), FindMatchingRecipe() |
UItemRegistrySubsystem |
Cross-container item lookup | FindItemByInstanceId(), MoveItemToContainer() |
UModifierSubsystem |
Modifier pool management | Pool loading and modifier generation |
UCameraObstructionSubsystem |
Camera visibility | Actor fade/obstruction management |
UBalanceSubsystem |
Enemy stat scaling | CalculateScaledStats(), GetThreatTierMultipliers() |
UDungeonSubsystem |
Dungeon generation and runtime state | GenerateDungeon(), TryEnterRoom(), MarkRoomCleared() |
UWorldMapSubsystem |
World map node navigation and domains | TravelToNode(), MarkNodeCleared(), LiberateDomain() |
UEnvironmentSubsystem |
Domain-specific lighting sublevels | LoadLighting(), LoadLightingForDomain() |
LocalPlayer Subsystems¶
| Subsystem | Purpose | Key Methods |
|---|---|---|
UEternalInputSubsystem |
Input processing, delegates to player interaction component | BindInputActions(), HandleAbilityInputPressed(), HandleInteract() |
Accessing Subsystems¶
GameInstance Subsystems:
LocalPlayer Subsystems:
Initialization Flow¶
Startup Sequence¶
Game Launch
|
v
+-----------------------------------+
| UEternalGameInstance::Init() |
| - Register loading screen hooks |
+-----------------------------------+
|
v
+-----------------------------------+
| Subsystems Auto-Initialize |
| (in dependency order) |
| |
| UEternalApiSubsystem |
| +-- Set default BaseUrl |
| +-- Create API services |
| |
| UEternalUISubsystem |
| +-- LoadUIConfig() |
| +-- CreateControllers() |
| |
| UCraftingSubsystem |
| +-- LoadAllRecipeAssets() |
| |
| UItemRegistrySubsystem |
| +-- Initialize container map |
| |
| UModifierSubsystem |
| +-- LoadPoolAssets() |
| |
| UDungeonSubsystem |
| +-- Create Generator |
| |
| UWorldMapSubsystem |
| +-- (Ready for map loading) |
| |
| UEnvironmentSubsystem |
| +-- Bind dungeon/map events |
+-----------------------------------+
|
v
+-----------------------------------+
| Per-LocalPlayer Initialization |
| |
| UEternalInputSubsystem |
| +-- LoadInputConfig() |
| +-- Cache input references |
+-----------------------------------+
|
v
+-----------------------------------+
| World/Level Load |
| |
| AEternalGameState Constructor |
| +-- Create QuestManager |
| +-- Create WorldPOIManager |
| |
| AEternalGameMode Loaded |
| +-- CharacterClassInfo ready |
+-----------------------------------+
Subsystem Dependency Order¶
Subsystems can declare dependencies to ensure proper initialization order:
| Subsystem | Depends On |
|---|---|
UEternalApiSubsystem |
(none) |
UEternalUISubsystem |
(none) |
UCraftingSubsystem |
UModifierSubsystem (for modifier lookup) |
UItemRegistrySubsystem |
(none) |
UBalanceSubsystem |
(none) |
UDungeonSubsystem |
(none) |
UWorldMapSubsystem |
UDungeonSubsystem (triggers dungeon generation) |
UEnvironmentSubsystem |
UDungeonSubsystem, UWorldMapSubsystem (binds to their events) |
Source Reference¶
| Topic | File | Line |
|---|---|---|
| GameInstance Definition | Source/ProjectEternal/Public/GameMode/EternalGameInstance.h |
1-50 |
| GameInstance Implementation | Source/ProjectEternal/Private/GameMode/EternalGameInstance.cpp |
1-40 |
| Pending preset carrier | Source/ProjectEternal/Public/GameMode/EternalGameInstance.h |
(file) |
| Auto-load travel gate | Source/ProjectEternal/Private/GameMode/EternalGameMode.cpp |
(file) |
| GameMode Definition | Source/ProjectEternal/Public/GameMode/EternalGameMode.h |
1-30 |
| GameState Definition | Source/ProjectEternal/Public/GameMode/EternalGameState.h |
1-60 |
| GameState Implementation | Source/ProjectEternal/Private/GameMode/EternalGameState.cpp |
1-80 |
| UI Subsystem | Source/ProjectEternal/Public/UI/EternalUISubsystem.h |
1-100 |
| Input Subsystem | Source/ProjectEternal/Public/Input/EternalInputSubsystem.h |
1-80 |
| Crafting Subsystem | Source/ProjectEternal/Public/Crafting/Subsystems/CraftingSubsystem.h |
1-50 |
| Item Registry | Source/ProjectEternal/Public/Inventory/SubSystems/ItemRegistrySubsystem.h |
1-50 |
| Dungeon Subsystem | Source/ProjectEternal/Public/Dungeon/DungeonSubsystem.h |
1 |
| World Map Subsystem | Source/ProjectEternal/Public/WorldMap/WorldMapSubsystem.h |
1 |
| Environment Subsystem | Source/ProjectEternal/Public/Environment/EnvironmentSubsystem.h |
1 |
Related Systems¶
- Core Architecture - Project structure and conventions
- Character Framework - Character and controller hierarchy
- Persistence - Provider selection, save/load lifecycle, and curated presets
- UI Architecture - MVVM framework details
- Crafting System - Crafting subsystem details
Recent Changes¶
| Date | Change | Impact |
|---|---|---|
| 2026-08-06 | Documented PendingStartNodeOverride as the second GameInstance run-state field, its clearing path (UEternalUISubsystem::ReturnToPresetSelect), and the bRouteToCharacterSelectOnMenuLoad / ConsumeRouteToCharacterSelect() one-shot |
Both run-state fields need explicit clearing since nothing else outlives the travel; the consume-once flag is the mechanism that lands the player on preset select instead of the main menu root |
| 2026-06-17 | Documented GameInstance PendingPreset carrier, the GameMode auto-load travel gate, and persistence provider-selection precedence (demo-preset cluster) |
GameInstance/GameMode persistence touchpoints; cross-references new Persistence doc |
| 2026-02 | Added UDungeonSubsystem, UWorldMapSubsystem, UEnvironmentSubsystem to docs | Three GI subsystems were undocumented |
| 2026-02 | Added UBalanceSubsystem | JSON-driven enemy stat scaling |
| 2026-01-19 | AEternalPlayer now owns UPlayerInteractionComponent; HandleInteract() delegates to it | Input handling refactored to player-owned component |
| - | Initial documentation | - |