Game multiplayer & netcode
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. |
Version contract — read first
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.
Topologies & authority
Pick the topology first; everything else follows from it.
- Client-server, dedicated server — a headless authoritative process no player controls. The gold standard for competitive/persistent games: one source of truth, hardest to cheat, costs money to run.
- Client-server, listen server — one player's machine is also the server (host). Cheap, zero infra, 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).
- Peer-to-peer — no central authority; peers exchange state directly. Low infra cost and low latency 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.
State replication vs RPCs
Two complementary tools — use the right one:
- State replication = continuous synchronized values (health, position, ammo, team). The framework 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.
- RPCs (remote procedure calls) = one-shot events ("fire weapon", "play explosion at P", "open door").
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
isDeadbool 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.
The hard problems
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:
- Client-side prediction — the owning client applies its own input immediately instead of waiting a full RTT for the server, so local movement feels instant.
- Server reconciliation — the server is still authoritative; the client tags each input with a sequence 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.
- Snapshot interpolation — remote entities are rendered ~100 ms in the past, interpolating between the last two received snapshots, so other players move smoothly instead of teleporting between updates.
- Lag compensation — for hitscan/instant hits, the server rewinds other entities to where the shooter 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.
Per-engine mapping
Short form here; full patterns in the references. All snippets are current-API.
Godot 4.x — high-level multiplayer
ENetMultiplayerPeer for transport, @rpc for events, MultiplayerSynchronizer for state, and
MultiplayerSpawner to replicate object spawns. Authority via set_multiplayer_authority(peer_id).
gdscript# 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 withis_multiplayer_authority(). A player scene usually gives its input node authority to the owning peer while the server keeps authority over health.MultiplayerSynchronizerstreams 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.MultiplayerSpawnerreplicates instancing of scenes under a path so late peers get existing objects.- Details, prediction wiring, and the full player-scene pattern: references/godot-high-level-multiplayer.md.
Unity — Netcode for GameObjects
NetworkObject (the identity, spawned via Spawn()), NetworkBehaviour (networked scripts),
NetworkVariable<T> (replicated state), and RPCs. NetworkManager.Singleton.StartHost/StartServer/StartClient.
csharppublic 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 */ } }
- Ownership/role:
IsOwner,IsServer,IsClient,IsHost,OwnerClientId. Only owners should send owner-intent RPCs; the server validates regardless. NetworkVariable<T>(value, readPerm, writePerm)—NetworkVariableWritePermission.Server(default) vs.Owner; subscribe viaOnValueChanged. UseNetworkTransformfor pos/rot sync.- Prefer the unified
[Rpc(SendTo.X)]; legacy[ServerRpc]/[ClientRpc]still work but need the method-name suffixes. Full patterns: references/unity-and-unreal-netcode.md.
Unreal — Actor replication
Set AActor::bReplicates = true (or SetReplicates(true)). Replicate properties with UPROPERTY and
register them in GetLifetimeReplicatedProps; use RPC UFUNCTION specifiers for events.
cppUPROPERTY(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 }
- Authority check:
HasAuthority()(server or standalone); rolesROLE_Authority,ROLE_AutonomousProxy(the owning client),ROLE_SimulatedProxy(observed elsewhere). - Relevancy keeps bandwidth sane:
NetCullDistanceSquared,bAlwaysRelevant,IsNetRelevantFor, and dormancy (SetNetDormancy) stop replicating actors a client can't perceive. Movement replicates viaCharacterMovementComponent, which already does prediction+reconciliation for you. - RepNotify vs Multicast, GAS/Network Prediction notes: references/unity-and-unreal-netcode.md.
Security / anti-cheat basics
- Validate every client input on the server — bounds-check movement deltas, cooldowns, line-of-sight, and resource costs. Reject the impossible; never assume a well-behaved client.
- Send a client only what it may know. Don't replicate enemy positions through walls (wallhack fuel), full inventories of others, or hidden RNG seeds — scope with relevancy/visibility.
- Rate-limit and sanity-check RPCs; a client spamming
ServerFirefaster than the weapon allows is caught by server-side cooldowns, not client-side ones. - Encryption/DTLS stops packet tampering on the wire but does not stop a modified client — only server authority does. Treat anti-cheat as defense in depth, not a product this skill ships.
Testing multiplayer locally
- Godot: enable Debug → Run Multiple Instances (2–4 windows) to host+join on one machine.
- Unity: Multiplayer Play Mode (or ParrelSync) runs several virtual players from one project; Network Simulator injects latency/loss.
- Unreal: PIE Number of Players + Net Mode (Play As Listen Server / Client), and Net PktLag / packet-emulation console vars to fake latency.
- Always test with simulated latency and packet loss, not just on localhost — localhost hides every prediction/reconciliation bug. Test the client path, not only the host's.
Anti-patterns
| 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 |


