# Multiplayer Guide

A deep dive into how AP StatusFX Suite handles replication, authority, and all the edge cases that make multiplayer status effects hard to get right.


# Architecture at a Glance

┌─────────────────────────────────────────────┐
│                  SERVER                      │
│                                             │
│  ApplyEffect()  ──►  StatusFXComponent      │
│  RemoveEffect()       (authoritative state) │
│  RefreshEffect()             │              │
│  GrantImmunity()             │              │
│                              ▼              │
│              FastArray Delta Replication    │
└─────────────────────────────────────────────┘
                              │
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
         Client A          Client B       Late Join C
    (replicated state)  (replicated state)  (full state)
    HasEffect() ✓       HasEffect() ✓    HasEffect() ✓
    OnEffectApplied ✓   OnEffectApplied ✓  (no replay)

# The Golden Rules


# Replication: FastArray Delta Serialization

The active effects array uses Unreal's FFastArraySerializer — the same technique used by Fortnite's item inventory. Only changed effect entries replicate on each net update, not the entire array.

What this means in practice:

Scenario Network Cost
Apply 1 new effect 1 entry replicated
Update stack count on 1 effect 1 entry replicated
10 effects active, 1 changes 1 entry replicated
10 effects active, none change 0 bytes of effect data

# Late-Join State Reconstruction

When a client joins a server mid-game, Unreal's initial replication will deliver the full current state of the effects array. The component handles this automatically — the late joiner receives every active effect as if they had been there since the start.


# Applying Effects From a Client

Clients cannot call ApplyEffect directly. Use a Server RPC to route the request through the server:

Character.h
UFUNCTION(Server, Reliable)
void ServerApplyPoison(AActor* Target, UAP_StatusEffectDefinition* PoisonDef);
Character.cpp
void AMyCharacter::ServerApplyPoison_Implementation(
    AActor* Target,
    UAP_StatusEffectDefinition* PoisonDef)
{
    // Now on the server — safe to call ApplyEffect
    UAP_StatusFXBlueprintLibrary::ApplyEffectToActor(Target, PoisonDef, this);
}

In Blueprint, use a Server Event (Reliable, Run on Server) with the same pattern.


# Delegates: Who Gets What

Delegate Server Client Notes
OnEffectApplied Fires with full snapshot on both
OnEffectRemoved See note below on removal reason
OnEffectRefreshed  
OnEffectStackChanged ⚠️ Clients receive OnEffectRefreshed instead — bind both
OnEffectTick Server only — use bIsTicking from snapshot for client UI
OnEffectExpired Server only

# Client Removal Reason (v1.0 Limitation)

OnEffectRemoved fires on clients, but the EAP_EffectRemovalReason value will always be RemovedManually regardless of actual cause. The specific reason is server-side state not currently replicated. This is planned for v1.1.

If you need accurate removal reason on clients right now, send a custom Server → Client RPC after removal.

# Stack Changes on Clients

When a stack count changes on the server, OnEffectStackChanged fires on the server. Clients detect the array change through FastArray and fire OnEffectRefreshed instead. To handle stack display correctly on all machines:

// Bind BOTH of these on clients
StatusFXComponent->OnEffectStackChanged.AddDynamic(this, &UBuffBar::RefreshStackDisplay);
StatusFXComponent->OnEffectRefreshed.AddDynamic(this, &UBuffBar::RefreshStackDisplay);

# Immunity Across the Network

Immunity state replicates with the effects array. IsImmuneToEffect() reads replicated state and is accurate on both server and client. When immunity is granted:

  1. Server grants immunity via GrantImmunity() or an interaction/expiry rule.
  2. The immunity entry replicates to all clients.
  3. Clients can query IsImmuneToEffect() immediately — no RPC needed.

# Tick Effects in Multiplayer

OnEffectTick fires on the server only, driven by TimerManager. Clients do not receive tick callbacks directly.

Handling periodic effects on clients:

Option A — React to gameplay consequences: The server applies the actual damage/healing and those values replicate through your normal game systems (e.g., Health attribute). Clients just display the effect is active.

Option B — UI tick indicator: Read bIsTicking from the snapshot to show a pulsing indicator in your buff bar. The snapshot also provides TotalDuration and ServerTimeApplied so you can compute elapsed time client-side for animated effects.

Option C — Client tick simulation (planned for v1.1): The roadmap includes client-side tick simulation for cosmetic-only periodic callbacks.


# Dedicated Server vs. Listen Server

The component behaves identically on both:

Setup Notes
Dedicated Server Server has the component but no local player. All mutation calls happen on the server process. Clients bind delegates on their local copies.
Listen Server The host is both server and a client. Mutations happen on the host's authority context. Remote clients receive replication as normal.

# Testing Checklist

Use this checklist before shipping any status effect gameplay:

  • Applied an effect on the server and confirmed HasEffect() returns true on a connected client
  • Confirmed effect state is correct for a client that joined after the effect was applied (late-join test)
  • Confirmed OnEffectApplied fires on both server and client
  • Confirmed stack count is accurate on clients after multiple applications (GetStackCount query)
  • Tested effect expiry on a dedicated server — confirmed HasEffect() returns false on clients after expiry
  • Confirmed immunity blocks reapplication on both server and client (IsImmuneToEffect check)
  • Verified interaction rules (e.g., Frozen removes Burning) replicate correctly to clients
  • Simulated packet loss / high latency — effect state should still converge correctly

# Bandwidth Estimation

For rough planning purposes:

Scenario Approx. Replication Cost
Single timed effect applied ~64–128 bytes (initial)
Stack count update ~16–32 bytes (delta)
Duration refresh ~16–32 bytes (delta)
No changes (idle) 0 bytes

Actual sizes depend on your Gameplay Tag length, Instigator reference, and UE serialization overhead. Enable bEnableReplicationLogging in Project Settings during development to see exact traffic in the output log.


# Related Pages


AfterPrime Systems — Building the Gameplay Foundation