@ericrisco/rsc 0.1.35 → 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 +8 -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/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/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/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/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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Dialogue tools — the four primitives, a pick-one guide, and one conversation in three formats
|
|
2
|
+
|
|
3
|
+
Deep dive for the SKILL's "Dialogue systems" section. **Engine-agnostic:** author dialogue in a portable format here; the engine skill (`godot`/`unity`/`unreal`) wires the runner, presenter, save/serialize, and any plugin. Do not write engine binding code in this skill.
|
|
4
|
+
|
|
5
|
+
## Every tool reduces to four primitives
|
|
6
|
+
|
|
7
|
+
Design against these, then map them onto whichever tool the project uses:
|
|
8
|
+
|
|
9
|
+
| Primitive | What it is | Ink | Yarn Spinner | Twine (Twee) | articy:draft X |
|
|
10
|
+
| --- | --- | --- | --- | --- | --- |
|
|
11
|
+
| **Node** | addressable unit of content | knot `=== name ===` / stitch `= name` | node with `title:` + `===` end | passage `:: Name` | flow / dialogue fragment |
|
|
12
|
+
| **Condition** | boolean over state gating content | `{ cond: ... }`, `{ cond: a \| b }` | `<<if $x>> ... <<endif>>` | `(if: $x)[...]` (Harlowe) / `<<if $x>>` (SugarCube) | condition node / expression on a pin |
|
|
13
|
+
| **Variable** | the story state read/written | `VAR x = 0`, `~ x = 1` | `<<set $x to 1>>` | `(set: $x to 1)` / `<<set $x = 1>>` | global variable in the variable set |
|
|
14
|
+
| **Effect** | side effect a node fires | `~ func()`, tags `#`, external funcs | `<<command>>`, functions, `<<set>>` | macros, `<<run>>` (SugarCube) | instruction node / script pin |
|
|
15
|
+
|
|
16
|
+
If you can express a conversation in these four primitives, it ports to any of the tools below.
|
|
17
|
+
|
|
18
|
+
## The tools (pointers, current as of 2026)
|
|
19
|
+
|
|
20
|
+
### Ink — inkle's narrative scripting language
|
|
21
|
+
- **Shape:** a writer-first *language* (not a visual editor). Text is the default; logic is woven in. Prose-heavy, highly expressive.
|
|
22
|
+
- **Core syntax:** knots `=== name ===`, stitches `= name`, diverts `-> name`, once-only choices `*`, sticky choices `+`, gathers `-`, weave (nested choices/gathers), `VAR`, `CONST`, `LIST` (state machines / multi-value), conditionals `{ }`, tunnels `-> x ->`, threads `<- x`, functions, tags `#`, glue `<>`.
|
|
23
|
+
- **Runtime:** compiles to JSON; run with **inkjs** (web/JS) or engine integrations (Unity's `ink-unity-integration`; community Godot integrations). **Inky** is the authoring editor with live preview.
|
|
24
|
+
- **Best for:** branching prose, interactive fiction, dialogue with lots of state and reincorporation. Weakest as a visual overview for non-writers.
|
|
25
|
+
|
|
26
|
+
### Yarn Spinner — the friendly dialogue tool (v3.x current)
|
|
27
|
+
- **Shape:** node-based, screenplay-ish; friendlier to designers and non-programmers than raw Ink. Nodes edit well in a graph or plain text.
|
|
28
|
+
- **Core syntax:** node = header (`title: Name`) + `---` + body + `===`; options with `->`; `<<set $x to 1>>`, `<<if>>/<<elseif>>/<<else>>/<<endif>>`, `<<jump Node>>`, `<<declare>>`, custom `<<commands>>`, functions, and inline markup `[b]...[/b]`.
|
|
29
|
+
- **Runtime & engines:** first-class **Unity** support; **Godot** via official C# (beta) and GDScript (alpha) integrations; **Unreal** integration in progress for 2026. String tables export for localization are built in.
|
|
30
|
+
- **Best for:** game dialogue with a designer-writer split, VO line management, and Unity projects.
|
|
31
|
+
|
|
32
|
+
### Twine — visual hypertext editor
|
|
33
|
+
- **Shape:** a **visual node editor** that publishes a self-contained HTML file. Lowest barrier to entry; excellent for prototyping and interactive fiction shipped as a web page.
|
|
34
|
+
- **Story formats matter:** scripting depends on the chosen format — **Harlowe** (default, macro-based `(set:)/(if:)`), **SugarCube** (`<<set>>/<<if>>`, save system, richer for games), **Snowman** (JS-forward), **Chapbook** (prose-forward). Pick the format for the job.
|
|
35
|
+
- **Twee** is Twine's plain-text form (`:: Passage` headers) — version-controllable and a good neutral interchange for design even if the game engine isn't Twine.
|
|
36
|
+
- **Best for:** prototyping branches fast, web-delivered IF, and design-time storyboarding. Not a drop-in engine dialogue runtime for a AAA build.
|
|
37
|
+
|
|
38
|
+
### articy:draft X — visual narrative database (commercial)
|
|
39
|
+
- **Shape:** a heavyweight **narrative DB + flow editor**. Models flow fragments, dialogue fragments, hubs, conditions, instructions, plus a full entity/location/variable database and templates. Built for large teams and large scripts.
|
|
40
|
+
- **Pipeline:** free **Unity** and **Unreal** importers pull dialogues, entities, variables, and localization in cleanly; a **Generic Engine Export** produces JSON (+ optional assets) for any custom engine. Runs on Windows and macOS.
|
|
41
|
+
- **Best for:** big branching RPGs, teams that need a shared narrative database, template-driven content, and translation at scale. Overkill for a small project.
|
|
42
|
+
|
|
43
|
+
## Pick one — a fast decision guide
|
|
44
|
+
|
|
45
|
+
| If… | Reach for |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| Prose-heavy IF, lots of state, writer-driven | **Ink** |
|
|
48
|
+
| Game dialogue, designer/writer split, Unity, VO + loc built in | **Yarn Spinner** |
|
|
49
|
+
| Rapid prototype, web-delivered, or teaching branching visually | **Twine** (SugarCube for game-like saves) |
|
|
50
|
+
| Large team, huge script, shared entity/variable DB, Unity/Unreal | **articy:draft X** |
|
|
51
|
+
| You just need a portable design artifact, engine undecided | **Twee** or an Ink file (both are plain text, diff-able) |
|
|
52
|
+
|
|
53
|
+
When the engine is fixed, let the **engine skill** confirm the best-supported integration; when it isn't, author in plain-text (Ink/Twee) so nothing locks you in.
|
|
54
|
+
|
|
55
|
+
## The same conversation, three formats
|
|
56
|
+
|
|
57
|
+
A guard who lets you pass only if `has_pass` is true; otherwise you can bribe (needs `gold >= 10`) or leave.
|
|
58
|
+
|
|
59
|
+
**Ink**
|
|
60
|
+
```ink
|
|
61
|
+
VAR has_pass = false
|
|
62
|
+
VAR gold = 0
|
|
63
|
+
|
|
64
|
+
=== gate ===
|
|
65
|
+
The guard blocks the door.
|
|
66
|
+
{ has_pass:
|
|
67
|
+
"Pass, citizen." -> town
|
|
68
|
+
}
|
|
69
|
+
* [Offer a bribe] { gold >= 10:
|
|
70
|
+
~ gold = gold - 10
|
|
71
|
+
The guard pockets it. -> town
|
|
72
|
+
- else:
|
|
73
|
+
"You're ten short." -> gate
|
|
74
|
+
}
|
|
75
|
+
* [Leave] -> street
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Yarn Spinner**
|
|
79
|
+
```yarn
|
|
80
|
+
title: Gate
|
|
81
|
+
---
|
|
82
|
+
The guard blocks the door.
|
|
83
|
+
<<if $has_pass>>
|
|
84
|
+
Guard: Pass, citizen.
|
|
85
|
+
<<jump Town>>
|
|
86
|
+
<<endif>>
|
|
87
|
+
-> Offer a bribe <<if $gold >= 10>>
|
|
88
|
+
<<set $gold = $gold - 10>>
|
|
89
|
+
The guard pockets it.
|
|
90
|
+
<<jump Town>>
|
|
91
|
+
-> Leave
|
|
92
|
+
<<jump Street>>
|
|
93
|
+
===
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Twee (SugarCube)**
|
|
97
|
+
```twee
|
|
98
|
+
:: Gate
|
|
99
|
+
The guard blocks the door.
|
|
100
|
+
<<if $has_pass>>Guard: "Pass, citizen." [[Town]]<<else>>\
|
|
101
|
+
[[Offer a bribe->Bribe]]
|
|
102
|
+
[[Leave->Street]]
|
|
103
|
+
<</if>>
|
|
104
|
+
|
|
105
|
+
:: Bribe
|
|
106
|
+
<<if $gold gte 10>><<set $gold to $gold - 10>>The guard pockets it. [[Town]]\
|
|
107
|
+
<<else>>"You're ten short." [[Gate]]<</if>>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Same four primitives (node / condition / variable / effect), three syntaxes. Design once; the engine skill binds it.
|
|
111
|
+
|
|
112
|
+
## Delivery notes (design-side)
|
|
113
|
+
|
|
114
|
+
- **Barks vs. conversations** — short, interruptible one-liners (barks) need a priority + cooldown model so they don't stack or repeat; full conversations need focus/turn-taking. Design them as different node types.
|
|
115
|
+
- **Line IDs & loc** — every player-facing line carries a stable id and lives in a string table (see the SKILL's localization notes). Ink tags, Yarn's string-table export, and articy's loc columns all support this; use them.
|
|
116
|
+
- **Never concatenate** sentence fragments across variables for grammar (gender/plural/word order) — author full templated lines with named placeholders.
|
|
117
|
+
- **Presentation is the engine's job** — typewriter effect, portraits, camera, choice-timer UI, skip/auto — hand these to the engine skill; keep the dialogue content pure.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Environmental & systemic storytelling — show, don't tell
|
|
2
|
+
|
|
3
|
+
Deep dive for the SKILL's "Environmental & systemic storytelling" section. The goal: deliver story *through the world and its rules* so the player assembles meaning and owns it, instead of being narrated at. Engine-agnostic; coordinate the *space* with `level-design` and the *rules* with `game-design`.
|
|
4
|
+
|
|
5
|
+
## Why show over tell
|
|
6
|
+
|
|
7
|
+
Told information is received; discovered information is *earned*. A player who infers "someone died here defending this door" from a skeleton, a spent revolver, and claw marks believes it more, and remembers it longer, than the same fact in a text box. Reserve telling (codex, narrator, log dumps) for what the world genuinely cannot carry — precise numbers, distant history, abstract rules.
|
|
8
|
+
|
|
9
|
+
**Show-don't-tell ladder** (prefer the top; fall back only when needed):
|
|
10
|
+
|
|
11
|
+
1. **Enacted** — it happens in front of the player through play (an ally sacrifices themselves in a scripted-but-interactive beat).
|
|
12
|
+
2. **Environmental** — the world's state implies the event (aftermath, traces, arrangement).
|
|
13
|
+
3. **Overheard / ambient** — barks, one-sided radio, NPCs talking to each other.
|
|
14
|
+
4. **Readable in-world** — item descriptions, signage, graffiti, terminals that *belong* to the world.
|
|
15
|
+
5. **Collectible logs** — audio logs, diaries, notes. Useful but the most "tell"-like; easy to overuse.
|
|
16
|
+
6. **Non-diegetic** — narrator, codex, cutscene exposition, HUD text. Last resort.
|
|
17
|
+
|
|
18
|
+
## Environmental storytelling — the technique catalog
|
|
19
|
+
|
|
20
|
+
- **Set dressing with intent.** Every prop can carry meaning: a single toy in a barracks, wedding photos in a looted home. Composition points the eye (leading lines, light, framing a key object) — coordinate with `level-design` and lighting.
|
|
21
|
+
- **Aftermath / the frozen moment.** Show the scene *after* the event and let the player reconstruct it: overturned table, blood trail, a meal half-eaten. The gap between "what I see" and "what happened" is the story.
|
|
22
|
+
- **Traces & evidence.** Bullet holes, scorch marks, footprints in dust, a barricade built from the wrong side. Physical cause-and-effect the player reads like a detective.
|
|
23
|
+
- **Spatial sequencing.** The *order* the player encounters space tells a story: descend from opulence into rot; a triumphant mural, then the mass grave behind it. The space is a sentence; its layout is the grammar (a `level-design` collaboration).
|
|
24
|
+
- **Object placement & juxtaposition.** Meaning from adjacency — a child's drawing pinned above a weapons cache; a prayer shrine facing a bricked-up window.
|
|
25
|
+
- **Readable descriptions.** Item and examine text that implies a life ("Dented canteen. Someone scratched three tally marks and stopped."). Small, optional, high-flavor; rewards curiosity without gating.
|
|
26
|
+
- **World-state change.** The strongest environmental beat: the space *changes* in response to the player or the plot (a hub burns, a garden regrows, a faction's banners replace another's). The player sees consequence written on the world.
|
|
27
|
+
- **Absence.** What's missing tells a story too — an empty crib, a cleared-out desk, a name scratched off a roster.
|
|
28
|
+
|
|
29
|
+
Guardrails:
|
|
30
|
+
|
|
31
|
+
- **Legibility vs. mystery.** Give enough signal that most players read the intended gist, with deeper layers for the attentive. If *no one* gets it, it's decoration, not storytelling; if it's spelled out, it's a text box.
|
|
32
|
+
- **Consistency = trust.** Once the world's props reliably mean things, players *read* every scene. One arbitrary/contradictory arrangement teaches them to stop looking.
|
|
33
|
+
- **Optional, not load-bearing.** Environmental story enriches; don't hide *required* plot solely in a missable detail. Critical beats need a guaranteed channel.
|
|
34
|
+
|
|
35
|
+
## Diegetic vs. non-diegetic delivery
|
|
36
|
+
|
|
37
|
+
| | Diegetic (in-world) | Non-diegetic (outside the fiction) |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| **Examples** | signage, barks, radio, environmental change, in-world terminals | narrator VO, codex/lore menu, HUD text, tutorial popups, cutscene title cards |
|
|
40
|
+
| **Feel** | immersive, earned, "the world tells me" | efficient, precise, "the game tells me" |
|
|
41
|
+
| **Use for** | mood, place, character, consequence, most story | exact numbers, dense history, rules the world can't show, accessibility |
|
|
42
|
+
| **Cost of overuse** | can be missed/ambiguous | breaks immersion; reads as a lecture |
|
|
43
|
+
|
|
44
|
+
**Diegetic first.** Push information into the world; drop to non-diegetic only for what the world can't carry. Note the honest exceptions: **accessibility** (subtitles, clear objective text, difficulty-scaled hints) and **usability** (a control prompt) are worth a non-diegetic cost — never sacrifice comprehension for purity.
|
|
45
|
+
|
|
46
|
+
## Systemic & emergent storytelling — the anecdote factory
|
|
47
|
+
|
|
48
|
+
Scripted story is *authored*; systemic story is *generated* by rules + player + world state producing events no writer placed. The famous colonist who starved defending the fridge, the rival who keeps hunting you across a sandbox — nobody wrote those; the systems did.
|
|
49
|
+
|
|
50
|
+
You don't write these stories. You design the **conditions and vocabulary** that let them happen and be legible — an "anecdote factory." This is primarily a `game-design` collaboration; your narrative-design job is to make the output *readable as story*.
|
|
51
|
+
|
|
52
|
+
Design levers:
|
|
53
|
+
|
|
54
|
+
- **Legible actors** — entities with names, traits, memory, and relationships (a guard who remembers you spared him). Persistent identity turns a system event into a *character* moment.
|
|
55
|
+
- **Consequence that persists** — the world remembers and reflects actions (reputation, scars, changed factions), so events chain into arcs instead of resetting.
|
|
56
|
+
- **Readable causality** — the player can *see why* something happened (a needs/mood readout, a reaction bark, a kill-cam). An unexplained emergent event is just noise; a readable one is an anecdote.
|
|
57
|
+
- **Framing & capture** — surfacing tools (event log, photo mode, relationship screen, killfeed, end-of-run summary) that let the player *notice and retell* the story the systems produced. The retelling is the payoff.
|
|
58
|
+
- **Meaningful randomness** — variety within authored bounds (curated event pools, weighted tables) so runs differ without becoming incoherent.
|
|
59
|
+
- **Interacting systems** — emergence needs ≥2 systems that touch (fire + wind + wood; hunger + weather + AI). One system alone produces events, not stories.
|
|
60
|
+
|
|
61
|
+
Guardrails:
|
|
62
|
+
|
|
63
|
+
- **Coherence budget.** Pure emergence drifts into nonsense; pure authorship can't surprise. Set authored guardrails (tone, bounds, curated pools) inside which systems play.
|
|
64
|
+
- **Framing is half the work.** An emergent event the player never notices or can't interpret isn't a story. Invest in the surfacing/retelling tools as much as the simulation.
|
|
65
|
+
- **Name things.** Named actors, places, and factions convert anonymous system state into story the player cares about and repeats.
|
|
66
|
+
|
|
67
|
+
## Where this hands off
|
|
68
|
+
|
|
69
|
+
- **`level-design`** — owns the physical space, blockout, sightlines, and encounter flow that environmental beats live inside. Co-own set-dressing that carries narrative.
|
|
70
|
+
- **`game-design`** — owns the systems whose interaction produces emergent story; you specify what must be *legible* and *persistent* for those events to read as narrative.
|
|
71
|
+
- **the engine skill** (`godot`/`unity`/`unreal`) — implements world-state changes, triggers, bark systems, event logs, and the persistence that makes consequence stick.
|
|
@@ -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.
|