Skip to main content

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. Using TWeakObjectPtr 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 the FactsDBUncooked and FactsDB 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 its FFactSchemaTag. The component's BeginPlay logic is responsible for initializing its data from the schema and registering itself with the UFactRuntimeSubsystem. 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 repeated GetFact calls, reducing the cost of map lookups in the underlying FFactContainer.

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 from FFastArraySerializer. 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 whole TArray.
  • 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-replicated TMap<FFactTag, int32> FactIndices. This map provides an O(1) average-time lookup to find the index of any FFactInstance in the Items array. This map is built locally on the server and reconstructed on clients in the PostReplicatedAdd and PreReplicatedRemove 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 (from FFactFloat to a complex user struct) in a single, type-safe container without resorting to slower UObject 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 their CustomThunk functions handle the "packing" and "unpacking" of data between native Blueprint pin types (like float or int) and the internal FInstancedStruct wrappers (like FFactFloat or FFactInteger).

The Data Lifecycle

  1. Editor-Time: A programmer or designer creates a UDataTable with FFactDefinition rows and registers it in UFactsDBRuntimeSettings.
  2. PIE/Startup: The UFactSchemaManager loads all registered UDataTables, resolves schema inheritance, and builds a cached, in-memory representation of all schemas.
  3. BeginPlay: A UFactsComponent on an Actor wakes up. It asks the UFactRuntimeSubsystem (via the data provider) for the resolved data of its assigned SchemaUsed.
  4. Initialization: The component populates its FFactContainers with FFactInstances, one for each definition in the schema, using the default values.
  5. Registration: The component calls RegisterFactComponent on the subsystem, adding itself to the global registry with its FFactContextID.
  6. Runtime:
    • A Blueprint calls Update Fact by Tag. This finds the component via the subsystem, updates the FFactInstance in the FFactContainer, 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.

Next Up: C++ API and Usage