Skip to main content

Advanced: Replication In-Depth

FactsDB is designed with multiplayer support as a first-class feature. Its architecture leverages standard, efficient Unreal Engine networking patterns to ensure that your game state is synchronized correctly and performantly.

The Replication Model

The core of the system follows a standard Server-Authoritative model.

  1. Changes are made on the Server: All modifications to replicated facts should occur on the server. The UFactsComponent::UpdateFact function will fail if called on a client for a replicated component.
  2. Server Updates Data: The server updates the FFactInstance within the ReplicatedFacts container (FFactContainer).
  3. Engine Replicates Changes: Unreal Engine's networking system, using the highly efficient FFastArraySerializer, detects the change and sends only the modified data (a "delta") to the relevant clients.
  4. Clients Receive Updates: On the client, the FFactContainer receives the new data and updates its local FFactInstance.
  5. Clients React to Changes: The replication process automatically triggers the OnFactValueChanged and OnGlobalFactValueChanged delegates on the client, allowing client-side systems (like UI) to react to the new state.

Controlling What Gets Replicated

You have two levels of control over replication: at the Component level and at the individual Fact level. They work together to determine the final behavior.

1. Component-Level: EFactReplicationType

This property on the UFactsComponent determines the overall replication strategy for its ReplicatedFacts container.

  • Global (Default)

    • Behavior: The component and its replicated facts are visible and sent to all connected clients.
    • Use Case: Data that everyone needs to know, such as a player's health, name, or position in a shared world. The state of world objects like doors or objectives.
  • OwnerOnly

    • Behavior: The component and its replicated facts are only sent to the owning client of the Actor. Other clients will not receive this data.
    • Use Case: Data that is only relevant to the player controlling the character, but still needs to be server-authoritative. Examples include a player's personal inventory, ammo count for their current weapon, or cooldown timers for their abilities. This is a crucial optimization for reducing network traffic.
  • Local

    • Behavior: The component does not replicate at all. All its facts, even those in the ReplicatedFacts container, are treated as local to the machine they are on.
    • Use Case: For purely cosmetic systems or client-side logic that doesn't need to be synchronized. For example, a client-side effect controller that uses facts to manage its state.

2. Fact-Level: bReplicate

This is a boolean property on the FFactDefinition struct within your UDataTable. It determines whether a fact is placed in the ReplicatedFacts container or the LocalFacts container upon initialization.

  • bReplicate = true (Default)
    • The fact will be placed in the ReplicatedFacts container. Its replication will then be governed by the component's EFactReplicationType.
  • bReplicate = false
    • The fact will be placed in the LocalFacts container. It will never be replicated, regardless of the component's settings.
    • Use Case: For temporary, intermediate values on the server that clients don't need, or for purely client-side state information within a replicated component.

How They Interact: A Decision Table

UFactsComponent ReplicationTypeFFactDefinition bReplicateResulting Behavior
GlobaltrueFact is replicated to all clients.
GlobalfalseFact is local to each machine (server/client). Not replicated.
OwnerOnlytrueFact is replicated to the owning client only.
OwnerOnlyfalseFact is local to each machine. Not replicated.
LocaltrueFact is local. ReplicationType overrides the fact's setting.
LocalfalseFact is local.

Initial Replication (OnInitialStateReceived)

When a client first becomes aware of a replicated UFactsComponent (e.g., when a player enters the network relevancy range of another player), the server sends a full initial state of all replicated facts.

Receiving these one by one and firing OnFactValueChanged for each can be inefficient and lead to "pop-in" effects on UI. To solve this, the UFactsComponent provides the OnInitialStateReceived delegate.

  • This delegate fires once on the client after the entire initial set of replicated facts has been received.
  • Individual OnFactValueChanged events are suppressed during this initial dump. They will only begin firing for subsequent changes.
Best Practice for Client UI

Always use OnInitialStateReceived to perform the initial setup of your UI. Bind to this event to populate all health bars, ammo counts, etc. Use OnFactValueChanged only for updating the UI after that initial setup is complete.

// In a client-side UI widget's BeginPlay or Initialize
void UMyPlayerHUD::InitializeForTarget(UFactsComponent* PlayerFactsComponent)
{
if (PlayerFactsComponent)
{
// Bind to both delegates
PlayerFactsComponent->OnInitialStateReceivedNative.AddUObject(this, &UMyPlayerHUD::HandleInitialState);
PlayerFactsComponent->OnFactValueChangedNative.AddUObject(this, &UMyPlayerHUD::HandleHealthChanged);
}
}

// Fired once when the component is first replicated to us.
void UMyPlayerHUD::HandleInitialState(UFactsComponent* SourceComponent)
{
// Do a full UI refresh here
UpdateHealthBar(SourceComponent);
UpdateAmmoCount(SourceComponent);
UpdateStatusEffectIcons(SourceComponent);
}

// Fired for every subsequent change after the initial state.
void UMyPlayerHUD::HandleHealthChanged(const FFactTag& FactTag, const FInstancedStruct& OldValue, const FInstancedStruct& NewValue)
{
if (FactTag == FGameplayTag::RequestGameplayTag(TEXT("Fact.Player.Stat.Health")))
{
// Do a targeted update for just the health bar
const float NewHealth = NewValue.Get<FFactFloat>().Value;
HealthBarWidget->SetPercent(NewHealth / 100.0f);
}
}