Skip to content

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

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:

GameInstance->GetSubsystem<UEternalUISubsystem>()
GameInstance->GetSubsystem<UCraftingSubsystem>()

LocalPlayer Subsystems:

LocalPlayer->GetSubsystem<UEternalInputSubsystem>()


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


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 -