Why does a feature work in PIE single-player but break in multiplayer?
Item pickup works in PIE single-player. With two clients, the item vanishes only for the player who grabbed it (the other still sees it floating), and the inventory counter sometimes does not update on remote clients.
void AItem::OnPickup(APawn* Picker)
{
Picker->FindComponentByClass<UInventoryComponent>()->Count++;
Destroy();
}
// UInventoryComponent.h
UPROPERTY()
int32 Count = 0;
// UInventoryComponent.cpp — GetLifetimeReplicatedProps is not overridden
Find and fix the bug.
In single-player one machine is both server and client, so unreplicated state and client-side mutations just work. In multiplayer the server and clients are separate, exposing missing replication, wrong-authority writes, and ungated RPC calls.
- ✗Assuming code that works in PIE single-player is already network-correct
- ✗Writing gameplay state on the client because it appeared to work locally
- ✗Testing only with one player instead of PIE multiple-clients mode
- →How does PIE's multiple-clients mode help catch these bugs early?
- →Why is
HasAuthority()a key guard when porting single-player logic?
Scenario
Picking up an item works perfectly when testing PIE with a single player. Run PIE with two clients — the item disappears only for the player who grabbed it, while the second client still sees it floating in the world. Sometimes the inventory counter does not update at all.
Why single-player hid it
In PIE with one player the server and client are the same machine and the same UWorld. When "client" code changes a variable, it actually changes it on the "server" too — because it is one object. Unreplicated state looks synced because there is nothing to sync.
In multiplayer these are two separate processes. A change on the client stays on the client; the server never sees it and overwrites it on the next replication pass.
Diagnosis
the server/client split.
It turns out the item was destroyed on the client.
no DOREPLIFETIME registration.
- Run PIE → Number of Players: 2, Play As Client mode. This reproduces
- Wrap the pickup logic in
if (HasAuthority())and log which side runs it. - Inspect the inventory counter
UPROPERTY— it has noReplicatedflag and
Root cause
Destroy(), so the visible effect was purely local.
- The item destroy was called on the client — the client has no authority to
- The inventory counter was mutated on the client and never replicated.
Fix
void AItem::OnPickup(APawn* Picker)
{
if (!HasAuthority()) return; // server only
Picker->FindComponentByClass<UInventoryComponent>()->Count++;
Destroy(); // authoritative destroy replicates
}
void UInventoryComponent::GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& Out) const
{
Super::GetLifetimeReplicatedProps(Out);
DOREPLIFETIME(UInventoryComponent, Count);
}
A server-side destroy replicates to all clients; the replicated Count updates the UI through its OnRep callback.