mcpbeat Sign in

Gamedev Multiplayer Agent Skill

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`).

10k tokens
context cost
the whole folder, loaded on every use
6
files
instructions only
0
copies elsewhere
how many repositories repackaged it
105
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/ericrisco/rsc-harness --skill gamedev-multiplayer

What comes with it

25 828 bytes besides the instruction
evals/README.md
evals/cases.yaml
references/godot-high-level-multiplayer.md
references/prediction-and-latency.md
references/unity-and-unreal-netcode.md

The instruction itself

12 sections, as written by the author

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 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.

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).

# 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 scene

usually 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.
  • 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.

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 */ }
}
  • 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 via OnValueChanged. Use NetworkTransform for 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.

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
}
  • Authority check: HasAuthority() (server or standalone); roles ROLE_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 via

CharacterMovementComponent, 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 ServerFire faster 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 |

How to use it

Copy the folder

Take ericrisco/gamedev-multiplayer from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

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.