summer-engine 1.3.2 → 1.5.0
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/.claude-plugin/plugin.json +59 -0
- package/.codex-plugin/plugin.json +38 -0
- package/.cursor-plugin/plugin.json +42 -0
- package/.mcp.json +8 -0
- package/.opencode/plugins/summer.js +49 -0
- package/README.md +13 -0
- package/dist/lib/skills-registry.d.ts +96 -0
- package/dist/lib/skills-registry.js +205 -0
- package/gemini-extension.json +21 -0
- package/hooks/hooks-cursor.json +21 -0
- package/hooks/hooks.json +31 -0
- package/hooks/pre-commit-doctor.sh +123 -0
- package/hooks/run-hook.cmd +23 -0
- package/hooks/session-start.sh +120 -0
- package/marketplace.json +24 -0
- package/package.json +9 -1
- package/skills/_tests/specs/design-npc.md +43 -12
- package/skills/_tests/specs/fps-controller.md +33 -20
- package/skills/_tests/specs/host-authoritative-state.md +104 -0
- package/skills/_tests/specs/peer-to-peer-multiplayer.md +99 -0
- package/skills/_tests/specs/vfx.md +56 -38
- package/skills/ai-and-npcs/design-npc/SKILL.md +264 -122
- package/skills/asset-pipeline/asset-strategy/SKILL.md +8 -0
- package/skills/catalog.yaml +84 -0
- package/skills/character-controllers/fps-controller/SKILL.md +277 -69
- package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +332 -0
- package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +378 -0
- package/skills/rendering-and-lighting/3d-lighting/SKILL.md +20 -0
- package/skills/scene-and-project/make-game/SKILL.md +10 -0
- package/skills/scene-and-project/scene-composition/SKILL.md +8 -0
- package/skills/ui-and-ux/ui-basics/SKILL.md +28 -0
- package/skills/visual-effects/vfx/SKILL.md +233 -134
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: peer-to-peer-multiplayer
|
|
3
|
+
description: Use when starting a multiplayer Godot game from scratch — peer-to-peer with host authority. Build the network architecture top-down so authoritative state, routing rules, and real-time rendering aren't bolted on later. Use BEFORE writing game logic, not after. Trigger on "multiplayer", "peer-to-peer", "p2p", "co-op", "host", "multiplayer architecture", "add multiplayer".
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: [Cursor, Claude Code, Windsurf, Codex]
|
|
6
|
+
category: multiplayer-and-networking
|
|
7
|
+
user-invocable: true
|
|
8
|
+
allowed-tools: Read Grep Glob summer_get_scene_tree summer_inspect_node summer_create_scene summer_add_node summer_set_prop summer_connect_signal summer_save_scene summer_get_script_errors summer_project_setting
|
|
9
|
+
paths: ["**/*.gd", "**/*.tscn", "**/project.godot"]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# /peer-to-peer-multiplayer — Build a P2P Game From the Ground Up
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
**Multiplayer is architecture, not a feature.** Bolting it onto a single-player game is 5× harder than starting multiplayer-first. This skill walks the four layers in order. Skip any of them and you'll spend the rest of development paying for it in cheat-vulnerable state, desync bugs, and "why is the other player teleporting" reports.
|
|
17
|
+
|
|
18
|
+
The four layers, top-down:
|
|
19
|
+
|
|
20
|
+
1. **NetworkManager** — the autoload that owns peer IDs, connection lifecycle, message routing.
|
|
21
|
+
2. **Authoritative state** — the decision matrix for *which fields the host owns vs. which the client renders*.
|
|
22
|
+
3. **Three routing patterns** — host→client, client→host, client→client. Each has different rules.
|
|
23
|
+
4. **Real-time rendering** — prediction + interpolation so the local player feels responsive while remote state catches up.
|
|
24
|
+
|
|
25
|
+
Build them in that order. Don't write a single piece of game logic until layers 1 + 2 exist.
|
|
26
|
+
|
|
27
|
+
## Step 0 — Should it be P2P at all?
|
|
28
|
+
|
|
29
|
+
Ask the user before anything else:
|
|
30
|
+
|
|
31
|
+
> Are players connecting through Steam friends / Epic friends / a private invite link, or do they need matchmaking at scale (1000s of concurrent matches)?
|
|
32
|
+
|
|
33
|
+
| Answer | Architecture |
|
|
34
|
+
|---|---|
|
|
35
|
+
| Friend-invite / small lobby (≤8 players) | **P2P with host authority.** This skill. |
|
|
36
|
+
| Matchmaking + anti-cheat + scale | **Dedicated client-server.** Different skill (`/summer:client-server-multiplayer`). |
|
|
37
|
+
| MMO / persistent world | **Custom server.** Out of scope for this skill. |
|
|
38
|
+
|
|
39
|
+
If P2P is right, continue. Otherwise, stop and route to the right skill.
|
|
40
|
+
|
|
41
|
+
## Layer 1 — NetworkManager
|
|
42
|
+
|
|
43
|
+
A single autoload that owns the network. Nothing else in the game touches `MultiplayerAPI` directly.
|
|
44
|
+
|
|
45
|
+
May I create `autoloads/network_manager.gd` and register it as a Godot autoload?
|
|
46
|
+
|
|
47
|
+
```gdscript
|
|
48
|
+
class_name NetworkManager
|
|
49
|
+
extends Node
|
|
50
|
+
|
|
51
|
+
# ── Signals (all game systems listen here, not to MultiplayerAPI directly) ──
|
|
52
|
+
signal connection_succeeded
|
|
53
|
+
signal connection_failed(reason: String)
|
|
54
|
+
signal peer_joined(peer_id: int)
|
|
55
|
+
signal peer_left(peer_id: int)
|
|
56
|
+
signal session_ended
|
|
57
|
+
|
|
58
|
+
const DEFAULT_PORT := 7777
|
|
59
|
+
const MAX_PEERS := 8
|
|
60
|
+
|
|
61
|
+
var is_host: bool = false
|
|
62
|
+
var local_peer_id: int = 0
|
|
63
|
+
var peers: Dictionary = {} # peer_id -> { name, ready, ... }
|
|
64
|
+
|
|
65
|
+
func host(port: int = DEFAULT_PORT) -> Error:
|
|
66
|
+
var peer := ENetMultiplayerPeer.new()
|
|
67
|
+
var err := peer.create_server(port, MAX_PEERS)
|
|
68
|
+
if err != OK:
|
|
69
|
+
connection_failed.emit("create_server failed: %s" % error_string(err))
|
|
70
|
+
return err
|
|
71
|
+
multiplayer.multiplayer_peer = peer
|
|
72
|
+
is_host = true
|
|
73
|
+
local_peer_id = 1
|
|
74
|
+
_wire_signals()
|
|
75
|
+
connection_succeeded.emit()
|
|
76
|
+
return OK
|
|
77
|
+
|
|
78
|
+
func join(address: String, port: int = DEFAULT_PORT) -> Error:
|
|
79
|
+
var peer := ENetMultiplayerPeer.new()
|
|
80
|
+
var err := peer.create_client(address, port)
|
|
81
|
+
if err != OK:
|
|
82
|
+
connection_failed.emit("create_client failed: %s" % error_string(err))
|
|
83
|
+
return err
|
|
84
|
+
multiplayer.multiplayer_peer = peer
|
|
85
|
+
is_host = false
|
|
86
|
+
_wire_signals()
|
|
87
|
+
return OK
|
|
88
|
+
|
|
89
|
+
func leave() -> void:
|
|
90
|
+
if multiplayer.multiplayer_peer:
|
|
91
|
+
multiplayer.multiplayer_peer.close()
|
|
92
|
+
multiplayer.multiplayer_peer = null
|
|
93
|
+
is_host = false
|
|
94
|
+
local_peer_id = 0
|
|
95
|
+
peers.clear()
|
|
96
|
+
session_ended.emit()
|
|
97
|
+
|
|
98
|
+
func _wire_signals() -> void:
|
|
99
|
+
multiplayer.peer_connected.connect(_on_peer_connected)
|
|
100
|
+
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
|
101
|
+
multiplayer.connected_to_server.connect(_on_connected_to_server)
|
|
102
|
+
multiplayer.connection_failed.connect(_on_connection_failed)
|
|
103
|
+
multiplayer.server_disconnected.connect(_on_server_disconnected)
|
|
104
|
+
|
|
105
|
+
func _on_peer_connected(id: int) -> void:
|
|
106
|
+
peers[id] = { "name": "Player %d" % id, "ready": false }
|
|
107
|
+
peer_joined.emit(id)
|
|
108
|
+
|
|
109
|
+
func _on_peer_disconnected(id: int) -> void:
|
|
110
|
+
peers.erase(id)
|
|
111
|
+
peer_left.emit(id)
|
|
112
|
+
|
|
113
|
+
func _on_connected_to_server() -> void:
|
|
114
|
+
local_peer_id = multiplayer.get_unique_id()
|
|
115
|
+
connection_succeeded.emit()
|
|
116
|
+
|
|
117
|
+
func _on_connection_failed() -> void:
|
|
118
|
+
connection_failed.emit("connection refused or timed out")
|
|
119
|
+
|
|
120
|
+
func _on_server_disconnected() -> void:
|
|
121
|
+
leave()
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Register as autoload: `summer_project_setting(key="autoload/NetworkManager", value="*res://autoloads/network_manager.gd")`. The leading `*` makes it a singleton.
|
|
125
|
+
|
|
126
|
+
**Why this shape:** every system in the game listens to `NetworkManager.peer_joined` / `peer_left`, never to `multiplayer.peer_connected` directly. When you swap transports (ENet → WebRTC → Steam P2P) later, you change one file.
|
|
127
|
+
|
|
128
|
+
## Layer 2 — Authoritative State
|
|
129
|
+
|
|
130
|
+
For every piece of state in your game, answer one question: **if a malicious client lies about this, does it break the game?**
|
|
131
|
+
|
|
132
|
+
- **Yes** → host owns it. Clients send intent; host decides. Replicated to clients read-only.
|
|
133
|
+
- **No** (cosmetic / pure local feel) → client owns it. Replicated for visual sync only.
|
|
134
|
+
|
|
135
|
+
The canonical decision matrix:
|
|
136
|
+
|
|
137
|
+
| State | Owner | Reasoning |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| health, mana, stamina | **host** | damage logic must not be cheatable |
|
|
140
|
+
| score, kills, objectives | **host** | competitive integrity |
|
|
141
|
+
| inventory contents | **host** | items would dupe |
|
|
142
|
+
| ability cooldowns | **host** | spam-cast attack vector |
|
|
143
|
+
| current weapon equipped | **host** | tied to inventory |
|
|
144
|
+
| position (player) | **client-predicted, host-reconciled** | input latency feels awful otherwise |
|
|
145
|
+
| velocity | **client-predicted, host-reconciled** | same |
|
|
146
|
+
| look direction (camera yaw/pitch) | **client** | cosmetic, host doesn't need it |
|
|
147
|
+
| animation state | **client** | visual; can lead the truth |
|
|
148
|
+
| footstep sounds | **client** | host doesn't care |
|
|
149
|
+
| muzzle flash, particles | **client** | host doesn't care |
|
|
150
|
+
| chat messages | **host-relayed** | for moderation hooks |
|
|
151
|
+
|
|
152
|
+
Build a State Layer that codifies this. May I create `autoloads/game_state.gd`?
|
|
153
|
+
|
|
154
|
+
```gdscript
|
|
155
|
+
class_name GameState
|
|
156
|
+
extends Node
|
|
157
|
+
|
|
158
|
+
# Host-authoritative dictionaries. Keyed by peer_id where applicable.
|
|
159
|
+
var health: Dictionary = {} # peer_id -> int
|
|
160
|
+
var score: Dictionary = {} # peer_id -> int
|
|
161
|
+
var inventory: Dictionary = {} # peer_id -> Array[ItemData]
|
|
162
|
+
|
|
163
|
+
# Local-only mirrors (read for rendering; never written by clients).
|
|
164
|
+
var local_health_view: int = 100
|
|
165
|
+
|
|
166
|
+
func _ready() -> void:
|
|
167
|
+
NetworkManager.peer_joined.connect(_on_peer_joined)
|
|
168
|
+
NetworkManager.peer_left.connect(_on_peer_left)
|
|
169
|
+
|
|
170
|
+
# ── Host-only mutators (called via RPC from client requests) ──
|
|
171
|
+
func _host_apply_damage(target: int, amount: int) -> void:
|
|
172
|
+
if not NetworkManager.is_host: return
|
|
173
|
+
var new_hp = max(0, health.get(target, 100) - amount)
|
|
174
|
+
health[target] = new_hp
|
|
175
|
+
_broadcast_health(target, new_hp)
|
|
176
|
+
if new_hp == 0:
|
|
177
|
+
_broadcast_death(target)
|
|
178
|
+
|
|
179
|
+
@rpc("authority", "call_remote", "reliable")
|
|
180
|
+
func _broadcast_health(peer: int, hp: int) -> void:
|
|
181
|
+
if peer == NetworkManager.local_peer_id:
|
|
182
|
+
local_health_view = hp
|
|
183
|
+
|
|
184
|
+
@rpc("authority", "call_remote", "reliable")
|
|
185
|
+
func _broadcast_death(peer: int) -> void:
|
|
186
|
+
pass # play death anim, etc.
|
|
187
|
+
|
|
188
|
+
# ── Client-side request (intent only — host decides) ──
|
|
189
|
+
func client_request_damage(target: int, amount: int) -> void:
|
|
190
|
+
rpc_id(1, "_host_handle_damage_request", target, amount)
|
|
191
|
+
|
|
192
|
+
@rpc("any_peer", "call_remote", "reliable")
|
|
193
|
+
func _host_handle_damage_request(target: int, amount: int) -> void:
|
|
194
|
+
if not NetworkManager.is_host: return
|
|
195
|
+
var sender := multiplayer.get_remote_sender_id()
|
|
196
|
+
# validate: did sender's hitbox actually overlap target this frame?
|
|
197
|
+
# if validation passes, apply. otherwise, drop silently.
|
|
198
|
+
if _validate_hit(sender, target):
|
|
199
|
+
_host_apply_damage(target, amount)
|
|
200
|
+
|
|
201
|
+
func _validate_hit(_attacker: int, _target: int) -> bool:
|
|
202
|
+
return true # implement: distance check, line-of-sight, cooldown, etc.
|
|
203
|
+
|
|
204
|
+
func _on_peer_joined(id: int) -> void:
|
|
205
|
+
if NetworkManager.is_host:
|
|
206
|
+
health[id] = 100
|
|
207
|
+
score[id] = 0
|
|
208
|
+
inventory[id] = []
|
|
209
|
+
_broadcast_health(id, 100)
|
|
210
|
+
|
|
211
|
+
func _on_peer_left(id: int) -> void:
|
|
212
|
+
if NetworkManager.is_host:
|
|
213
|
+
health.erase(id)
|
|
214
|
+
score.erase(id)
|
|
215
|
+
inventory.erase(id)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Key invariants enforced by this shape:**
|
|
219
|
+
- Every host-authoritative field has a single mutator function on the host.
|
|
220
|
+
- Clients call `client_request_*` to request a change; the host validates and broadcasts.
|
|
221
|
+
- Clients never directly modify `health` / `score` / `inventory` even on their own peer.
|
|
222
|
+
- `@rpc("authority", ...)` ensures only the host can call broadcast functions.
|
|
223
|
+
- `@rpc("any_peer", ...)` allows any client to call request functions, but the function checks `NetworkManager.is_host` and validates the request.
|
|
224
|
+
|
|
225
|
+
## Layer 3 — The Three Routing Patterns
|
|
226
|
+
|
|
227
|
+
Every networked function fits one of three patterns. Pick the right one or you'll have bugs.
|
|
228
|
+
|
|
229
|
+
### Pattern A: Host → Client (state broadcast)
|
|
230
|
+
|
|
231
|
+
State the host owns, replicated read-only to clients.
|
|
232
|
+
|
|
233
|
+
```gdscript
|
|
234
|
+
@rpc("authority", "call_remote", "reliable")
|
|
235
|
+
func _broadcast_score(peer: int, value: int) -> void:
|
|
236
|
+
score[peer] = value
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Rules:**
|
|
240
|
+
- `authority` mode = only host can call this (anyone else's call is ignored).
|
|
241
|
+
- `call_remote` = doesn't run on the caller (host doesn't need to call its own update).
|
|
242
|
+
- `reliable` = must arrive (state corruption otherwise).
|
|
243
|
+
|
|
244
|
+
### Pattern B: Client → Host (intent / request)
|
|
245
|
+
|
|
246
|
+
Client wants something to happen. Host decides yes or no.
|
|
247
|
+
|
|
248
|
+
```gdscript
|
|
249
|
+
@rpc("any_peer", "call_remote", "reliable")
|
|
250
|
+
func _host_handle_use_item_request(item_id: String) -> void:
|
|
251
|
+
if not NetworkManager.is_host: return
|
|
252
|
+
var sender := multiplayer.get_remote_sender_id()
|
|
253
|
+
if _can_use(sender, item_id):
|
|
254
|
+
_host_apply_use_item(sender, item_id)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Rules:**
|
|
258
|
+
- `any_peer` = any client can call.
|
|
259
|
+
- First line is always `if not NetworkManager.is_host: return`. Defensive.
|
|
260
|
+
- Always validate (`_can_use`). Trusting client input is the #1 multiplayer cheat vector.
|
|
261
|
+
- Use `multiplayer.get_remote_sender_id()` — clients can't lie about who sent the request.
|
|
262
|
+
|
|
263
|
+
### Pattern C: Client → Client (cosmetic relay)
|
|
264
|
+
|
|
265
|
+
Visual-only events. Host relays but doesn't validate.
|
|
266
|
+
|
|
267
|
+
```gdscript
|
|
268
|
+
@rpc("any_peer", "call_remote", "unreliable")
|
|
269
|
+
func _peer_play_emote(emote_id: String) -> void:
|
|
270
|
+
# everyone plays the emote on the sender's player
|
|
271
|
+
var sender := multiplayer.get_remote_sender_id()
|
|
272
|
+
_play_emote_visual(sender, emote_id)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Rules:**
|
|
276
|
+
- `unreliable` is fine for cosmetic stuff (animations, particles, footstep SFX).
|
|
277
|
+
- No validation needed — worst case is a player's emote desyncs by a frame.
|
|
278
|
+
- Save bandwidth: keep payloads tiny.
|
|
279
|
+
|
|
280
|
+
**Anti-pattern:** mixing patterns in one function. A function that broadcasts state AND handles client intent is going to have bugs. Split.
|
|
281
|
+
|
|
282
|
+
## Layer 4 — Real-time Rendering
|
|
283
|
+
|
|
284
|
+
The local player needs to feel instant. Remote players need to look smooth despite network jitter. Two techniques:
|
|
285
|
+
|
|
286
|
+
### Client-side prediction (for the local player's input)
|
|
287
|
+
|
|
288
|
+
```gdscript
|
|
289
|
+
# Local player runs movement immediately on input. Host runs the SAME function
|
|
290
|
+
# authoritatively. If they diverge, the client snaps to host state.
|
|
291
|
+
|
|
292
|
+
func _physics_process(delta: float) -> void:
|
|
293
|
+
if not _is_local_authority(): return
|
|
294
|
+
var input := _read_input()
|
|
295
|
+
velocity = _compute_velocity(input, velocity, delta)
|
|
296
|
+
move_and_slide()
|
|
297
|
+
if NetworkManager.local_peer_id != 1: # not the host
|
|
298
|
+
_send_input_to_host(input)
|
|
299
|
+
|
|
300
|
+
@rpc("any_peer", "call_remote", "unreliable_ordered")
|
|
301
|
+
func _send_input_to_host(_input: InputState) -> void:
|
|
302
|
+
if not NetworkManager.is_host: return
|
|
303
|
+
# host runs the SAME _compute_velocity to validate + reconcile
|
|
304
|
+
pass
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Interpolation (for remote players' positions)
|
|
308
|
+
|
|
309
|
+
Don't snap remote players to incoming positions — interpolate.
|
|
310
|
+
|
|
311
|
+
```gdscript
|
|
312
|
+
var _network_position_buffer: Array[Vector3] = []
|
|
313
|
+
var _network_position_timestamps: Array[float] = []
|
|
314
|
+
|
|
315
|
+
func _physics_process(_delta: float) -> void:
|
|
316
|
+
if _is_local_authority(): return # local handled by prediction
|
|
317
|
+
_interpolate_to_buffer()
|
|
318
|
+
|
|
319
|
+
@rpc("authority", "call_remote", "unreliable_ordered")
|
|
320
|
+
func _receive_remote_position(pos: Vector3, t: float) -> void:
|
|
321
|
+
_network_position_buffer.append(pos)
|
|
322
|
+
_network_position_timestamps.append(t)
|
|
323
|
+
while _network_position_buffer.size() > 4:
|
|
324
|
+
_network_position_buffer.pop_front()
|
|
325
|
+
_network_position_timestamps.pop_front()
|
|
326
|
+
|
|
327
|
+
func _interpolate_to_buffer() -> void:
|
|
328
|
+
if _network_position_buffer.size() < 2: return
|
|
329
|
+
var render_time := Time.get_ticks_msec() / 1000.0 - 0.1 # 100ms interp delay
|
|
330
|
+
# find the two buffer entries that bracket render_time, lerp between them
|
|
331
|
+
# (full impl: ~30 lines)
|
|
332
|
+
pass
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Common mistakes (anti-patterns)
|
|
336
|
+
|
|
337
|
+
| Mistake | What goes wrong | Fix |
|
|
338
|
+
|---|---|---|
|
|
339
|
+
| Putting health on the client and trusting it | Trivial cheats: edit memory, infinite HP. | Move all damage logic to host. Client requests, host decides. |
|
|
340
|
+
| Calling `multiplayer.peer_connected.connect()` directly in 5 different scripts | Connection logic scattered. Refactor pain. | Always go through `NetworkManager` signals. |
|
|
341
|
+
| `@rpc("any_peer")` with no `is_host` check inside | Client can broadcast as if host. State corruption. | First line of every `any_peer` handler is `if not is_host: return`. |
|
|
342
|
+
| Reliable RPC for cosmetic stuff (footsteps, particles) | Bandwidth blowup. | Use `unreliable` for cosmetic. Reserve `reliable` for state. |
|
|
343
|
+
| Sending position 60×/sec via reliable RPC | Network spam, dropped packets cause stutters. | `unreliable_ordered` at 20–30 Hz + interpolation buffer on receivers. |
|
|
344
|
+
| Adding multiplayer after the game's built | Single-player code assumed direct state writes everywhere. Massive refactor. | Use this skill BEFORE writing game logic. |
|
|
345
|
+
| Forgetting host-leaves handling | Host disconnects → game freezes. | `multiplayer.server_disconnected` → graceful end-session in `NetworkManager.leave()`. |
|
|
346
|
+
|
|
347
|
+
## When to NOT use this skill
|
|
348
|
+
|
|
349
|
+
- **Single-player game with leaderboards.** No multiplayer needed.
|
|
350
|
+
- **Async multiplayer** (turn-based via REST API). This skill assumes real-time.
|
|
351
|
+
- **Dedicated server with anti-cheat.** Use `/summer:client-server-multiplayer` (when shipped).
|
|
352
|
+
- **Already-shipped game getting multiplayer retrofit.** Sorry — you're in for a refactor. This skill helps for the redesign but not the migration.
|
|
353
|
+
|
|
354
|
+
## Collaborative protocol
|
|
355
|
+
|
|
356
|
+
This skill writes multiple new files (`autoloads/network_manager.gd`, `autoloads/game_state.gd`, plus updates to `project.godot` for the autoload registration). Always ask before each phase:
|
|
357
|
+
|
|
358
|
+
> May I create the NetworkManager autoload?
|
|
359
|
+
> May I create the GameState autoload and register both in project.godot?
|
|
360
|
+
> May I add the prediction/interpolation logic to your existing player scene?
|
|
361
|
+
|
|
362
|
+
Don't apply all four layers in one shot — checkpoint after each layer so the user can verify before adding the next.
|
|
363
|
+
|
|
364
|
+
See [`_shared/collaborative-protocol.md`](../../_shared/collaborative-protocol.md).
|
|
365
|
+
|
|
366
|
+
## Want a working starter?
|
|
367
|
+
|
|
368
|
+
No template ships with this exact architecture yet — it's deliberately built fresh for each project because the State Layer's authority decisions are game-specific (a stealth game's "visibility" and a shooter's "ammo count" both want host authority but for different reasons). Run this skill from scratch.
|
|
369
|
+
|
|
370
|
+
When the `template-co-op-3d` template ships in the registry, this skill will reference it as a starter — for now it's hand-rolled.
|
|
371
|
+
|
|
372
|
+
## See also
|
|
373
|
+
|
|
374
|
+
- [`_shared/godot-version.md`](../../_shared/godot-version.md) — Godot 4.5 multiplayer API stability notes
|
|
375
|
+
- [`_shared/mcp-tools-reference.md`](../../_shared/mcp-tools-reference.md) — `summer_project_setting` for autoload registration
|
|
376
|
+
- [`_shared/gd-style.md`](../../_shared/gd-style.md) — typed-GDScript conventions
|
|
377
|
+
- [`host-authoritative-state`](../host-authoritative-state/SKILL.md) — deeper dive on JUST the state ownership decision matrix
|
|
378
|
+
- [`setup-multiplayer`](../setup-multiplayer/SKILL.md) — lighter intro that just gets a session running (use that one if the user just wants 2 players to see each other; use this one if they want to ship a real game)
|
|
@@ -102,3 +102,23 @@ Always use `Color(r, g, b, a)` with values 0.0–1.0:
|
|
|
102
102
|
- Cool white: `Color(0.9, 0.95, 1, 1)`
|
|
103
103
|
- Orange (torch): `Color(1, 0.6, 0.2, 1)`
|
|
104
104
|
- Green (alien): `Color(0.5, 1, 0.5, 1)`
|
|
105
|
+
|
|
106
|
+
## Fallback (no MCP — edit `.tscn` directly)
|
|
107
|
+
|
|
108
|
+
If Summer MCP isn't connected, append the lighting block to your scene file:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
[node name="Sun" type="DirectionalLight3D" parent="World"]
|
|
112
|
+
transform = Transform3D(0.866, 0, -0.5, 0.354, 0.707, 0.612, 0.354, -0.707, 0.612, 0, 0, 0)
|
|
113
|
+
light_color = Color(1, 0.95, 0.9, 1)
|
|
114
|
+
light_energy = 1.0
|
|
115
|
+
shadow_enabled = true
|
|
116
|
+
|
|
117
|
+
[node name="WorldEnvironment" type="WorldEnvironment" parent="World"]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Then create `Environment` and `Sky` resources via the inspector or load from `.tres`.
|
|
121
|
+
|
|
122
|
+
## Collaborative protocol
|
|
123
|
+
|
|
124
|
+
This skill mutates the scene (adds lights + WorldEnvironment + tunes Environment sub-properties). Always ask before applying: "May I add a DirectionalLight3D Sun with shadows + a WorldEnvironment with a procedural sky?". See `../../_shared/collaborative-protocol.md`.
|
|
@@ -440,3 +440,13 @@ After first playable, ask the user what to improve. Common next steps:
|
|
|
440
440
|
| Debug | `summer_get_console` | Read output |
|
|
441
441
|
| Debug | `summer_get_script_errors` | Check .gd compile |
|
|
442
442
|
| Debug | `summer_clear_console` | Clear output |
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Collaborative protocol
|
|
447
|
+
|
|
448
|
+
This skill orchestrates a full game build — many writes across scenes, scripts, and project settings. Always ask before each phase: "May I bootstrap the project (scenes, project.godot, folders)?", "May I generate the assets (image + 3D) for X, Y, Z?", "May I scaffold the player scene + attach `player.gd`?". Group related writes into one ask per phase, never bulk-write across phases. See `../../_shared/collaborative-protocol.md`.
|
|
449
|
+
|
|
450
|
+
## Fallback
|
|
451
|
+
|
|
452
|
+
No fallback for this — Summer MCP required. The whole point of `make-game` is the orchestration layer over `summer_*` tools. If MCP isn't connected, fall back to running the individual sub-skills (`fps-controller`, `3d-lighting`, etc.) which each document their own MCP-or-`.tscn` fallback paths.
|
|
@@ -107,3 +107,11 @@ Use `./` for paths relative to the scene root:
|
|
|
107
107
|
2. **Unnamed scene:** Saving without a path fails if the scene was never saved. Use `path` for new scenes.
|
|
108
108
|
3. **Duplicate names:** Godot auto-renames (Node, Node2, etc.). Use descriptive unique names.
|
|
109
109
|
4. **Mixing 2D and 3D:** Don't put Node2D under Node3D or vice versa in the same hierarchy.
|
|
110
|
+
|
|
111
|
+
## Fallback
|
|
112
|
+
|
|
113
|
+
No fallback for this — Summer MCP required. Handwriting `.tscn` files for hierarchy mutations is error-prone (UID collisions, wrong format version, broken sub_resource refs). If MCP isn't connected, open the scene in the Godot editor and use the SceneTree dock.
|
|
114
|
+
|
|
115
|
+
## Collaborative protocol
|
|
116
|
+
|
|
117
|
+
This skill creates and mutates scene files. Always ask before applying: "May I create `res://scenes/player.tscn` and instantiate it under `./World`?". See `../../_shared/collaborative-protocol.md`.
|
|
@@ -120,3 +120,31 @@ For UI that scales with window size:
|
|
|
120
120
|
3. MarginContainer with percentage-based margins (may need theme or script)
|
|
121
121
|
|
|
122
122
|
For simple cases, CenterContainer + fixed-size children works. For complex layouts, a Theme resource or script may be needed.
|
|
123
|
+
|
|
124
|
+
## Fallback (no MCP — edit `.tscn` directly)
|
|
125
|
+
|
|
126
|
+
If Summer MCP isn't connected, write the menu hierarchy by hand:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
[node name="MainMenu" type="Control" parent="."]
|
|
130
|
+
anchors_preset = 15
|
|
131
|
+
|
|
132
|
+
[node name="Margin" type="MarginContainer" parent="MainMenu"]
|
|
133
|
+
anchors_preset = 15
|
|
134
|
+
theme_override_constants/margin_left = 50
|
|
135
|
+
theme_override_constants/margin_right = 50
|
|
136
|
+
theme_override_constants/margin_top = 50
|
|
137
|
+
theme_override_constants/margin_bottom = 50
|
|
138
|
+
|
|
139
|
+
[node name="VBox" type="VBoxContainer" parent="MainMenu/Margin"]
|
|
140
|
+
|
|
141
|
+
[node name="Title" type="Label" parent="MainMenu/Margin/VBox"]
|
|
142
|
+
text = "My Game"
|
|
143
|
+
|
|
144
|
+
[node name="StartButton" type="Button" parent="MainMenu/Margin/VBox"]
|
|
145
|
+
text = "Start Game"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Collaborative protocol
|
|
149
|
+
|
|
150
|
+
This skill writes UI scene nodes and wires button signals. Always ask before applying: "May I add the MainMenu (Control + Margin + VBox + Title + Start/Quit buttons) and connect Start.pressed to `_on_start_pressed`?". See `../../_shared/collaborative-protocol.md`.
|