summer-engine 1.3.1 → 1.3.2
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/CHANGELOG.md +67 -0
- package/package.json +5 -3
- package/skills/_meta/skill-test/SKILL.md +1 -1
- package/skills/_shared/summer-folder.md +44 -0
- package/skills/_tests/specs/art-direction.md +115 -0
- package/skills/_tests/specs/audio-direction.md +123 -0
- package/skills/_tests/specs/brainstorm-game.md +99 -0
- package/skills/_tests/specs/design-level.md +113 -0
- package/skills/_tests/specs/design-mechanic.md +112 -0
- package/skills/_tests/specs/design-npc.md +112 -0
- package/skills/_tests/specs/export-and-ship.md +114 -0
- package/skills/_tests/specs/fps-controller.md +5 -5
- package/skills/_tests/specs/setup-multiplayer.md +106 -0
- package/skills/_tests/specs/tune-performance.md +104 -0
- package/skills/_tests/specs/vfx.md +98 -0
- package/skills/ai-and-npcs/design-npc/SKILL.md +343 -0
- package/skills/audio/audio-direction/SKILL.md +258 -0
- package/skills/deployment/export-and-ship/SKILL.md +300 -0
- package/skills/gameplay-mechanics/design-mechanic/SKILL.md +265 -0
- package/skills/level-design/design-level/SKILL.md +266 -0
- package/skills/multiplayer-and-networking/setup-multiplayer/SKILL.md +328 -0
- package/skills/performance/tune-performance/SKILL.md +219 -0
- package/skills/rendering-and-lighting/art-direction/SKILL.md +284 -0
- package/skills/scene-and-project/brainstorm-game/SKILL.md +210 -0
- package/skills/visual-effects/vfx/SKILL.md +280 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design-level
|
|
3
|
+
description: Use when designing a single level — layout, pacing, encounters, secrets, reward gating. Outputs a level design doc and a concrete node-tree skeleton for `summer_create_scene`. Trigger on "design a level", "design level 1", "tutorial level", "boss arena", "design the encounter", "make a level layout".
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: [Cursor, Claude Code, Windsurf, Codex]
|
|
6
|
+
category: level-design
|
|
7
|
+
user-invocable: true
|
|
8
|
+
allowed-tools: Read Grep Glob Write Edit summer_get_scene_tree summer_create_scene summer_open_scene summer_add_node summer_set_prop summer_save_scene summer_get_script_errors
|
|
9
|
+
paths: ["**/*.tscn", "**/*.gd", ".summer/**", "levels/**"]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# /design-level — Design One Level
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
A level is a controlled sequence of player experiences in physical space. Layout, pacing, encounters, secrets, and reward gating must work together — a "fun layout" with a flat pacing curve still feels boring. This skill walks the six design beats of a single level and drops a node-tree skeleton ready for `summer_create_scene`.
|
|
17
|
+
|
|
18
|
+
**Core principle:** A level has a *pacing curve* and a *teaching arc*. If you can't draw the tension graph and name what the player learned by the end, the level isn't designed yet.
|
|
19
|
+
|
|
20
|
+
## Steps
|
|
21
|
+
|
|
22
|
+
### 1. Pin the level type
|
|
23
|
+
|
|
24
|
+
Ask exactly one question:
|
|
25
|
+
|
|
26
|
+
> What kind of level? Pick one: tutorial / hub / combat encounter / boss / exploration / puzzle / stealth / platforming-challenge / mixed.
|
|
27
|
+
|
|
28
|
+
Each type has different beats. Don't blend types in v1 — a "boss room with stealth and puzzles" is three levels poorly squeezed into one.
|
|
29
|
+
|
|
30
|
+
### 2. Read context
|
|
31
|
+
|
|
32
|
+
If `.summer/GameSoul.md` exists, read it. The level must serve the brief's core loop and the three mechanics.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
Read .summer/GameSoul.md
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If `.summer/mechanics/` exists, list the mechanics. Quote them back:
|
|
39
|
+
|
|
40
|
+
> Brief says mechanics are: dash, parry, double-jump. This level should *exercise* at least one and ideally combine two.
|
|
41
|
+
|
|
42
|
+
### 3. Define the teaching / experience goal
|
|
43
|
+
|
|
44
|
+
Ask plain:
|
|
45
|
+
|
|
46
|
+
> What does the player know by the end of this level that they didn't at the start?
|
|
47
|
+
|
|
48
|
+
Tutorial-level answer: "How to dash." Combat-encounter answer: "That parrying converts damage to mana, and that big enemies need parry, not dodge." Exploration answer: "That this region's secrets are hidden behind double-jumpable gaps."
|
|
49
|
+
|
|
50
|
+
If the user can't answer, the level has no purpose. Say it:
|
|
51
|
+
|
|
52
|
+
> If there's no answer to that question, the level is filler. Either give it a teaching goal, give it a *failure-state* goal (the player must die here to progress), or merge it into the next level.
|
|
53
|
+
|
|
54
|
+
### 4. Sketch the pacing curve
|
|
55
|
+
|
|
56
|
+
Draw it as text. Five beats, intensity 0-10:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
Intro Build Peak Release Hook
|
|
60
|
+
2 → 5 → 8 → 3 → 6
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For a 5-minute encounter level:
|
|
64
|
+
|
|
65
|
+
| Beat | Duration | Intensity | Player feels | Designer plants |
|
|
66
|
+
|---|---|---|---|---|
|
|
67
|
+
| Intro | 30s | 2 | Calm, oriented | Visual landmark for the goal direction |
|
|
68
|
+
| Build | 90s | 5 | Tension rising, small pressure | First weak enemy / first puzzle clue |
|
|
69
|
+
| Peak | 60s | 8 | Stakes high, mistakes hurt | Main encounter / hardest fight |
|
|
70
|
+
| Release | 60s | 3 | Catharsis, loot | Reward + breath room |
|
|
71
|
+
| Hook | 30s | 6 | Curiosity for next | Visible-but-locked secret or path |
|
|
72
|
+
|
|
73
|
+
Show the curve. Ask:
|
|
74
|
+
|
|
75
|
+
> Pacing above. Does the peak land where you want, or does the level need a second wave?
|
|
76
|
+
|
|
77
|
+
### 5. List encounters and secrets
|
|
78
|
+
|
|
79
|
+
Encounters = explicit player tests (combat, puzzle, platforming). Secrets = optional rewards that respect the pacing curve.
|
|
80
|
+
|
|
81
|
+
State them flat. For a combat encounter level:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
Encounters (in player-traversal order):
|
|
85
|
+
1. E1 — Tutorial enemy (1 weak grunt) at 0:30, in the open atrium. Teaches the basic attack.
|
|
86
|
+
2. E2 — Forced parry (mid-tier enemy with telegraphed wind-up) at 1:30, in a corridor with no dodge room. Teaches parry.
|
|
87
|
+
3. E3 — Mixed wave (2 grunts + 1 parry-required) at 3:00, in the peak arena. Tests both.
|
|
88
|
+
4. E4 — Mini-boss at 4:00 in the chamber. Combines.
|
|
89
|
+
|
|
90
|
+
Secrets (3 total — one per beat after intro):
|
|
91
|
+
- S1 (build beat): Behind a breakable wall in the corridor, holds 50 currency. Teaches "walls can break".
|
|
92
|
+
- S2 (peak beat): Visible from the arena, accessible only via parrying off an enemy. Rewards mastery.
|
|
93
|
+
- S3 (hook beat): Locked door, key in a future level. Plants curiosity for next level.
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 6. Define reward gating
|
|
97
|
+
|
|
98
|
+
What's behind which gate? Levels feel cheap when they hand out rewards without respecting the difficulty curve.
|
|
99
|
+
|
|
100
|
+
| Reward | Gate | Why |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| Heal pickup | After E2 | Player just learned parry; reward + safety net before the peak |
|
|
103
|
+
| New ability unlock | After mini-boss | Mastery-pinned; player demonstrates parry before getting upgrade |
|
|
104
|
+
| Currency cache | Behind S1 (breakable wall) | Optional; rewards observation |
|
|
105
|
+
| Story note | At the calm intro | Free; orients the player to the level's narrative |
|
|
106
|
+
|
|
107
|
+
Ask:
|
|
108
|
+
|
|
109
|
+
> Reward gating above. Anything missing or mis-placed?
|
|
110
|
+
|
|
111
|
+
### 7. Define the layout primitives
|
|
112
|
+
|
|
113
|
+
Every level is built from a small vocabulary of spaces. Name them. Most levels need 4-6 distinct spaces, no more.
|
|
114
|
+
|
|
115
|
+
For a combat-encounter level:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
Spaces (node-tree skeleton):
|
|
119
|
+
World (Node3D)
|
|
120
|
+
├── Geometry (Node3D)
|
|
121
|
+
│ ├── IntroAtrium (Node3D) # open, calm, well-lit
|
|
122
|
+
│ ├── Corridor (Node3D) # tight, forces parry
|
|
123
|
+
│ ├── PeakArena (Node3D) # large, multiple cover, exit visible
|
|
124
|
+
│ └── BossChamber (Node3D) # contained, single exit, dramatic lighting
|
|
125
|
+
├── Spawns (Node3D)
|
|
126
|
+
│ ├── PlayerSpawn (Node3D) # marker
|
|
127
|
+
│ ├── E1_Spawn (Node3D)
|
|
128
|
+
│ ├── E2_Spawn (Node3D)
|
|
129
|
+
│ ├── E3_Wave_Spawns (Node3D)
|
|
130
|
+
│ └── MiniBoss_Spawn (Node3D)
|
|
131
|
+
├── Triggers (Node3D)
|
|
132
|
+
│ ├── E2_GateTrigger (Area3D) # locks door behind player on entry
|
|
133
|
+
│ ├── PeakWaveTrigger (Area3D)
|
|
134
|
+
│ └── BossDoorTrigger (Area3D)
|
|
135
|
+
├── Pickups (Node3D)
|
|
136
|
+
│ ├── HealAfterE2 (Node3D)
|
|
137
|
+
│ └── CurrencyS1 (Node3D)
|
|
138
|
+
├── Secrets (Node3D)
|
|
139
|
+
│ ├── S1_BreakableWall (StaticBody3D)
|
|
140
|
+
│ ├── S2_ParryLedge (StaticBody3D)
|
|
141
|
+
│ └── S3_LockedDoor (StaticBody3D)
|
|
142
|
+
└── Lighting (Node3D) # WorldEnvironment + DirectionalLight3D
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Show the skeleton. Ask:
|
|
146
|
+
|
|
147
|
+
> Tree above. May I create the level scene `levels/<level-name>.tscn` with this skeleton?
|
|
148
|
+
|
|
149
|
+
### 8. Scaffold via MCP
|
|
150
|
+
|
|
151
|
+
**Preferred (Summer MCP):**
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
summer_create_scene(path="res://levels/level_01.tscn", root_type="Node3D", root_name="World")
|
|
155
|
+
summer_add_node(parent="./World", type="Node3D", name="Geometry")
|
|
156
|
+
summer_add_node(parent="./World/Geometry", type="Node3D", name="IntroAtrium")
|
|
157
|
+
summer_add_node(parent="./World/Geometry", type="Node3D", name="Corridor")
|
|
158
|
+
summer_add_node(parent="./World/Geometry", type="Node3D", name="PeakArena")
|
|
159
|
+
summer_add_node(parent="./World/Geometry", type="Node3D", name="BossChamber")
|
|
160
|
+
summer_add_node(parent="./World", type="Node3D", name="Spawns")
|
|
161
|
+
summer_add_node(parent="./World/Spawns", type="Node3D", name="PlayerSpawn")
|
|
162
|
+
summer_add_node(parent="./World/Spawns", type="Node3D", name="E1_Spawn")
|
|
163
|
+
# ... and so on
|
|
164
|
+
summer_add_node(parent="./World", type="Node3D", name="Triggers")
|
|
165
|
+
summer_add_node(parent="./World/Triggers", type="Area3D", name="E2_GateTrigger")
|
|
166
|
+
# ... etc.
|
|
167
|
+
summer_add_node(parent="./World", type="Node3D", name="Lighting")
|
|
168
|
+
summer_add_node(parent="./World/Lighting", type="WorldEnvironment", name="WorldEnvironment")
|
|
169
|
+
summer_add_node(parent="./World/Lighting", type="DirectionalLight3D", name="Sun")
|
|
170
|
+
summer_save_scene
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
This is a **skeleton, not playable geometry**. Markers and Area3Ds, no meshes, no enemy scripts. Implementation in subsequent sessions / templates.
|
|
174
|
+
|
|
175
|
+
**Fallback (no MCP):** Print the `.tscn` file contents for the user to save manually.
|
|
176
|
+
|
|
177
|
+
### 9. Save the level design doc
|
|
178
|
+
|
|
179
|
+
Always write the design doc — it's the artifact that drives implementation. Path: `.summer/levels/<level-name>.md`.
|
|
180
|
+
|
|
181
|
+
```markdown
|
|
182
|
+
# Level: <Name>
|
|
183
|
+
|
|
184
|
+
**Type:** combat encounter
|
|
185
|
+
**Duration target:** 5 min
|
|
186
|
+
**Teaching goal:** Player learns parry and that parrying mid-tier enemies is required, not optional.
|
|
187
|
+
|
|
188
|
+
**Pacing curve:** 2 → 5 → 8 → 3 → 6
|
|
189
|
+
**Beats:** Intro (30s) | Build (90s) | Peak (60s) | Release (60s) | Hook (30s)
|
|
190
|
+
|
|
191
|
+
## Encounters
|
|
192
|
+
- E1 — Tutorial grunt @ 0:30 (IntroAtrium)
|
|
193
|
+
- E2 — Forced parry @ 1:30 (Corridor)
|
|
194
|
+
- E3 — Mixed wave @ 3:00 (PeakArena)
|
|
195
|
+
- E4 — Mini-boss @ 4:00 (BossChamber)
|
|
196
|
+
|
|
197
|
+
## Secrets
|
|
198
|
+
- S1 — Breakable wall (Corridor) → 50 currency
|
|
199
|
+
- S2 — Parry-ledge (PeakArena) → cosmetic
|
|
200
|
+
- S3 — Locked door (BossChamber) → next level
|
|
201
|
+
|
|
202
|
+
## Reward Gating
|
|
203
|
+
- Heal pickup after E2
|
|
204
|
+
- Ability unlock after mini-boss
|
|
205
|
+
|
|
206
|
+
## Spaces
|
|
207
|
+
1. IntroAtrium — open, calm
|
|
208
|
+
2. Corridor — tight, forces parry
|
|
209
|
+
3. PeakArena — large with cover
|
|
210
|
+
4. BossChamber — contained, dramatic
|
|
211
|
+
|
|
212
|
+
## Open questions
|
|
213
|
+
<list anything the user couldn't answer>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Ask:
|
|
217
|
+
|
|
218
|
+
> May I create `.summer/levels/<level-name>.md` with this design doc?
|
|
219
|
+
|
|
220
|
+
### 10. Route to the next step
|
|
221
|
+
|
|
222
|
+
End with:
|
|
223
|
+
|
|
224
|
+
> Skeleton + design doc saved. Next:
|
|
225
|
+
> - `/summer:design-mechanic` to design any mechanics this level requires that aren't designed yet.
|
|
226
|
+
> - `/summer:design-npc` to design the enemies (E1-E4) in detail.
|
|
227
|
+
> - `/summer:art-direction` if the visual style isn't pinned yet.
|
|
228
|
+
> - Or open `levels/<level-name>.tscn` and start blocking out geometry.
|
|
229
|
+
|
|
230
|
+
## Common mistakes
|
|
231
|
+
|
|
232
|
+
| Mistake | Fix |
|
|
233
|
+
|---|---|
|
|
234
|
+
| No teaching goal | Every level teaches something or fails. State it explicitly. |
|
|
235
|
+
| Flat pacing curve | If intensity stays at 5 the whole time, you have a hallway, not a level. |
|
|
236
|
+
| Too many spaces | 4-6 distinct spaces max. More fragments the player's mental map. |
|
|
237
|
+
| Encounters not tied to mechanics | E2 should *require* parry, not just allow it. Forced use teaches; optional use doesn't. |
|
|
238
|
+
| Secrets ignore the pacing curve | Don't put a major secret in the calm intro — players aren't curious yet. |
|
|
239
|
+
| Reward gating too generous | Free heals before the peak deflate the peak. Gate them after the test. |
|
|
240
|
+
| Blending level types | Stealth + puzzle + boss in one level = three half-baked levels. Pick one. |
|
|
241
|
+
| Building geometry before the skeleton | Skeleton-first lets you iterate on layout cheaply. Geometry comes after the design is locked. |
|
|
242
|
+
| Calling SetResourceProperty on inline sub_resources | Silent fail. See `_shared/mcp-tools-reference.md` § Trap. |
|
|
243
|
+
|
|
244
|
+
## Collaborative protocol
|
|
245
|
+
|
|
246
|
+
This skill creates a new scene file (`.tscn`) and a design doc (`.md`). Group writes per phase. Always ask before `summer_create_scene`. See `_shared/collaborative-protocol.md`.
|
|
247
|
+
|
|
248
|
+
## Want a working starter?
|
|
249
|
+
|
|
250
|
+
This is a workflow that produces a skeleton scene. For runnable level examples, see:
|
|
251
|
+
|
|
252
|
+
- `template-id: template-3d-fps` (https://github.com/SummerEngine/FPS-template-Summer-Engine) ships a sample arena.
|
|
253
|
+
- `template-id: template-3d-platformer` (TBD) for jump-based level samples.
|
|
254
|
+
|
|
255
|
+
The skeleton this skill produces is meant to be filled in via `/summer:design-mechanic`, gridmap blockout, or a template merge.
|
|
256
|
+
|
|
257
|
+
## See also
|
|
258
|
+
|
|
259
|
+
- `_shared/collaborative-protocol.md`
|
|
260
|
+
- `_shared/mcp-tools-reference.md`
|
|
261
|
+
- `_shared/godot-version.md` — Godot 4.5 API notes
|
|
262
|
+
- `scene-and-project/brainstorm-game/SKILL.md` — produces `.summer/GameSoul.md`
|
|
263
|
+
- `gameplay-mechanics/design-mechanic/SKILL.md` — designs the verbs the level exercises
|
|
264
|
+
- `ai-and-npcs/design-npc/SKILL.md` — designs the enemies the encounters reference
|
|
265
|
+
- `rendering-and-lighting/3d-lighting/SKILL.md` — sets the lighting in `WorldEnvironment` + lights
|
|
266
|
+
- `rendering-and-lighting/art-direction/SKILL.md` — defines the visual style the level expresses
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: setup-multiplayer
|
|
3
|
+
description: Use when the user wants to add multiplayer to an existing single-player game — co-op LAN, co-op online, competitive PvP, or lobbies/matchmaking. Strong opinion that you start with Godot's high-level MultiplayerAPI plus MultiplayerSpawner and MultiplayerSynchronizer; only roll custom networking with a justified reason. Walks peer authority, RPC patterns, replication graph, and irreversible architecture decisions. Trigger on "multiplayer", "co-op", "PvP", "online", "netcode", "rollback", "MultiplayerAPI", "host migration", "matchmaking".
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: [Cursor, Claude Code, Windsurf, Codex]
|
|
6
|
+
category: multiplayer-and-networking
|
|
7
|
+
user-invocable: true
|
|
8
|
+
allowed-tools: Read Grep Edit Write summer_get_scene_tree summer_inspect_node summer_add_node summer_set_prop summer_connect_signal summer_save_scene summer_get_script_errors summer_project_setting summer_play summer_stop
|
|
9
|
+
paths: ["**/*.gd", "**/*.tscn", "**/*.tres", "project.godot"]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# /setup-multiplayer — Add Multiplayer Without Repainting the Project
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
Multiplayer is a one-way door. Pick the wrong authority model and refactoring later is brutal. This skill enforces an opinionated path: **start with Godot's high-level MultiplayerAPI + MultiplayerSpawner + MultiplayerSynchronizer + ENet**. Don't roll custom networking unless you have a measured reason. Make the irreversible decisions up front, in writing.
|
|
17
|
+
|
|
18
|
+
**Core principle:** the architecture decision (peer authority vs server authority) is harder than the code. Lock it before touching a single RPC.
|
|
19
|
+
|
|
20
|
+
## Steps
|
|
21
|
+
|
|
22
|
+
### 1. Ask the four blocking questions
|
|
23
|
+
|
|
24
|
+
Before any code:
|
|
25
|
+
|
|
26
|
+
> 1. What kind of multiplayer? co-op LAN / co-op online / competitive PvP / lobbies + matchmaking
|
|
27
|
+
> 2. How many players? 2 / 2–4 / 8+
|
|
28
|
+
> 3. Who runs the simulation? host (one of the players) / dedicated server / authoritative peer-to-peer
|
|
29
|
+
> 4. What's the network you're targeting? same Wi-Fi / public internet with ~80ms RTT / mobile
|
|
30
|
+
|
|
31
|
+
Wait. Each answer flips an irreversible knob.
|
|
32
|
+
|
|
33
|
+
### 2. Pick the authority model — irreversible decision
|
|
34
|
+
|
|
35
|
+
**Lock it now.** Refactoring authority is a project-killer.
|
|
36
|
+
|
|
37
|
+
| Model | Best for | Pros | Cons |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| **Host authority** (one player runs the sim) | Co-op (2–4 players), casual PvP, friends-only | Cheapest. No server bill. Host migration possible. | Host advantage (no lag for them). Cheating possible. |
|
|
40
|
+
| **Dedicated server authority** | Competitive PvP, matchmaking, > 4 players | Fair. Anti-cheat possible. Stable. | Server bill. Need a deploy story. Higher complexity. |
|
|
41
|
+
| **Peer-to-peer with shared authority** | Lock-step strategy / RTS, fighting games (rollback) | Symmetric latency. No "host advantage". | Determinism is a project on its own. Hard to debug. |
|
|
42
|
+
|
|
43
|
+
**Default recommendation:** host authority for co-op (2–4 players), dedicated server for everything else. If user says "competitive PvP" + "lobbies": dedicated.
|
|
44
|
+
|
|
45
|
+
State the choice explicitly:
|
|
46
|
+
|
|
47
|
+
> Going with **host authority** for 2–4 player co-op. The first player to start a game is the host. Their scene tree is the source of truth; clients receive replicated state and send input RPCs. Host migration is hard with this model — if the host quits, the game ends. Confirm?
|
|
48
|
+
|
|
49
|
+
### 3. Pick the transport
|
|
50
|
+
|
|
51
|
+
Almost always: **ENetMultiplayerPeer**. It's UDP with reliability layers, ships with Godot, and just works for < 32 peers.
|
|
52
|
+
|
|
53
|
+
When to switch:
|
|
54
|
+
- **WebSocketMultiplayerPeer**: if you need the game to run in the browser (HTML5 export). ENet doesn't work in browsers.
|
|
55
|
+
- **WebRTCMultiplayerPeer**: if you need browser AND P2P (no central server). Higher complexity.
|
|
56
|
+
- **Custom transport**: never, unless you've shipped multiplayer before.
|
|
57
|
+
|
|
58
|
+
### 4. Pick what to replicate — the replication graph
|
|
59
|
+
|
|
60
|
+
Replicate the *minimum*. Every replicated field is bandwidth + bug surface.
|
|
61
|
+
|
|
62
|
+
For a typical co-op shooter:
|
|
63
|
+
|
|
64
|
+
| Object | Replicated state | Replication frequency | Authority |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| Player position / rotation | x, y, z, yaw | 20 Hz | Owner peer |
|
|
67
|
+
| Player health | int | On change | Server/host |
|
|
68
|
+
| Player input intent | move_dir, jump, fire | 30 Hz | Owner peer (sends to host) |
|
|
69
|
+
| Enemy state | position, hp, FSM state | 10 Hz | Host only |
|
|
70
|
+
| World items (pickups) | spawn / despawn events | On change | Host only |
|
|
71
|
+
| UI / menus | nothing | nothing | Local only |
|
|
72
|
+
| Particles, decorative VFX | nothing | nothing | Local only (each peer plays its own) |
|
|
73
|
+
|
|
74
|
+
**Don't replicate:**
|
|
75
|
+
- Local UI state.
|
|
76
|
+
- Cosmetic-only VFX (each peer simulates locally).
|
|
77
|
+
- Camera / view position (each peer has their own camera).
|
|
78
|
+
- Audio (use 3D audio with replicated positions, not replicate the AudioStreamPlayer).
|
|
79
|
+
|
|
80
|
+
### 5. Read the scene to find what changes
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
summer_get_scene_tree
|
|
84
|
+
summer_inspect_node "./World/Player"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Note: the existing single-player Player needs to become a per-peer Player. The path is to spawn one Player per connected peer.
|
|
88
|
+
|
|
89
|
+
### 6. Propose the change set
|
|
90
|
+
|
|
91
|
+
> I'm about to:
|
|
92
|
+
> - Add a `Network` autoload (`scripts/network.gd`) that owns peer setup + spawn lifecycle.
|
|
93
|
+
> - Convert `./World/Player` from a static scene node into a `MultiplayerSpawner`-managed instance, spawned per peer on join.
|
|
94
|
+
> - Add a `MultiplayerSynchronizer` to the player scene replicating `position`, `rotation.y`, `health`.
|
|
95
|
+
> - Convert player input from local → input intent RPC sent to the authority.
|
|
96
|
+
> - Add a basic host/join UI on the main menu.
|
|
97
|
+
>
|
|
98
|
+
> Authority model: host. Transport: ENet. Tick: 30 Hz input, 20 Hz position. Confirm before I touch anything?
|
|
99
|
+
|
|
100
|
+
### 7. Build it
|
|
101
|
+
|
|
102
|
+
#### a) Network autoload
|
|
103
|
+
|
|
104
|
+
`scripts/network.gd`:
|
|
105
|
+
|
|
106
|
+
```gdscript
|
|
107
|
+
class_name Network
|
|
108
|
+
extends Node
|
|
109
|
+
|
|
110
|
+
const PORT := 7000
|
|
111
|
+
const MAX_PLAYERS := 4
|
|
112
|
+
|
|
113
|
+
signal player_joined(peer_id: int)
|
|
114
|
+
signal player_left(peer_id: int)
|
|
115
|
+
|
|
116
|
+
@export var player_scene: PackedScene
|
|
117
|
+
|
|
118
|
+
var _peer: MultiplayerPeer
|
|
119
|
+
|
|
120
|
+
func host_game() -> Error:
|
|
121
|
+
var enet := ENetMultiplayerPeer.new()
|
|
122
|
+
var err := enet.create_server(PORT, MAX_PLAYERS - 1)
|
|
123
|
+
if err != OK:
|
|
124
|
+
return err
|
|
125
|
+
_peer = enet
|
|
126
|
+
multiplayer.multiplayer_peer = _peer
|
|
127
|
+
multiplayer.peer_connected.connect(_on_peer_connected)
|
|
128
|
+
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
|
129
|
+
# Host spawns its own player at id=1 (server id)
|
|
130
|
+
_spawn_player(1)
|
|
131
|
+
return OK
|
|
132
|
+
|
|
133
|
+
func join_game(address: String) -> Error:
|
|
134
|
+
var enet := ENetMultiplayerPeer.new()
|
|
135
|
+
var err := enet.create_client(address, PORT)
|
|
136
|
+
if err != OK:
|
|
137
|
+
return err
|
|
138
|
+
_peer = enet
|
|
139
|
+
multiplayer.multiplayer_peer = _peer
|
|
140
|
+
return OK
|
|
141
|
+
|
|
142
|
+
func _on_peer_connected(peer_id: int) -> void:
|
|
143
|
+
# Only the host spawns players. Clients receive via MultiplayerSpawner.
|
|
144
|
+
if multiplayer.is_server():
|
|
145
|
+
_spawn_player(peer_id)
|
|
146
|
+
player_joined.emit(peer_id)
|
|
147
|
+
|
|
148
|
+
func _on_peer_disconnected(peer_id: int) -> void:
|
|
149
|
+
var node_name := str(peer_id)
|
|
150
|
+
var players := get_tree().get_first_node_in_group("players_root")
|
|
151
|
+
if players != null and players.has_node(node_name):
|
|
152
|
+
players.get_node(node_name).queue_free()
|
|
153
|
+
player_left.emit(peer_id)
|
|
154
|
+
|
|
155
|
+
func _spawn_player(peer_id: int) -> void:
|
|
156
|
+
var player: Node = player_scene.instantiate()
|
|
157
|
+
player.name = str(peer_id) # name = peer id; sync by name
|
|
158
|
+
player.set_multiplayer_authority(peer_id)
|
|
159
|
+
var players_root := get_tree().get_first_node_in_group("players_root")
|
|
160
|
+
players_root.add_child(player, true)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Register as autoload in `project.godot`:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
summer_project_setting(name="autoload/Network", value="*res://scripts/network.gd")
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### b) Player scene with MultiplayerSynchronizer
|
|
170
|
+
|
|
171
|
+
In the player scene, add:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
Player (CharacterBody3D)
|
|
175
|
+
├── CollisionShape3D
|
|
176
|
+
├── Camera3D # local-only, attach in script if peer is local
|
|
177
|
+
├── MeshInstance3D
|
|
178
|
+
└── MultiplayerSynchronizer
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
summer_add_node(parent="./World/Players/Player", type="MultiplayerSynchronizer", name="Sync")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Configure the synchronizer to replicate `position`, `rotation.y`, `health`. Save as a `SceneReplicationConfig.tres` (NOT inline sub_resource — silent-fail trap):
|
|
186
|
+
|
|
187
|
+
```gdscript
|
|
188
|
+
# scripts/player_net.gd
|
|
189
|
+
extends CharacterBody3D
|
|
190
|
+
|
|
191
|
+
@export var move_speed: float = 5.0
|
|
192
|
+
@export var max_health: int = 100
|
|
193
|
+
|
|
194
|
+
@onready var camera: Camera3D = $Camera3D
|
|
195
|
+
@onready var sync: MultiplayerSynchronizer = $Sync
|
|
196
|
+
|
|
197
|
+
var health: int = 100
|
|
198
|
+
|
|
199
|
+
func _ready() -> void:
|
|
200
|
+
health = max_health
|
|
201
|
+
var auth := get_multiplayer_authority()
|
|
202
|
+
var is_local := auth == multiplayer.get_unique_id()
|
|
203
|
+
camera.current = is_local
|
|
204
|
+
set_physics_process(is_local) # only the owner runs movement
|
|
205
|
+
set_process_input(is_local)
|
|
206
|
+
|
|
207
|
+
func _physics_process(delta: float) -> void:
|
|
208
|
+
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
|
209
|
+
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
|
|
210
|
+
if direction:
|
|
211
|
+
velocity.x = direction.x * move_speed
|
|
212
|
+
velocity.z = direction.z * move_speed
|
|
213
|
+
else:
|
|
214
|
+
velocity.x = move_toward(velocity.x, 0, move_speed)
|
|
215
|
+
velocity.z = move_toward(velocity.z, 0, move_speed)
|
|
216
|
+
if not is_on_floor():
|
|
217
|
+
velocity.y -= 9.8 * delta
|
|
218
|
+
move_and_slide()
|
|
219
|
+
# position is replicated automatically via MultiplayerSynchronizer
|
|
220
|
+
|
|
221
|
+
@rpc("any_peer", "call_local", "reliable")
|
|
222
|
+
func take_damage(amount: int) -> void:
|
|
223
|
+
if not multiplayer.is_server():
|
|
224
|
+
return # only host applies damage
|
|
225
|
+
health = max(0, health - amount)
|
|
226
|
+
if health == 0:
|
|
227
|
+
rpc("on_died")
|
|
228
|
+
|
|
229
|
+
@rpc("authority", "call_local", "reliable")
|
|
230
|
+
func on_died() -> void:
|
|
231
|
+
queue_free()
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### c) RPC patterns — the four flavors
|
|
235
|
+
|
|
236
|
+
Lock the conventions:
|
|
237
|
+
|
|
238
|
+
| RPC config | Use case |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `@rpc("authority", "call_local", "reliable")` | Host tells everyone something happened (health change result, death) |
|
|
241
|
+
| `@rpc("any_peer", "call_remote", "unreliable")` | Frequent input intent (movement, look) — drop is OK |
|
|
242
|
+
| `@rpc("any_peer", "call_remote", "reliable")` | Rare input event (fire weapon, pick up item) — drop NOT OK |
|
|
243
|
+
| `@rpc("any_peer", "call_local", "reliable")` | Player asks host to do something (request damage) |
|
|
244
|
+
|
|
245
|
+
**Authority validation is mandatory.** Inside any `any_peer` RPC, check `multiplayer.get_remote_sender_id()` and validate the sender owns the action.
|
|
246
|
+
|
|
247
|
+
```gdscript
|
|
248
|
+
@rpc("any_peer", "call_local", "reliable")
|
|
249
|
+
func request_fire() -> void:
|
|
250
|
+
var sender := multiplayer.get_remote_sender_id()
|
|
251
|
+
if sender != get_multiplayer_authority():
|
|
252
|
+
return # someone else is trying to control this player; drop
|
|
253
|
+
# ... actually fire
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
#### d) Lag compensation outline
|
|
257
|
+
|
|
258
|
+
Defer to a specialist skill, but flag the basics:
|
|
259
|
+
|
|
260
|
+
- For projectiles: spawn locally on owner's machine at fire time, replicate the spawn event with a timestamp. Other peers fast-forward the projectile by their RTT.
|
|
261
|
+
- For hit detection: server-authoritative + simple latency tolerance (host accepts hits within ~100ms past server time).
|
|
262
|
+
- For movement: client-side prediction + server reconciliation is the standard model. Godot doesn't ship this — you write it. If users ask for "competitive PvP shooter", say: this is a deeper specialist topic and link `multiplayer-and-networking/lag-compensation/SKILL.md` when it ships.
|
|
263
|
+
|
|
264
|
+
### 8. Save and verify
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
summer_save_scene
|
|
268
|
+
summer_get_script_errors
|
|
269
|
+
summer_play
|
|
270
|
+
# user tests by running two instances locally (Project → Run Multiple Instances → 2)
|
|
271
|
+
summer_stop
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Test checklist:
|
|
275
|
+
- [ ] Two instances connect (one hosts, one joins).
|
|
276
|
+
- [ ] Each instance sees its own player as local (camera attached, can move).
|
|
277
|
+
- [ ] Each instance sees the other player move smoothly (sync at 20 Hz).
|
|
278
|
+
- [ ] Damage RPC routes through host (not double-applied on each peer).
|
|
279
|
+
- [ ] Disconnect cleans up the disconnected player's node.
|
|
280
|
+
|
|
281
|
+
## Common mistakes
|
|
282
|
+
|
|
283
|
+
| Don't | Do | Why |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| Decide authority model halfway through | Lock it in step 2 before touching code | Refactoring authority is a project-killer |
|
|
286
|
+
| Replicate every property | Replicate position, rotation, health, that's it | Bandwidth + bug surface |
|
|
287
|
+
| Run physics on every peer for every player | Physics on owner only, others receive transform | Otherwise jitter and divergence |
|
|
288
|
+
| Trust the client | Validate `get_remote_sender_id()` in every `any_peer` RPC | Cheaters route through the wrong RPC |
|
|
289
|
+
| Inline `SceneReplicationConfig` sub_resource | Save as standalone `.tres` | Silent-fail trap (see `_shared/mcp-tools-reference.md`) |
|
|
290
|
+
| Roll custom transport | ENet (or WebSocket for browser) | You will hit edge cases ENet already solved |
|
|
291
|
+
| Skip authority check in damage RPC | `if not multiplayer.is_server(): return` at the top of damage handlers | Otherwise damage applied N times |
|
|
292
|
+
| Spawn players on every peer | MultiplayerSpawner — host spawns, clients receive | Otherwise N copies of each player |
|
|
293
|
+
| `@rpc("any_peer")` everything | Use `authority` for server→clients, `any_peer` only for client→server | Mismatched flow = exploits |
|
|
294
|
+
| Camera replicated | Camera is local-only, set `current = is_local` on spawn | Each peer needs their own camera |
|
|
295
|
+
|
|
296
|
+
## Anti-patterns
|
|
297
|
+
|
|
298
|
+
- **"Let's also have rollback netcode for our co-op shooter."** Rollback is a 6-month project on its own. Use it for fighting games and lock-step RTS, not co-op.
|
|
299
|
+
- **NAT punchthrough on day one.** Use Steam Networking or a relay; don't write punchthrough yourself for v1.
|
|
300
|
+
- **Dedicated servers without a deploy story.** If you can't redeploy in < 5 min, the dedicated path isn't ready.
|
|
301
|
+
- **Custom serialization.** Godot's variant serialization is fine. Don't write your own packing until you measure bandwidth.
|
|
302
|
+
- **Host migration in v1.** It's a feature, not a primitive. Ship without it; add later if churn data demands.
|
|
303
|
+
|
|
304
|
+
## Collaborative protocol
|
|
305
|
+
|
|
306
|
+
This skill rewires the project at the architectural level. Always ask before each block of changes. Group writes — "I'm about to add the autoload + 2 scenes + 3 scripts + InputMap entries. OK?". See `_shared/collaborative-protocol.md`.
|
|
307
|
+
|
|
308
|
+
## Want a working starter?
|
|
309
|
+
|
|
310
|
+
For a known-good multiplayer scaffold (host + join UI, ENet transport, MultiplayerSpawner, replicated player), point users at:
|
|
311
|
+
|
|
312
|
+
→ **template-id**: TBD (`template-co-op-online` planned, see `_shared/template-registry.md`)
|
|
313
|
+
|
|
314
|
+
For now: this skill produces the scaffold inline. When the template lands, link from here.
|
|
315
|
+
|
|
316
|
+
## See also
|
|
317
|
+
|
|
318
|
+
- `_shared/mcp-tools-reference.md` — full MCP tool list (project-setting, autoload registration)
|
|
319
|
+
- `_shared/godot-version.md` — `MultiplayerAPI` is medium-churn; `SceneReplicationConfig` settled in 4.0+
|
|
320
|
+
- `_shared/collaborative-protocol.md` — "May I write" pattern
|
|
321
|
+
- `_shared/gd-style.md` — typed GDScript conventions
|
|
322
|
+
- `multiplayer-and-networking/multiplayerapi-basics/SKILL.md` — deeper MultiplayerAPI patterns
|
|
323
|
+
- `multiplayer-and-networking/client-server-pattern/SKILL.md` — dedicated server architecture
|
|
324
|
+
- `multiplayer-and-networking/peer-replication/SKILL.md` — synchronizer config patterns
|
|
325
|
+
- `multiplayer-and-networking/enet-vs-websocket/SKILL.md` — transport choice
|
|
326
|
+
- `multiplayer-and-networking/lag-compensation/SKILL.md` — deeper netcode for competitive (when shipped)
|
|
327
|
+
- `multiplayer-and-networking/state-sync/SKILL.md` — selective replication patterns
|
|
328
|
+
- `multiplayer-and-networking/lobby-and-matchmaking/SKILL.md` — lobby UI + Steam matchmaking
|