Runtime Architecture
This document provides a C++ programmer's perspective on the core runtime architecture of FactsDB. Understanding these components is key to extending the system, debugging complex issues, and integrating it effectively with other engine systems.
System Components Overview
The FactsDB runtime is built around a classic Unreal Engine Subsystem/Component pattern.
1. UFactRuntimeSubsystem
- The Global Manager
The UFactRuntimeSubsystem
is a UGameInstanceSubsystem
that acts as the central hub for all runtime operations.
- Component Registry: Its primary role is to maintain a
TMap<FFactContextID, TWeakObjectPtr<UFactsComponent>>
. This registry is the single source of truth for finding any active fact context in the game world. UsingTWeakObjectPtr
is crucial as it prevents the subsystem from keeping components alive and avoids issues with garbage collection. - Global Event Hub: It exposes global delegates, most importantly
OnGlobalFactValueChangedNative
. Game systems that need to react to any fact change, regardless of its context (e.g., a global UI system), should bind to this delegate. - Data Provider Interface: The subsystem does not load data directly. It delegates all requests for Schema and Query definitions to an
IFactsDBDataProvider
. This is a key design choice that allows the data source to be switched between the live editor managers (UFactSchemaManager
,UFactQueryManager
) during PIE and a single, cooked data asset (UFactsDBRuntimeData
) in a packaged build. This abstraction is managed by theFactsDBUncooked
andFactsDB
modules.
2. UFactsComponent
- The Data Container
The UFactsComponent
is an UActorComponent
that holds the live instance of a Schema's data.
- Identity: Its identity is defined by its
FFactContextID
and its structure is defined by itsFFactSchemaTag
. The component'sBeginPlay
logic is responsible for initializing its data from the schema and registering itself with theUFactRuntimeSubsystem
.EndPlay
handles unregistering. - Data Stores: A component contains two
FFactContainer
structs:ReplicatedFacts
: For facts that should be networked.LocalFacts
: For facts that are local to the machine and should not be replicated. This separation provides a clear and efficient way to control network traffic.
- Local Events: It exposes its own delegates (
OnFactValueChangedNative
) for systems that only care about changes within that specific component. This is more efficient than listening to the global subsystem delegate if your logic is self-contained within an Actor. - Caching: It includes an optional LRU cache (
FFactCache
) to speed up repeatedGetFact
calls, reducing the cost of map lookups in the underlyingFFactContainer
.
3. FFactContainer
- The Replicated Data Store
This struct is the workhorse of the replication system. It is not a USTRUCT
meant for direct Blueprint exposure, but rather an internal data structure.
FFastArraySerializer
:FFactContainer
inherits fromFFastArraySerializer
. This is the modern, standard Unreal Engine mechanism for efficiently replicating arrays of structs. It performs delta replication, meaning it only sends information about items that have been added, removed, or changed, drastically reducing bandwidth compared to replicating a wholeTArray
.- Fast Lookups: While the
Items
array is required for the serializer, linear searches would be too slow (O(n)). To solve this, the container also maintains a non-replicatedTMap<FFactTag, int32> FactIndices
. This map provides an O(1) average-time lookup to find the index of anyFFactInstance
in theItems
array. This map is built locally on the server and reconstructed on clients in thePostReplicatedAdd
andPreReplicatedRemove
callbacks. This combination gives us the best of both worlds: efficient replication and fast access.
4. FInstancedStruct
- The Polymorphic Value
The Value
property within an FFactInstance
is an FInstancedStruct
. This is a modern Unreal Engine feature that allows a USTRUCT
to hold a polymorphic instance of another USTRUCT
.
- Why it's used: It allows FactsDB to store any type of
USTRUCT
data (fromFFactFloat
to a complex userstruct
) in a single, type-safe container without resorting to slowerUObject
pointers or error-prone raw memory buffers. - Blueprint Interaction: In Blueprints, you never interact with
FInstancedStruct
directly. The custom K2Nodes (K2Node_GetFactByTag
,K2Node_UpdateFactByTag
, etc.) and theirCustomThunk
functions handle the "packing" and "unpacking" of data between native Blueprint pin types (likefloat
orint
) and the internalFInstancedStruct
wrappers (likeFFactFloat
orFFactInteger
).
The Data Lifecycle
- Editor-Time: A programmer or designer creates a
UDataTable
withFFactDefinition
rows and registers it inUFactsDBRuntimeSettings
. - PIE/Startup: The
UFactSchemaManager
loads all registeredUDataTables
, resolves schema inheritance, and builds a cached, in-memory representation of all schemas. BeginPlay
: AUFactsComponent
on an Actor wakes up. It asks theUFactRuntimeSubsystem
(via the data provider) for the resolved data of its assignedSchemaUsed
.- Initialization: The component populates its
FFactContainer
s withFFactInstance
s, one for each definition in the schema, using the default values. - Registration: The component calls
RegisterFactComponent
on the subsystem, adding itself to the global registry with itsFFactContextID
. - Runtime:
- A Blueprint calls Update Fact by Tag. This finds the component via the subsystem, updates the
FFactInstance
in theFFactContainer
, and marks the item dirty for replication. - The change is replicated to clients via the
FFastArraySerializer
mechanism. - On both server and client, the change triggers the component's local delegate and the subsystem's global delegate, notifying all listening systems.
- A Blueprint calls Update Fact by Tag. This finds the component via the subsystem, updates the
Next Up: C++ API and Usage