Networked Build Ghost
Proposed Factory Replication Iris

Networked Build Ghost

The build ghost is the preview of what a player is about to place, a Mass entity that shows its tiles green where they can be built and red where they cannot. This page makes every player see every other player's ghost, drawn from state the server replicates, while the placing player keeps a local ghost that follows the cursor with no round trip. The server never sends a player a copy of their own ghost.

Overview

  • The placing player drives a local ghost entity from the build cursor. It resolves its intent into tiles and draws the same frame. On a client, SetBuildGhostIntent also sends the intent to the server.
  • The server keeps one ghost entity per player in build mode and resolves it with DFBuildGhost::Resolve, the same function the local ghost and Server_Build use. The resolved fragment replicates through Horizon.
  • Every other player receives an ordinary Horizon mirror, drawn by the ghost's visualization processor.
  • The owner never receives its own ghost: an Iris exclusion group disallows the owner's connection.

One entity per ghost per process. The same holds with the roles swapped for C's ghost.

Filtering the Owner

When Horizon registers the server's ghost entity:

ReplicationSystem->SetOwningNetConnection(RepHandle, OwningConnectionId);
// FHorizonSkipOwnerTag only:
Group = ReplicationSystem->CreateGroup(NAME_None);
ReplicationSystem->AddExclusionFilterGroup(Group);
ReplicationSystem->SetGroupFilterStatus(Group, {OwningConnectionId}, ENetFilterStatus::Disallow);
ReplicationSystem->AddToGroup(Group, RepHandle);
// UnregisterEntity: DestroyGroup(Group)

The bit-array overload of SetGroupFilterStatus gives every other connection slot the opposite status, including slots nobody holds yet (ReplicationFiltering.cpp, UE 5.8). Players who join later see the ghost, and the owner never does:

connection 1
Player B, owner
Disallow
connection 2
Player C
Allow
connection 3
Player D
Allow
connection 4…N
Joins later
Allow
  • The fact: FHorizonOwningConnectionFragment { uint32 ConnectionId; }, added by the player's building handler when it spawns the ghost. It feeds SetOwningNetConnection, so Unreal's owner conditions on properties work too.
  • The policy: FHorizonSkipOwnerTag, on the ghost's trait, adds the group. Both are server-only and in the archetype when Horizon's registration observer runs, so nothing in Horizon knows about ghosts.
  • The id comes from PC->GetNetConnection()->GetConnectionHandle().GetParentConnectionId(). A listen host has no connection, so its ghost has no fragment and goes to every client.
  • One group per ghost, destroyed with the entity, so a reused connection slot never inherits it. Exclusion groups run before dynamic filters, so a spatial filter would still compose.

Opt-in Filtering

The same mechanism covers state only some players ask for. Example: a machine's exact inputs and outputs, sent only while a player has the entity inspector open on it. Everyone keeps the summary in FMachineRepStateFragment, the slot states and output part per slot. The detail uses a per-entity exclusion group with the opposite default.

Skip owner (ghost)Allowed connections (machine detail)
FactOne connection, fixed at spawnThe players inspecting it, changing as inspectors open and close
Unlisted connectionAllowedDisallowed
Other usesOwner-only filter, property conditionsNone, so no tag
  • The detail is its own entity, because Iris filters whole objects and a machine has one proxy. FMachineDetailRepStateFragment holds each slot's inputs and output with their counts, plus the machine's network id. The server spawns it when the first player inspects the machine and destroys it when the last one closes, so uninspected machines cost nothing.
  • A server processor fills it from the machine's own fragments while it exists. The machine never learns it is being inspected.
  • Inspecting is a player action: Server_InspectEntity(NetID) and Server_StopInspecting() raise a request. A processor applies it to the detail entity's FHorizonAllowedConnectionsFragment, a bitmask of connection slots. Logging out stops inspecting, and Horizon clears a closing connection from every set.
  • The inspector shows the summary until the detail mirror arrives, about one round trip, and never assumes it exists. Closing destroys the mirror, so reopening resends it.

The two facts stay separate rather than one client list with an allow or deny switch. Horizon's group helper takes a default status and per-connection overrides, and can apply them again after registration. The ghost uses only the default-Allow case, set once.

The Change

  • What replicates: FBuildGhostFragment becomes FBuildGhostRepStateFragment, the same shape: machine type, tiles, valid bits, joins and revision. Viewers draw what the server resolved and never run Resolve themselves. Everyone sees the same colours, red tiles included.
  • RPCs: Server_UpdateBuildGhost(Intent, Surface), unreliable, sent on change at most 20 Hz, spawns or updates the server ghost. Server_ClearBuildGhost() is reliable. Both validate the type, cell count and surface. Server_Build re-resolves its own intent and never reads the server ghost.
  • Teardown: a Horizon remove observer on FHorizonNetIDFragment unregisters every replicated entity on destroy. Explicit UnregisterEntity calls become harmless repeats. Tear-off callers keep theirs, since only they know the reason.
ProcessOwn ghostOthers' ghosts
StandaloneLocal entity, drawnnone
Listen hostThe server entity, drawnServer entities, drawn
Dedicated servernoneOne per player, not drawn
ClientLocal entity, not registered, drawnMirrors, drawn