Use when adding multiplayer or netcode to a game — client-server vs P2P, server authority and anti-cheat, state replication vs RPCs, prediction and reconciliation, lag compensation, plus Godot 4 / Unity NGO / Unreal wiring. NOT single-player gameplay (that is `godot`, `unity`, `unreal`), NOT matchmaking or server hosting (that is `deployment`).
npx skills add https://github.com/ericrisco/rsc-harness --skill gamedev-multiplayer
Design and wire the network layer of a game across Godot 4.x, Unity, and Unreal: choose a
topology, put authority in the right place, replicate the right state, and hide latency without
opening the door to cheaters. This skill owns the *networking* decisions; the engine skills own
the local gameplay those decisions sit on top of.
Route elsewhere when the ask is not netcode:
| The ask | Route to | Why not here |
| --- | --- | --- |
| Single-player movement, AI, input, animation, save files | godot / unity / unreal | The engine skill owns local gameplay; no network dimension. |
| Physics determinism, collision, character controllers | gamedev-physics | Netcode *consumes* determinism; it does not own the physics fix. |
| Matchmaking, lobby backend, dedicated-server hosting, relays, DB, game-server CI | deployment | Running the fleet, not designing the in-game net layer. |
| Web realtime — chat, presence, a generic WebSocket app | webhooks | Not a game world/simulation. |
| General threat modeling, authz review | secure-coding | This skill keeps only the game-specific anti-cheat controls. |
Target the current APIs. Never emit these deprecated / removed APIs:
| Engine | Target | Banned (removed/legacy) | Emit instead |
|---|---|---|---|
| Godot | 4.x | master/puppet/remote/sync keywords, rpc_config() as a func, rset, NetworkedMultiplayerENet | @rpc(...) annotation, set_multiplayer_authority(), ENetMultiplayerPeer, MultiplayerSynchronizer |
| Unity | Netcode for GameObjects (NGO) 1.x/2.x | UNet / HLAPI: NetworkIdentity, [Command], [SyncVar], NetworkServer, UnityEngine.Networking | NGO NetworkObject, NetworkBehaviour, NetworkVariable<T>, [Rpc(SendTo.X)] |
| Unreal | 5.x | replicating by polling in Tick, skipping WithValidation on gameplay RPCs | UPROPERTY(Replicated / ReplicatedUsing=...) + GetLifetimeReplicatedProps, Server/Client/NetMulticast UFUNCTIONs |
UNet is removed from modern Unity — do not scaffold it. If a repo genuinely uses Mirror (a
community UNet-like fork), say so; this skill targets first-party NGO. In NGO 2.x the unified
[Rpc(SendTo.Server)] / [Rpc(SendTo.ClientsAndHost)] attribute is preferred; the older
[ServerRpc] / [ClientRpc] still compile (and enforce …ServerRpc / …ClientRpc method-name
suffixes) — cover them but prefer the unified form on new code.
Pick the topology first; everything else follows from it.
standard for competitive/persistent games: one source of truth, hardest to cheat, costs money to run.
but the host has a latency advantage and can tamper; fine for co-op and casual PvP. **Design so the
same code runs on both** (a listen server is a dedicated server that also has a local client).
between peers, but NAT traversal is painful, it is the easiest to cheat, and it needs lockstep or a
rollback model to stay consistent. Reasonable for small trusted lobbies or deterministic fighting games.
Server-authoritative is the default. The server owns the truth; clients send *intent* (inputs,
requests), never results. Never trust the client: the client cannot set its own health, position,
score, currency, or hit results — it *asks*, the server *decides*, everyone else *observes*. A client
that says "I moved to X / I did 40 damage / I have 999 gold" is either lagging or cheating; treat both
the same. The one thing a client legitimately owns is its own input and, by convention, cosmetic-only
state. See references/prediction-and-latency.md for how to
keep this responsive.
Two complementary tools — use the right one:
watches a variable and pushes changes to relevant peers; late-joiners get the current value automatically.
Use for anything a client needs the *latest* of. It is idempotent and self-healing on packet loss.
Fire-and-forget; a dropped unreliable RPC is simply lost. Use for events, never for state you need to
stay consistent — a missed "you died" RPC leaves a client desynced forever; a replicated isDead bool
self-corrects.
Rule of thumb: replicate *nouns* (state), RPC *verbs* (events). What to replicate: only what other
peers must see, at the coarsest rate that still looks right — position/rotation of visible actors,
gameplay stats, animation state flags. Do not replicate values a peer can derive locally, purely
cosmetic particles, or high-frequency data no one observes.
Ownership / authority decides *who may write*. Every networked object has an owner (a peer id) and an
authority (usually the server). Writes to replicated state must come from the authority; ownership lets a
client drive *its own* pawn's input while the server still validates the outcome. Getting ownership wrong
is the #1 cause of "it works for the host but not the client" bugs.
Latency (round-trip time, RTT) is the enemy; jitter and packet loss make it worse. Four techniques,
covered in depth in references/prediction-and-latency.md:
full RTT for the server, so local movement feels instant.
number, and when the authoritative state arrives it *replays* any inputs the server hadn't processed yet.
Mismatch → a correction (the visible "rubber-band"); minimize it, don't hide it.
last two received snapshots, so other players move smoothly instead of teleporting between updates.
*saw* them (at the shooter's render time) before testing the hit. Fairer for the shooter; occasionally
"shot behind a wall" for the victim. A deliberate tradeoff, not a bug.
Tick rate / netrate: the server simulates at a fixed tick (e.g. 20–64 Hz) and sends state at a *send
rate* that may be lower than its sim rate and decoupled from client render FPS. Never tie gameplay to frame
rate on a networked game — simulate on a fixed timestep, interpolate for rendering.
Short form here; full patterns in the references. All snippets are current-API.
ENetMultiplayerPeer for transport, @rpc for events, MultiplayerSynchronizer for state, and
MultiplayerSpawner to replicate object spawns. Authority via set_multiplayer_authority(peer_id).
# Host / join
func host(port := 9999) -> void:
var peer := ENetMultiplayerPeer.new()
peer.create_server(port, 8)
multiplayer.multiplayer_peer = peer
func join(ip: String, port := 9999) -> void:
var peer := ENetMultiplayerPeer.new()
peer.create_client(ip, port)
multiplayer.multiplayer_peer = peer
# RPC: params are optional and order-free. any_peer lets clients call it;
# authority (default) means only the multiplayer authority may call it.
@rpc("any_peer", "call_local", "reliable")
func request_fire(target: Vector3) -> void:
if not multiplayer.is_server(): return # server validates
var shooter := multiplayer.get_remote_sender_id()
_resolve_shot(shooter, target) # server decides the result
set_multiplayer_authority(id) on a node; check with is_multiplayer_authority(). A player sceneusually gives its input node authority to the owning peer while the server keeps authority over health.
MultiplayerSynchronizer streams a chosen property list (position, etc.) from the authority to peers;set its replication interval and visibility (set_visibility_for(peer, bool)) to scope bandwidth.
MultiplayerSpawner replicates instancing of scenes under a path so late peers get existing objects.references/godot-high-level-multiplayer.md.
NetworkObject (the identity, spawned via Spawn()), NetworkBehaviour (networked scripts),
NetworkVariable<T> (replicated state), and RPCs. NetworkManager.Singleton.StartHost/StartServer/StartClient.
public class Player : NetworkBehaviour {
// Default perms: everyone reads, only the SERVER writes → authoritative health.
public NetworkVariable<int> Health = new(100);
[Rpc(SendTo.Server)] // client → server: an intent
void FireRpc(Vector3 target) {
// runs on the server; validate, then apply and let NetworkVariables replicate the result
ResolveShot(OwnerClientId, target);
}
[Rpc(SendTo.ClientsAndHost)] // server → everyone: an event
void PlayMuzzleFxRpc(Vector3 at) { /* cosmetic only */ }
}
IsOwner, IsServer, IsClient, IsHost, OwnerClientId. Only owners should sendowner-intent RPCs; the server validates regardless.
NetworkVariable<T>(value, readPerm, writePerm) — NetworkVariableWritePermission.Server (default) vs.Owner; subscribe via OnValueChanged. Use NetworkTransform for pos/rot sync.
[Rpc(SendTo.X)]; legacy [ServerRpc]/[ClientRpc] still work but need themethod-name suffixes. Full patterns:
references/unity-and-unreal-netcode.md.
Set AActor::bReplicates = true (or SetReplicates(true)). Replicate properties with UPROPERTY and
register them in GetLifetimeReplicatedProps; use RPC UFUNCTION specifiers for events.
UPROPERTY(ReplicatedUsing = OnRep_Health) // RepNotify fires on clients when it changes
float Health = 100.f;
UFUNCTION(Server, Reliable, WithValidation) // client → server intent (+ validation)
void ServerFire(FVector Target);
UFUNCTION(NetMulticast, Unreliable) // server → all: cosmetic event
void MulticastMuzzleFx(FVector At);
void AMyPawn::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Out) const {
Super::GetLifetimeReplicatedProps(Out);
DOREPLIFETIME(AMyPawn, Health); // or DOREPLIFETIME_CONDITION for push/owner-only
}
HasAuthority() (server or standalone); roles ROLE_Authority, ROLE_AutonomousProxy(the owning client), ROLE_SimulatedProxy (observed elsewhere).
NetCullDistanceSquared, bAlwaysRelevant, IsNetRelevantFor, anddormancy (SetNetDormancy) stop replicating actors a client can't perceive. Movement replicates via
CharacterMovementComponent, which already does prediction+reconciliation for you.
references/unity-and-unreal-netcode.md.
and resource costs. Reject the impossible; never assume a well-behaved client.
full inventories of others, or hidden RNG seeds — scope with relevancy/visibility.
ServerFire faster than the weapon allows is caughtby server-side cooldowns, not client-side ones.
authority does. Treat anti-cheat as defense in depth, not a product this skill ships.
Network Simulator injects latency/loss.
packet-emulation console vars to fake latency.
prediction/reconciliation bug. Test the client path, not only the host's.
| Anti-pattern | Why it bites | Do instead |
| --- | --- | --- |
| Writing net code before the topology is chosen | Authority and cost model change everything downstream | Pick dedicated / listen / P2P first, and make the *same code* run on server and listen host |
| Trusting a client-reported result (health, position, score, hit) | That is the cheat surface, and lag looks identical to cheating | Clients send intent only; the server validates and owns all gameplay state |
| RPCing state that must stay consistent | A dropped unreliable event desyncs that client permanently | Replicate nouns (state), RPC verbs (events) — replicated values self-correct |
| Ownership/authority left implicit | The #1 cause of "works for the host, breaks for the client" | Set authority/ownership explicitly on spawn |
| Forgetting the listen-server host is also a client | Authority-only logic double-executes on the host | Guard it behind is_server() / HasAuthority() |
| Making the owning client wait a full RTT for its own input | Movement feels sluggish and unresponsive | Predict locally + reconcile; snapshot-interpolate remote entities |
| Client-side hit detection for hitscan | Trivially spoofed, and unfair to the higher-ping shooter | Server-side lag compensation: rewind to the shooter's render time |
| Tying simulation to render FPS | Sim diverges between machines at different frame rates | Fixed timestep on the sim, interpolate on the render |
| Replicating everything, every tick, to everyone | Melts bandwidth and leaks wallhack fuel | Scope by relevancy/visibility and send rate; rate-limit RPCs |
| Testing only on localhost | 0 ms RTT masks the exact desync/rubber-band bugs players will hit | Inject latency + packet loss across instances, exercising the client path |
| Emitting UNet/HLAPI, Godot 3.x master/puppet/rset, or Tick-polled Unreal replication | Removed or legacy — it will not compile or will silently underperform | The current APIs in the version contract above |
Take ericrisco/gamedev-multiplayer from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.