@ericrisco/rsc 0.1.34 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -2
- package/manifest.json +241 -2
- package/package.json +1 -1
- package/scripts/lib/domains.js +1 -0
- package/scripts/lib/recommend.js +4 -0
- package/skills/agent-eval/SKILL.md +1 -1
- package/skills/analyze/SKILL.md +1 -1
- package/skills/brand-voice/SKILL.md +1 -1
- package/skills/brand-voice/references/voice-guide-template.md +14 -0
- package/skills/building-agents/SKILL.md +2 -2
- package/skills/competitor-watch/SKILL.md +9 -4
- package/skills/competitor-watch/references/tracker-schema.md +19 -0
- package/skills/constitution/references/constitution-template.md +10 -0
- package/skills/content-engine/SKILL.md +1 -1
- package/skills/content-engine/references/brief-and-pipeline.md +15 -1
- package/skills/course-builder/references/grounding-and-scoping.md +23 -0
- package/skills/course-storytelling/references/learner-grounding.md +22 -2
- package/skills/decision-records/SKILL.md +14 -1
- package/skills/decision-records/references/templates.md +20 -0
- package/skills/design/references/brand-grounding.md +11 -1
- package/skills/game-design/SKILL.md +170 -0
- package/skills/game-design/evals/README.md +13 -0
- package/skills/game-design/evals/cases.yaml +47 -0
- package/skills/game-design/references/mechanics-and-loops.md +78 -0
- package/skills/game-design/references/playtesting-and-scope.md +58 -0
- package/skills/game-design/references/progression-and-economy.md +70 -0
- package/skills/game-storytelling/SKILL.md +159 -0
- package/skills/game-storytelling/evals/README.md +28 -0
- package/skills/game-storytelling/evals/cases.yaml +51 -0
- package/skills/game-storytelling/references/branching-and-state.md +134 -0
- package/skills/game-storytelling/references/dialogue-tools.md +117 -0
- package/skills/game-storytelling/references/environmental-storytelling.md +71 -0
- package/skills/gamedev-multiplayer/SKILL.md +250 -0
- package/skills/gamedev-multiplayer/evals/README.md +12 -0
- package/skills/gamedev-multiplayer/evals/cases.yaml +44 -0
- package/skills/gamedev-multiplayer/references/godot-high-level-multiplayer.md +133 -0
- package/skills/gamedev-multiplayer/references/prediction-and-latency.md +104 -0
- package/skills/gamedev-multiplayer/references/unity-and-unreal-netcode.md +166 -0
- package/skills/gamedev-pathing/SKILL.md +242 -0
- package/skills/gamedev-pathing/evals/README.md +14 -0
- package/skills/gamedev-pathing/evals/cases.yaml +49 -0
- package/skills/gamedev-pathing/references/navmesh-workflow.md +110 -0
- package/skills/gamedev-pathing/references/search-algorithms.md +135 -0
- package/skills/gamedev-pathing/references/steering-and-avoidance.md +120 -0
- package/skills/gamedev-physics/SKILL.md +225 -0
- package/skills/gamedev-physics/evals/README.md +12 -0
- package/skills/gamedev-physics/evals/cases.yaml +54 -0
- package/skills/gamedev-physics/references/character-controllers.md +120 -0
- package/skills/gamedev-physics/references/determinism-and-queries.md +124 -0
- package/skills/gamedev-physics/references/layers-and-masks.md +115 -0
- package/skills/gamedev-shaders/SKILL.md +252 -0
- package/skills/gamedev-shaders/evals/README.md +14 -0
- package/skills/gamedev-shaders/evals/cases.yaml +45 -0
- package/skills/gamedev-shaders/references/effect-recipes.md +205 -0
- package/skills/gamedev-shaders/references/godot-shading-language.md +133 -0
- package/skills/gamedev-shaders/references/unity-and-unreal-shaders.md +169 -0
- package/skills/gamedev-shipping/SKILL.md +236 -0
- package/skills/gamedev-shipping/evals/README.md +9 -0
- package/skills/gamedev-shipping/evals/cases.yaml +55 -0
- package/skills/gamedev-shipping/references/mobile-and-desktop.md +127 -0
- package/skills/gamedev-shipping/references/stores-and-ci.md +162 -0
- package/skills/gamedev-shipping/references/web-export-headers.md +140 -0
- package/skills/godot/SKILL.md +319 -0
- package/skills/godot/evals/README.md +13 -0
- package/skills/godot/evals/cases.yaml +50 -0
- package/skills/godot/references/export-and-testing.md +102 -0
- package/skills/godot/references/gdscript-style.md +84 -0
- package/skills/godot/references/godot3-to-4-traps.md +89 -0
- package/skills/godot/references/nodes-scenes-resources.md +109 -0
- package/skills/harness/SKILL.md +3 -3
- package/skills/harness/references/daily-curation-automation.md +8 -6
- package/skills/harness/references/obsidian-scaffolding.md +16 -9
- package/skills/harness/references/wiki-article-template.md +31 -14
- package/skills/harness/references/wiki-index-template.md +7 -3
- package/skills/harness/references/wiki-protocol.md +120 -54
- package/skills/harness/references/wiki-worklog-template.md +5 -1
- package/skills/instagram-api/SKILL.md +8 -1
- package/skills/instagram-api/references/insights-metrics.md +8 -1
- package/skills/knowledge-ops/SKILL.md +9 -9
- package/skills/knowledge-ops/evals/cases.yaml +1 -1
- package/skills/knowledge-ops/references/gardening-playbook.md +18 -8
- package/skills/level-design/SKILL.md +175 -0
- package/skills/level-design/evals/README.md +35 -0
- package/skills/level-design/evals/cases.yaml +58 -0
- package/skills/level-design/references/blockout-to-art.md +118 -0
- package/skills/level-design/references/guidance-and-readability.md +152 -0
- package/skills/level-design/references/pacing-and-encounters.md +142 -0
- package/skills/linkedin-api/SKILL.md +10 -1
- package/skills/linkedin-strategy/SKILL.md +12 -0
- package/skills/linkedin-strategy/references/wiki-records.md +27 -1
- package/skills/marketing/references/brand-grounding.md +22 -2
- package/skills/medium-strategy/SKILL.md +12 -0
- package/skills/medium-strategy/references/wiki-records.md +39 -1
- package/skills/medium-writing/SKILL.md +19 -0
- package/skills/plan/references/plan-template.md +12 -1
- package/skills/roast-me/tools/adapters/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/base.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/claude.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/codex.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/gemini.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/registry.cpython-312.pyc +0 -0
- package/skills/shortform-packaging/SKILL.md +7 -2
- package/skills/shortform-packaging/references/package-templates.md +13 -2
- package/skills/shortform-strategy/SKILL.md +30 -6
- package/skills/shortform-strategy/references/learning-loop-template.md +9 -1
- package/skills/specify/references/spec-template.md +12 -1
- package/skills/tiktok-api/SKILL.md +6 -2
- package/skills/tiktok-api/references/wiki-schema.md +38 -4
- package/skills/unity/SKILL.md +282 -0
- package/skills/unity/evals/README.md +8 -0
- package/skills/unity/evals/cases.yaml +43 -0
- package/skills/unity/references/build-and-platforms.md +65 -0
- package/skills/unity/references/input-system.md +102 -0
- package/skills/unity/references/lifecycle-deep-dive.md +75 -0
- package/skills/unreal/SKILL.md +270 -0
- package/skills/unreal/evals/README.md +12 -0
- package/skills/unreal/evals/cases.yaml +45 -0
- package/skills/unreal/references/blueprint-cpp-parity.md +109 -0
- package/skills/unreal/references/gameplay-framework.md +95 -0
- package/skills/unreal/references/packaging.md +83 -0
- package/skills/verify/SKILL.md +11 -1
- package/skills/youtube-api/SKILL.md +7 -2
- package/skills/youtube-api/references/wiki-schema.md +25 -4
- package/skills/youtube-packaging/SKILL.md +10 -2
- package/skills/youtube-packaging/references/description-and-chapters.md +70 -2
- package/skills/youtube-strategy/SKILL.md +9 -2
- package/skills/youtube-strategy/references/wiki-records.md +39 -6
- package/skills/youtube-strategy/scripts/verify.sh +4 -0
- package/skills/youtube-thumbnails/SKILL.md +10 -5
- package/skills/youtube-thumbnails/references/experiment-log-format.md +28 -2
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gamedev-multiplayer
|
|
3
|
+
description: "Use when adding multiplayer, netcode, or replication to a game — syncing players/state across a network, online co-op or PvP, client-server vs peer-to-peer, server-authoritative design, client-side prediction + server reconciliation, snapshot interpolation, lag compensation, tick/netrate, and per-engine wiring: Godot 4.x MultiplayerSpawner/MultiplayerSynchronizer/@rpc/ENetMultiplayerPeer/set_multiplayer_authority, Unity Netcode for GameObjects (NetworkObject/NetworkBehaviour/NetworkVariable/[Rpc]/ServerRpc/ClientRpc), Unreal Actor replication (Replicated/ReplicatedUsing, Server/Client/NetMulticast RPCs, relevancy). Triggers: 'how do I sync players', 'client and server desync', 'players rubber-band or teleport', 'who has authority', 'laggy movement', 'cheater set their own health'. NOT single-player gameplay, AI, or input (route to engine skill godot/unity/unreal), and NOT matchmaking, dedicated-server hosting, or backend infra (route to deployment)."
|
|
4
|
+
tags: [multiplayer, netcode, replication, networking, prediction, authority]
|
|
5
|
+
recommends: [godot, unity, unreal, gamedev-physics]
|
|
6
|
+
profiles: [full]
|
|
7
|
+
origin: risco
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Game multiplayer & netcode
|
|
11
|
+
|
|
12
|
+
Design and wire the network layer of a game across Godot 4.x, Unity, and Unreal: choose a
|
|
13
|
+
topology, put authority in the right place, replicate the right state, and hide latency without
|
|
14
|
+
opening the door to cheaters. This skill owns the *networking* decisions; the engine skills own
|
|
15
|
+
the local gameplay those decisions sit on top of.
|
|
16
|
+
|
|
17
|
+
## Version contract — read first
|
|
18
|
+
|
|
19
|
+
Target the current APIs. **Never emit these deprecated / removed APIs:**
|
|
20
|
+
|
|
21
|
+
| Engine | Target | Banned (removed/legacy) | Emit instead |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| **Godot** | 4.x | `master`/`puppet`/`remote`/`sync` keywords, `rpc_config()` as a func, `rset`, `NetworkedMultiplayerENet` | `@rpc(...)` annotation, `set_multiplayer_authority()`, `ENetMultiplayerPeer`, `MultiplayerSynchronizer` |
|
|
24
|
+
| **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)]` |
|
|
25
|
+
| **Unreal** | 5.x | replicating by polling in `Tick`, skipping `WithValidation` on gameplay RPCs | `UPROPERTY(Replicated / ReplicatedUsing=...)` + `GetLifetimeReplicatedProps`, `Server`/`Client`/`NetMulticast` `UFUNCTION`s |
|
|
26
|
+
|
|
27
|
+
UNet is **removed** from modern Unity — do not scaffold it. If a repo genuinely uses Mirror (a
|
|
28
|
+
community UNet-like fork), say so; this skill targets first-party NGO. In NGO 2.x the unified
|
|
29
|
+
`[Rpc(SendTo.Server)]` / `[Rpc(SendTo.ClientsAndHost)]` attribute is preferred; the older
|
|
30
|
+
`[ServerRpc]` / `[ClientRpc]` still compile (and enforce `…ServerRpc` / `…ClientRpc` method-name
|
|
31
|
+
suffixes) — cover them but prefer the unified form on new code.
|
|
32
|
+
|
|
33
|
+
## Fires on / When NOT
|
|
34
|
+
|
|
35
|
+
**Fires on:** "how do I sync players", replication, netcode, RPCs, authority, online co-op or PvP,
|
|
36
|
+
"client and server disagree", rubber-banding / teleporting players, prediction & reconciliation,
|
|
37
|
+
lag compensation, tick rate / send rate, spawning networked objects, "the client can cheat".
|
|
38
|
+
|
|
39
|
+
**When NOT (route away):**
|
|
40
|
+
|
|
41
|
+
- Single-player movement, AI, input, animation, save files → the engine skill: `godot`, `unity`, `unreal`.
|
|
42
|
+
- Physics determinism, collision, character controllers → `gamedev-physics` (netcode *consumes* it).
|
|
43
|
+
- Matchmaking service, lobby backend, dedicated-server hosting/orchestration, relays, DB → `deployment`.
|
|
44
|
+
- Web realtime (chat, presence, generic WebSocket app) that is not a game world → `webhooks` / a backend skill.
|
|
45
|
+
|
|
46
|
+
## Topologies & authority
|
|
47
|
+
|
|
48
|
+
Pick the topology first; everything else follows from it.
|
|
49
|
+
|
|
50
|
+
- **Client-server, dedicated server** — a headless authoritative process no player controls. The gold
|
|
51
|
+
standard for competitive/persistent games: one source of truth, hardest to cheat, costs money to run.
|
|
52
|
+
- **Client-server, listen server** — one player's machine is also the server (host). Cheap, zero infra,
|
|
53
|
+
but the host has a latency advantage and can tamper; fine for co-op and casual PvP. **Design so the
|
|
54
|
+
same code runs on both** (a listen server is a dedicated server that also has a local client).
|
|
55
|
+
- **Peer-to-peer** — no central authority; peers exchange state directly. Low infra cost and low latency
|
|
56
|
+
between peers, but NAT traversal is painful, it is the easiest to cheat, and it needs lockstep or a
|
|
57
|
+
rollback model to stay consistent. Reasonable for small trusted lobbies or deterministic fighting games.
|
|
58
|
+
|
|
59
|
+
**Server-authoritative is the default.** The server owns the truth; clients send *intent* (inputs,
|
|
60
|
+
requests), never results. **Never trust the client:** the client cannot set its own health, position,
|
|
61
|
+
score, currency, or hit results — it *asks*, the server *decides*, everyone else *observes*. A client
|
|
62
|
+
that says "I moved to X / I did 40 damage / I have 999 gold" is either lagging or cheating; treat both
|
|
63
|
+
the same. The one thing a client legitimately owns is its own input and, by convention, cosmetic-only
|
|
64
|
+
state. See references/prediction-and-latency.md for how to keep this responsive.
|
|
65
|
+
|
|
66
|
+
## State replication vs RPCs
|
|
67
|
+
|
|
68
|
+
Two complementary tools — use the right one:
|
|
69
|
+
|
|
70
|
+
- **State replication** = *continuous* synchronized values (health, position, ammo, team). The framework
|
|
71
|
+
watches a variable and pushes changes to relevant peers; late-joiners get the current value automatically.
|
|
72
|
+
Use for anything a client needs the *latest* of. It is idempotent and self-healing on packet loss.
|
|
73
|
+
- **RPCs (remote procedure calls)** = *one-shot events* ("fire weapon", "play explosion at P", "open door").
|
|
74
|
+
Fire-and-forget; a dropped unreliable RPC is simply lost. Use for events, **never** for state you need to
|
|
75
|
+
stay consistent — a missed "you died" RPC leaves a client desynced forever; a replicated `isDead` bool
|
|
76
|
+
self-corrects.
|
|
77
|
+
|
|
78
|
+
**Rule of thumb:** replicate *nouns* (state), RPC *verbs* (events). What to replicate: only what other
|
|
79
|
+
peers must see, at the coarsest rate that still looks right — position/rotation of visible actors,
|
|
80
|
+
gameplay stats, animation state flags. Do **not** replicate values a peer can derive locally, purely
|
|
81
|
+
cosmetic particles, or high-frequency data no one observes.
|
|
82
|
+
|
|
83
|
+
**Ownership / authority** decides *who may write*. Every networked object has an owner (a peer id) and an
|
|
84
|
+
authority (usually the server). Writes to replicated state must come from the authority; ownership lets a
|
|
85
|
+
client drive *its own* pawn's input while the server still validates the outcome. Getting ownership wrong
|
|
86
|
+
is the #1 cause of "it works for the host but not the client" bugs.
|
|
87
|
+
|
|
88
|
+
## The hard problems
|
|
89
|
+
|
|
90
|
+
Latency (round-trip time, RTT) is the enemy; jitter and packet loss make it worse. Four techniques,
|
|
91
|
+
covered in depth in **references/prediction-and-latency.md**:
|
|
92
|
+
|
|
93
|
+
1. **Client-side prediction** — the owning client applies its own input *immediately* instead of waiting a
|
|
94
|
+
full RTT for the server, so local movement feels instant.
|
|
95
|
+
2. **Server reconciliation** — the server is still authoritative; the client tags each input with a sequence
|
|
96
|
+
number, and when the authoritative state arrives it *replays* any inputs the server hadn't processed yet.
|
|
97
|
+
Mismatch → a correction (the visible "rubber-band"); minimize it, don't hide it.
|
|
98
|
+
3. **Snapshot interpolation** — *remote* entities are rendered ~100 ms in the past, interpolating between the
|
|
99
|
+
last two received snapshots, so other players move smoothly instead of teleporting between updates.
|
|
100
|
+
4. **Lag compensation** — for hitscan/instant hits, the server rewinds other entities to where the shooter
|
|
101
|
+
*saw* them (at the shooter's render time) before testing the hit. Fairer for the shooter; occasionally
|
|
102
|
+
"shot behind a wall" for the victim. A deliberate tradeoff, not a bug.
|
|
103
|
+
|
|
104
|
+
**Tick rate / netrate:** the server simulates at a fixed tick (e.g. 20–64 Hz) and sends state at a *send
|
|
105
|
+
rate* that may be lower than its sim rate and decoupled from client render FPS. Never tie gameplay to frame
|
|
106
|
+
rate on a networked game — simulate on a fixed timestep, interpolate for rendering.
|
|
107
|
+
|
|
108
|
+
## Per-engine mapping
|
|
109
|
+
|
|
110
|
+
Short form here; full patterns in the references. All snippets are current-API.
|
|
111
|
+
|
|
112
|
+
### Godot 4.x — high-level multiplayer
|
|
113
|
+
|
|
114
|
+
`ENetMultiplayerPeer` for transport, `@rpc` for events, `MultiplayerSynchronizer` for state, and
|
|
115
|
+
`MultiplayerSpawner` to replicate object spawns. Authority via `set_multiplayer_authority(peer_id)`.
|
|
116
|
+
|
|
117
|
+
```gdscript
|
|
118
|
+
# Host / join
|
|
119
|
+
func host(port := 9999) -> void:
|
|
120
|
+
var peer := ENetMultiplayerPeer.new()
|
|
121
|
+
peer.create_server(port, 8)
|
|
122
|
+
multiplayer.multiplayer_peer = peer
|
|
123
|
+
|
|
124
|
+
func join(ip: String, port := 9999) -> void:
|
|
125
|
+
var peer := ENetMultiplayerPeer.new()
|
|
126
|
+
peer.create_client(ip, port)
|
|
127
|
+
multiplayer.multiplayer_peer = peer
|
|
128
|
+
|
|
129
|
+
# RPC: params are optional and order-free. any_peer lets clients call it;
|
|
130
|
+
# authority (default) means only the multiplayer authority may call it.
|
|
131
|
+
@rpc("any_peer", "call_local", "reliable")
|
|
132
|
+
func request_fire(target: Vector3) -> void:
|
|
133
|
+
if not multiplayer.is_server(): return # server validates
|
|
134
|
+
var shooter := multiplayer.get_remote_sender_id()
|
|
135
|
+
_resolve_shot(shooter, target) # server decides the result
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- `set_multiplayer_authority(id)` on a node; check with `is_multiplayer_authority()`. A player scene
|
|
139
|
+
usually gives its input node authority to the owning peer while the server keeps authority over health.
|
|
140
|
+
- `MultiplayerSynchronizer` streams a chosen property list (position, etc.) from the authority to peers;
|
|
141
|
+
set its replication interval and visibility (`set_visibility_for(peer, bool)`) to scope bandwidth.
|
|
142
|
+
- `MultiplayerSpawner` replicates instancing of scenes under a path so late peers get existing objects.
|
|
143
|
+
- Details, prediction wiring, and the full player-scene pattern: **references/godot-high-level-multiplayer.md**.
|
|
144
|
+
|
|
145
|
+
### Unity — Netcode for GameObjects
|
|
146
|
+
|
|
147
|
+
`NetworkObject` (the identity, spawned via `Spawn()`), `NetworkBehaviour` (networked scripts),
|
|
148
|
+
`NetworkVariable<T>` (replicated state), and RPCs. `NetworkManager.Singleton.StartHost/StartServer/StartClient`.
|
|
149
|
+
|
|
150
|
+
```csharp
|
|
151
|
+
public class Player : NetworkBehaviour {
|
|
152
|
+
// Default perms: everyone reads, only the SERVER writes → authoritative health.
|
|
153
|
+
public NetworkVariable<int> Health = new(100);
|
|
154
|
+
|
|
155
|
+
[Rpc(SendTo.Server)] // client → server: an intent
|
|
156
|
+
void FireRpc(Vector3 target) {
|
|
157
|
+
// runs on the server; validate, then apply and let NetworkVariables replicate the result
|
|
158
|
+
ResolveShot(OwnerClientId, target);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
[Rpc(SendTo.ClientsAndHost)] // server → everyone: an event
|
|
162
|
+
void PlayMuzzleFxRpc(Vector3 at) { /* cosmetic only */ }
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- Ownership/role: `IsOwner`, `IsServer`, `IsClient`, `IsHost`, `OwnerClientId`. Only owners should send
|
|
167
|
+
owner-intent RPCs; the server validates regardless.
|
|
168
|
+
- `NetworkVariable<T>(value, readPerm, writePerm)` — `NetworkVariableWritePermission.Server` (default) vs
|
|
169
|
+
`.Owner`; subscribe via `OnValueChanged`. Use `NetworkTransform` for pos/rot sync.
|
|
170
|
+
- Prefer the unified `[Rpc(SendTo.X)]`; legacy `[ServerRpc]`/`[ClientRpc]` still work but need the
|
|
171
|
+
method-name suffixes. Full patterns: **references/unity-and-unreal-netcode.md**.
|
|
172
|
+
|
|
173
|
+
### Unreal — Actor replication
|
|
174
|
+
|
|
175
|
+
Set `AActor::bReplicates = true` (or `SetReplicates(true)`). Replicate properties with `UPROPERTY` and
|
|
176
|
+
register them in `GetLifetimeReplicatedProps`; use RPC `UFUNCTION` specifiers for events.
|
|
177
|
+
|
|
178
|
+
```cpp
|
|
179
|
+
UPROPERTY(ReplicatedUsing = OnRep_Health) // RepNotify fires on clients when it changes
|
|
180
|
+
float Health = 100.f;
|
|
181
|
+
|
|
182
|
+
UFUNCTION(Server, Reliable, WithValidation) // client → server intent (+ validation)
|
|
183
|
+
void ServerFire(FVector Target);
|
|
184
|
+
|
|
185
|
+
UFUNCTION(NetMulticast, Unreliable) // server → all: cosmetic event
|
|
186
|
+
void MulticastMuzzleFx(FVector At);
|
|
187
|
+
|
|
188
|
+
void AMyPawn::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Out) const {
|
|
189
|
+
Super::GetLifetimeReplicatedProps(Out);
|
|
190
|
+
DOREPLIFETIME(AMyPawn, Health); // or DOREPLIFETIME_CONDITION for push/owner-only
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
- Authority check: `HasAuthority()` (server or standalone); roles `ROLE_Authority`, `ROLE_AutonomousProxy`
|
|
195
|
+
(the owning client), `ROLE_SimulatedProxy` (observed elsewhere).
|
|
196
|
+
- **Relevancy** keeps bandwidth sane: `NetCullDistanceSquared`, `bAlwaysRelevant`, `IsNetRelevantFor`, and
|
|
197
|
+
dormancy (`SetNetDormancy`) stop replicating actors a client can't perceive. Movement replicates via
|
|
198
|
+
`CharacterMovementComponent`, which already does prediction+reconciliation for you.
|
|
199
|
+
- RepNotify vs Multicast, GAS/Network Prediction notes: **references/unity-and-unreal-netcode.md**.
|
|
200
|
+
|
|
201
|
+
## Security / anti-cheat basics
|
|
202
|
+
|
|
203
|
+
- **Validate every client input on the server** — bounds-check movement deltas, cooldowns, line-of-sight,
|
|
204
|
+
and resource costs. Reject the impossible; never assume a well-behaved client.
|
|
205
|
+
- **Send a client only what it may know.** Don't replicate enemy positions through walls (wallhack fuel),
|
|
206
|
+
full inventories of others, or hidden RNG seeds — scope with relevancy/visibility.
|
|
207
|
+
- Rate-limit and sanity-check RPCs; a client spamming `ServerFire` faster than the weapon allows is caught
|
|
208
|
+
by server-side cooldowns, not client-side ones.
|
|
209
|
+
- Encryption/DTLS stops packet tampering on the wire but does **not** stop a modified client — only server
|
|
210
|
+
authority does. Treat anti-cheat as defense in depth, not a product this skill ships.
|
|
211
|
+
|
|
212
|
+
## Testing multiplayer locally
|
|
213
|
+
|
|
214
|
+
- **Godot:** enable *Debug → Run Multiple Instances* (2–4 windows) to host+join on one machine.
|
|
215
|
+
- **Unity:** *Multiplayer Play Mode* (or ParrelSync) runs several virtual players from one project;
|
|
216
|
+
Network Simulator injects latency/loss.
|
|
217
|
+
- **Unreal:** PIE *Number of Players* + *Net Mode* (Play As Listen Server / Client), and *Net PktLag* /
|
|
218
|
+
packet-emulation console vars to fake latency.
|
|
219
|
+
- Always test **with simulated latency and packet loss**, not just on localhost — localhost hides every
|
|
220
|
+
prediction/reconciliation bug. Test the client path, not only the host's.
|
|
221
|
+
|
|
222
|
+
## Guardrails / gotchas
|
|
223
|
+
|
|
224
|
+
- Localhost is a liar: 0 ms RTT masks the exact desync/rubber-band bugs your players will hit. Inject lag.
|
|
225
|
+
- Don't RPC state you need to stay consistent — replicate it (an unreliable event drop is permanent).
|
|
226
|
+
- Don't tie simulation to render FPS; fixed timestep on the sim, interpolate on the render.
|
|
227
|
+
- The listen-server host is a client too — guard authority-only logic behind an `is_server`/`HasAuthority`
|
|
228
|
+
check or it double-executes on the host.
|
|
229
|
+
- Wrong ownership = "works for host, breaks for client." Set authority/ownership explicitly on spawn.
|
|
230
|
+
- Replicating everything, every tick, to everyone melts bandwidth — scope by relevancy/visibility and rate.
|
|
231
|
+
|
|
232
|
+
## Related skills
|
|
233
|
+
|
|
234
|
+
- **`godot` / `unity` / `unreal`** — the engine your game runs in; owns local gameplay, input, scenes, build.
|
|
235
|
+
- **`gamedev-physics`** — determinism, collision, character controllers; netcode replicates its results and
|
|
236
|
+
needs its determinism for prediction/rollback.
|
|
237
|
+
- **`deployment`** — dedicated-server hosting, matchmaking/lobby backend, relays, and CI for game servers.
|
|
238
|
+
- **`secure-coding`** — general threat modeling; this skill keeps only the game-specific anti-cheat controls.
|
|
239
|
+
|
|
240
|
+
## Checklist
|
|
241
|
+
|
|
242
|
+
- [ ] Topology chosen (dedicated / listen / P2P) and the *same code* runs on server and listen host.
|
|
243
|
+
- [ ] Server-authoritative: clients send intent only; server validates and owns all gameplay state.
|
|
244
|
+
- [ ] Each networked value is either replicated (state/nouns) or an RPC (events/verbs) — not the wrong one.
|
|
245
|
+
- [ ] Ownership/authority set explicitly per object; authority-only logic guarded on the host.
|
|
246
|
+
- [ ] Owning-client movement predicted + reconciled; remote entities snapshot-interpolated.
|
|
247
|
+
- [ ] Hitscan uses server-side lag compensation; simulation on a fixed tick, decoupled from render FPS.
|
|
248
|
+
- [ ] Only need-to-know state replicated per client (relevancy/visibility scoped); RPCs rate-limited.
|
|
249
|
+
- [ ] Tested with simulated latency + packet loss across multiple instances, exercising the client path.
|
|
250
|
+
- [ ] No banned APIs (UNet/HLAPI, Godot 3.x `master`/`puppet`/`rset`, `Tick`-polled Unreal replication).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Eval harness — `gamedev-multiplayer` skill
|
|
2
|
+
|
|
3
|
+
`cases.yaml` is a human/CI-readable trigger-and-capability spec, not an automated runner. Load the full
|
|
4
|
+
skill-catalog descriptions into a routing agent and, for each `should_trigger` prompt, confirm this skill
|
|
5
|
+
fires (including the near-miss phrasings that never say "netcode"); for each `should_not_trigger` prompt,
|
|
6
|
+
confirm it stays silent and that whatever does fire matches the stated `route_to` sibling (godot, deployment,
|
|
7
|
+
gamedev-physics, webhooks, secure-coding). Because LLM routing is non-deterministic, run several trials and
|
|
8
|
+
take the majority. For the `capability` scenario, run it once with the skill body unavailable and once with
|
|
9
|
+
`SKILL.md` + references loaded, then check each output against `must_include` — the with-skill answer must
|
|
10
|
+
use current APIs (ENetMultiplayerPeer, `@rpc("any_peer", …)`, `is_server()` authority gate, no Godot 3.x
|
|
11
|
+
`master`/`puppet`/`rset`) and clearly beat the baseline. Keep `cases.yaml` in sync with SKILL.md's
|
|
12
|
+
"Fires on / When NOT" whenever scope changes.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
skill: gamedev-multiplayer
|
|
2
|
+
|
|
3
|
+
should_trigger:
|
|
4
|
+
- prompt: "I have a single-player platformer in Godot 4 and I want to add online co-op. How do I sync the two players' positions and let one host the game?"
|
|
5
|
+
why: "Adding multiplayer/sync to a game is the core trigger; should produce ENetMultiplayerPeer host/join + MultiplayerSynchronizer/authority guidance."
|
|
6
|
+
- prompt: "In my Unity shooter the client sets its own health with a NetworkVariable and players can cheat. How should authority actually work?"
|
|
7
|
+
why: "Server-authoritative / never-trust-the-client + NetworkVariable write permissions; center of gravity of this skill."
|
|
8
|
+
- prompt: "Players rubber-band and teleport around when there's lag in my online game. What's the right way to fix the jitter?"
|
|
9
|
+
why: "Symptom text for prediction/reconciliation + snapshot interpolation; routes here without the word 'netcode'."
|
|
10
|
+
- prompt: "Should I use a dedicated server or peer-to-peer for my PvP game, and how do I stop clients from cheating?"
|
|
11
|
+
why: "Topology + authority decision, the second core section; not an infra/hosting question about how to run the server."
|
|
12
|
+
- prompt: "In Unreal, how do I replicate a health value to all clients and run a RepNotify when it changes, and what's the difference between a Server RPC and a NetMulticast?"
|
|
13
|
+
why: "Unreal Actor replication mapping: ReplicatedUsing/DOREPLIFETIME + Server vs NetMulticast RPC semantics."
|
|
14
|
+
- prompt: "What tick rate should my authoritative server run at, and how do I test multiplayer locally with fake latency before I ship?"
|
|
15
|
+
why: "Tick/netrate + local multiplayer testing with simulated latency, both explicitly owned here."
|
|
16
|
+
- prompt: "Is ServerRpc/ClientRpc still the way to do RPCs in Unity Netcode for GameObjects, or is there a newer attribute?"
|
|
17
|
+
why: "Version-contract question the skill answers directly: unified [Rpc(SendTo.X)] preferred, legacy pair still supported, UNet removed."
|
|
18
|
+
|
|
19
|
+
should_not_trigger:
|
|
20
|
+
- prompt: "How do I read gamepad input and make my character jump in Godot 4?"
|
|
21
|
+
route_to: "godot"
|
|
22
|
+
why: "Single-player input/gameplay is the engine skill's job; no network dimension."
|
|
23
|
+
- prompt: "Set up a matchmaking backend and auto-scale dedicated game servers on a cloud provider with a lobby database."
|
|
24
|
+
route_to: "deployment"
|
|
25
|
+
why: "Server hosting/matchmaking/orchestration infra is delegated to deployment; this skill designs the in-game net layer, not the fleet."
|
|
26
|
+
- prompt: "My character controller clips through walls at high speed and collisions feel inconsistent — how do I fix the physics?"
|
|
27
|
+
route_to: "gamedev-physics"
|
|
28
|
+
why: "Collision/character-controller/determinism is physics; netcode consumes determinism but doesn't own the physics fix."
|
|
29
|
+
- prompt: "Build a realtime chat app with presence indicators over WebSockets for my website."
|
|
30
|
+
route_to: "webhooks"
|
|
31
|
+
why: "Generic web realtime that is not a game world/simulation is not this skill's domain."
|
|
32
|
+
- prompt: "Do a general threat model and authz review of my app's permission boundaries."
|
|
33
|
+
route_to: "secure-coding"
|
|
34
|
+
why: "Language/domain-agnostic threat modeling is secure-coding; this skill keeps only game-specific anti-cheat controls."
|
|
35
|
+
|
|
36
|
+
capability:
|
|
37
|
+
- scenario: "In Godot 4.x, wire a server-authoritative 'fire weapon' from a client so the server validates and applies the shot, with the same code working on a listen-server host. Show the RPC and the authority handling."
|
|
38
|
+
must_include:
|
|
39
|
+
- "Uses ENetMultiplayerPeer (create_server / create_client) assigned to multiplayer.multiplayer_peer — NOT the removed NetworkedMultiplayerENet"
|
|
40
|
+
- "Declares the intent RPC with @rpc(\"any_peer\", ...) so a client may call it, and the client sends it with rpc_id(1, ...) to the server"
|
|
41
|
+
- "Gates the body with multiplayer.is_server() (or is_multiplayer_authority) so authoritative logic runs once, correctly on a listen host"
|
|
42
|
+
- "Identifies the sender with multiplayer.get_remote_sender_id() and validates before applying (server decides the result, client sends intent only)"
|
|
43
|
+
- "Uses no Godot 3.x APIs (no master/puppet/remote keywords, no rset, no rpc_config as a legacy call)"
|
|
44
|
+
- "Distinguishes replicating authoritative state (e.g. via MultiplayerSynchronizer / a replicated var) from firing the one-shot RPC event"
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Godot 4.x high-level multiplayer
|
|
2
|
+
|
|
3
|
+
The high-level API sits on top of `MultiplayerPeer` and gives you three moving parts: **transport**
|
|
4
|
+
(`ENetMultiplayerPeer`), **events** (`@rpc`), and **state** (`MultiplayerSynchronizer` +
|
|
5
|
+
`MultiplayerSpawner`). Authority is per-node, set with `set_multiplayer_authority()`.
|
|
6
|
+
|
|
7
|
+
Everything here is Godot 4.x. Godot 3.x used `master`/`puppet`/`remote`/`remotesync` keywords, `rset`,
|
|
8
|
+
and `NetworkedMultiplayerENet` — **all gone**; never emit them.
|
|
9
|
+
|
|
10
|
+
## Transport & connection
|
|
11
|
+
|
|
12
|
+
```gdscript
|
|
13
|
+
const PORT := 9999
|
|
14
|
+
const MAX_PLAYERS := 8
|
|
15
|
+
|
|
16
|
+
func host() -> void:
|
|
17
|
+
var peer := ENetMultiplayerPeer.new()
|
|
18
|
+
var err := peer.create_server(PORT, MAX_PLAYERS)
|
|
19
|
+
if err != OK: push_error("host failed: %s" % err); return
|
|
20
|
+
multiplayer.multiplayer_peer = peer
|
|
21
|
+
|
|
22
|
+
func join(ip := "127.0.0.1") -> void:
|
|
23
|
+
var peer := ENetMultiplayerPeer.new()
|
|
24
|
+
peer.create_client(ip, PORT)
|
|
25
|
+
multiplayer.multiplayer_peer = peer
|
|
26
|
+
|
|
27
|
+
func _ready() -> void:
|
|
28
|
+
multiplayer.peer_connected.connect(_on_peer_connected) # server-side: a client joined
|
|
29
|
+
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
|
30
|
+
multiplayer.connected_to_server.connect(_on_connected) # client-side
|
|
31
|
+
multiplayer.server_disconnected.connect(_on_server_gone)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Useful `multiplayer` (SceneMultiplayer) members: `is_server()`, `get_unique_id()` (1 == server),
|
|
35
|
+
`get_remote_sender_id()` (valid only inside an RPC body), `multiplayer_peer`.
|
|
36
|
+
|
|
37
|
+
## RPCs — the `@rpc` annotation
|
|
38
|
+
|
|
39
|
+
Params are optional and **order-free**. Signature: mode, sync, transfer mode, channel.
|
|
40
|
+
|
|
41
|
+
- **mode**: `"authority"` (default — only the node's multiplayer authority may invoke it on remotes) or
|
|
42
|
+
`"any_peer"` (any client may invoke it — use this for client→server intent, then validate).
|
|
43
|
+
- **sync**: `"call_remote"` (default) or `"call_local"` (also run on the caller).
|
|
44
|
+
- **transfer**: `"unreliable"` (default), `"unreliable_ordered"`, or `"reliable"`.
|
|
45
|
+
- **channel**: an int, and it must be the **last** argument.
|
|
46
|
+
|
|
47
|
+
```gdscript
|
|
48
|
+
# Client asks the server to fire. any_peer + call_local so the host's own call runs too.
|
|
49
|
+
@rpc("any_peer", "call_local", "reliable")
|
|
50
|
+
func request_fire(target: Vector3) -> void:
|
|
51
|
+
if not multiplayer.is_server(): return # authority gate
|
|
52
|
+
var shooter := multiplayer.get_remote_sender_id()
|
|
53
|
+
if not _can_fire(shooter): return # server-side cooldown / validation
|
|
54
|
+
_apply_shot(shooter, target) # server mutates authoritative state
|
|
55
|
+
|
|
56
|
+
# Server → everyone, cosmetic only. Unreliable is fine for FX.
|
|
57
|
+
@rpc("authority", "call_local", "unreliable")
|
|
58
|
+
func play_muzzle_fx(at: Vector3) -> void:
|
|
59
|
+
_spawn_fx(at)
|
|
60
|
+
|
|
61
|
+
# Invoking: rpc() = all peers; rpc_id(peer, ...) = one peer; rpc_id(1, ...) = the server.
|
|
62
|
+
func _on_fire_pressed(target: Vector3) -> void:
|
|
63
|
+
rpc_id(1, "request_fire", target) # client sends intent to server
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Both sides must declare the **same** `@rpc` annotation on a function with the same name/path, or the call
|
|
67
|
+
is rejected. A missing/mismatched annotation is the usual "my RPC does nothing" cause.
|
|
68
|
+
|
|
69
|
+
## Authority
|
|
70
|
+
|
|
71
|
+
```gdscript
|
|
72
|
+
# On spawn, give the owning peer authority over its input node; server keeps authority over stats.
|
|
73
|
+
func setup(owner_peer_id: int) -> void:
|
|
74
|
+
$Input.set_multiplayer_authority(owner_peer_id) # recursive=true by default
|
|
75
|
+
# $Stats stays under server authority (peer id 1)
|
|
76
|
+
|
|
77
|
+
func _physics_process(delta: float) -> void:
|
|
78
|
+
if $Input.is_multiplayer_authority(): # only the owner reads local input
|
|
79
|
+
var dir := Input.get_vector("left","right","up","down")
|
|
80
|
+
rpc_id(1, "submit_input", dir) # send intent to server
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`set_multiplayer_authority(id, recursive := true)` sets it on the node (and children). Check with
|
|
84
|
+
`is_multiplayer_authority()`; read with `get_multiplayer_authority()`. Authority drives who a
|
|
85
|
+
`MultiplayerSynchronizer` streams *from* and who may call `"authority"`-mode RPCs.
|
|
86
|
+
|
|
87
|
+
## State — MultiplayerSynchronizer
|
|
88
|
+
|
|
89
|
+
Add a `MultiplayerSynchronizer` node, assign it a **replication config** (`SceneReplicationConfig`, edited
|
|
90
|
+
in the inspector: list the properties to sync, e.g. `position`, `rotation`, and per-property spawn vs sync).
|
|
91
|
+
It streams those properties **from the node's authority to the other peers** every replication interval.
|
|
92
|
+
|
|
93
|
+
- `replication_interval` — how often synced properties are sent (throttle to save bandwidth).
|
|
94
|
+
- `delta_interval` — for delta-synced properties.
|
|
95
|
+
- Visibility: `set_visibility_for(peer_id, visible)`, `public_visibility`, `update_visibility()` — scope
|
|
96
|
+
who receives a given synchronizer (relevancy / anti-wallhack).
|
|
97
|
+
|
|
98
|
+
Because it is authority-driven, an owner-authoritative synchronizer trusts the client's position — fine for
|
|
99
|
+
co-op, risky for competitive. For server-authoritative movement, keep authority on the server and have the
|
|
100
|
+
client send input via RPC (see prediction-and-latency.md).
|
|
101
|
+
|
|
102
|
+
## Spawning — MultiplayerSpawner
|
|
103
|
+
|
|
104
|
+
`MultiplayerSpawner` replicates *instancing*. Point its `spawn_path` at the parent node, register spawnable
|
|
105
|
+
scenes with `add_spawnable_scene("res://player.tscn")`, and when the authority instances a child under that
|
|
106
|
+
path (or you call `spawn(data)`), the same instance appears on every peer — including late joiners, which is
|
|
107
|
+
the big win over manually RPC-ing spawns.
|
|
108
|
+
|
|
109
|
+
```gdscript
|
|
110
|
+
@onready var spawner := $MultiplayerSpawner
|
|
111
|
+
|
|
112
|
+
func _ready() -> void:
|
|
113
|
+
spawner.spawn_function = _spawn_player # optional custom factory
|
|
114
|
+
|
|
115
|
+
func _on_peer_connected(id: int) -> void: # server only
|
|
116
|
+
if multiplayer.is_server():
|
|
117
|
+
spawner.spawn(id) # data passed to spawn_function on every peer
|
|
118
|
+
|
|
119
|
+
func _spawn_player(peer_id: int) -> Node:
|
|
120
|
+
var p := preload("res://player.tscn").instantiate()
|
|
121
|
+
p.name = str(peer_id)
|
|
122
|
+
p.setup(peer_id)
|
|
123
|
+
return p
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Gotchas
|
|
127
|
+
|
|
128
|
+
- Server (peer 1) is also a client on a listen host — always gate authority logic behind `is_server()`.
|
|
129
|
+
- `get_remote_sender_id()` is only meaningful *inside* an RPC; it returns 0 outside one.
|
|
130
|
+
- Node paths must match across peers for RPCs/synchronizers to resolve — spawn via `MultiplayerSpawner` so
|
|
131
|
+
names line up, and set `name` deterministically (e.g. the peer id).
|
|
132
|
+
- Reliable RPCs are ordered per channel and cost more; keep FX/heartbeats unreliable, keep state changes
|
|
133
|
+
reliable or (better) replicated.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Prediction, reconciliation & latency hiding
|
|
2
|
+
|
|
3
|
+
The whole reason netcode is hard: the speed of light. A round trip (RTT) of 60–150 ms is normal, and if the
|
|
4
|
+
owning client had to wait a full RTT to see its own movement, the game would feel broken. The techniques
|
|
5
|
+
below keep it responsive **without** giving up server authority. This is engine-agnostic theory; the
|
|
6
|
+
per-engine references show where the hooks live.
|
|
7
|
+
|
|
8
|
+
## The authoritative loop (mental model)
|
|
9
|
+
|
|
10
|
+
1. Client samples input each tick, tags it with a **sequence number**, sends it to the server, and **applies
|
|
11
|
+
it locally immediately** (prediction).
|
|
12
|
+
2. Server receives inputs, simulates them on its fixed tick, and produces authoritative state. It stamps each
|
|
13
|
+
state snapshot with the last input sequence number it processed for that client.
|
|
14
|
+
3. Client receives the authoritative snapshot, **reconciles**: snap to the server state, then re-apply every
|
|
15
|
+
input *after* the acknowledged sequence number (they haven't reached the server yet).
|
|
16
|
+
4. Other (remote) entities are rendered from buffered snapshots, **interpolated** slightly in the past.
|
|
17
|
+
|
|
18
|
+
Server is always the source of truth. The client only ever *guesses ahead* and gets corrected.
|
|
19
|
+
|
|
20
|
+
## 1. Client-side prediction
|
|
21
|
+
|
|
22
|
+
The owning client runs the same movement code the server will run, on the same fixed timestep, the instant it
|
|
23
|
+
presses a key — no waiting. Requirements:
|
|
24
|
+
|
|
25
|
+
- **Deterministic** movement/simulation for the predicted entity (same input + same state → same result).
|
|
26
|
+
This is why `gamedev-physics` determinism matters here.
|
|
27
|
+
- A **ring buffer** of unacknowledged inputs (`{seq, dt, input}`), kept until the server acknowledges them.
|
|
28
|
+
- Predict only what the client owns (its pawn). Never predict other players — interpolate them instead.
|
|
29
|
+
|
|
30
|
+
## 2. Server reconciliation
|
|
31
|
+
|
|
32
|
+
When an authoritative snapshot arrives tagged "processed up to input N":
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
clientState = serverState # trust the server
|
|
36
|
+
discard buffered inputs with seq <= N # server already applied these
|
|
37
|
+
for each remaining buffered input (seq > N):
|
|
38
|
+
clientState = simulate(clientState, input) # replay the unacknowledged ones
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If the replayed result matches what the client already had, the correction is invisible. If it diverges (the
|
|
42
|
+
server disagreed — lag, a missed collision, a rejected move), the entity snaps: the visible **rubber-band**.
|
|
43
|
+
Goal is to *minimize* divergence, not to suppress the correction — hiding a legitimate correction is how you
|
|
44
|
+
get players walking through walls on their screen.
|
|
45
|
+
|
|
46
|
+
- **Smoothing**: for small corrections, blend toward the corrected position over a few frames instead of
|
|
47
|
+
hard-snapping, so a 2 cm disagreement isn't a visible pop. Hard-snap large ones.
|
|
48
|
+
|
|
49
|
+
## 3. Snapshot interpolation (remote entities)
|
|
50
|
+
|
|
51
|
+
You receive remote entities' state only every send-tick (e.g. every 50 ms) — rendering the latest packet
|
|
52
|
+
directly makes them teleport. Instead, keep a small buffer and render them ~1–2 snapshots **in the past**:
|
|
53
|
+
|
|
54
|
+
- **Interpolation delay** ≈ 100 ms (2× send interval + jitter margin). Render remote entities at
|
|
55
|
+
`now − delay`, interpolating between the two snapshots that straddle that time.
|
|
56
|
+
- Trade-off: everyone else is shown slightly in the past. That is fine and standard.
|
|
57
|
+
- **Extrapolation** (dead reckoning) predicts a remote entity forward when a packet is late — cheaper on
|
|
58
|
+
bandwidth but wrong on direction changes; prefer interpolation and only extrapolate briefly to cover a gap.
|
|
59
|
+
|
|
60
|
+
So on one client, three clocks coexist: **its own pawn** = present (predicted), **remote entities** = past
|
|
61
|
+
(interpolated), **the server** = the arbiter reconciling the two.
|
|
62
|
+
|
|
63
|
+
## 4. Lag compensation (server-side rewind)
|
|
64
|
+
|
|
65
|
+
For instant/hitscan hits, the shooter aimed at where they *saw* the target — which is `interpolation delay +
|
|
66
|
+
½ RTT` in the past. If the server tests the hit against the target's *present* position, the shooter misses
|
|
67
|
+
despite a perfect aim. Fix: the server keeps a short **history** of every entity's recent positions and, when
|
|
68
|
+
processing a shot, **rewinds** the world to the shooter's view time before testing the hit.
|
|
69
|
+
|
|
70
|
+
- Store ~1 second of position history per entity (a ring of transforms per tick).
|
|
71
|
+
- On a shot: reconstruct positions at `clientRenderTime`, run the trace, then restore.
|
|
72
|
+
- Trade-off: the victim can be hit just after ducking behind cover ("shot behind the wall"). This is an
|
|
73
|
+
accepted fairness choice favoring the shooter; cap how far back you rewind to bound the unfairness.
|
|
74
|
+
|
|
75
|
+
## 5. Tick rate, send rate, netrate
|
|
76
|
+
|
|
77
|
+
- **Simulation tick** — fixed Hz the server steps the world (20 / 30 / 60+; higher = more precise + more CPU).
|
|
78
|
+
- **Send rate / snapshot rate** — how often state goes on the wire; often lower than sim tick, and independent
|
|
79
|
+
of client render FPS. This is the "netrate/updaterate/cmdrate" family in shooters.
|
|
80
|
+
- **Client render FPS** — decoupled from both; render via interpolation between fixed-step states.
|
|
81
|
+
- Bandwidth ≈ replicated-bytes × send-rate × relevant-peers. Shrink it with relevancy/visibility scoping,
|
|
82
|
+
quantized/delta-compressed fields, and lower send rates for distant or unimportant actors.
|
|
83
|
+
|
|
84
|
+
**Never run gameplay in a variable `_process`/`Update`/`Tick` on a networked game** — use a fixed timestep
|
|
85
|
+
(`_physics_process`, `NetworkUpdate`/`FixedUpdate`, Unreal's fixed sim) so prediction and reconciliation
|
|
86
|
+
replay identically on client and server.
|
|
87
|
+
|
|
88
|
+
## Alternative model: deterministic lockstep / rollback
|
|
89
|
+
|
|
90
|
+
For fighting games and some RTS: no per-frame state replication at all — every peer runs the identical
|
|
91
|
+
deterministic simulation and only **inputs** are exchanged. **Lockstep** waits for all inputs before advancing
|
|
92
|
+
(one slow peer stalls everyone). **Rollback** (GGPO-style) predicts absent inputs, then rolls back and
|
|
93
|
+
re-simulates when the real inputs arrive — excellent feel, but demands *bit-perfect determinism* (watch
|
|
94
|
+
floats, iteration order, RNG) and bounded state you can snapshot/restore cheaply. Choose it only when you can
|
|
95
|
+
guarantee that determinism; otherwise the client-server model above is the safer default.
|
|
96
|
+
|
|
97
|
+
## Checklist for a responsive authoritative game
|
|
98
|
+
|
|
99
|
+
- [ ] Owning pawn predicted locally on a fixed timestep; inputs buffered with sequence numbers.
|
|
100
|
+
- [ ] Server acks the last processed input seq in each snapshot; client replays unacknowledged inputs.
|
|
101
|
+
- [ ] Small corrections smoothed, large ones snapped; corrections never fully suppressed.
|
|
102
|
+
- [ ] Remote entities interpolated ~100 ms in the past from a snapshot buffer.
|
|
103
|
+
- [ ] Hitscan uses server-side rewind bounded to ~1 s of history.
|
|
104
|
+
- [ ] Sim tick / send rate / render FPS all decoupled; bandwidth scoped by relevancy.
|