summer-engine 1.3.1 → 1.4.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/CHANGELOG.md +67 -0
- package/README.md +13 -0
- package/dist/lib/skills-registry.d.ts +80 -0
- package/dist/lib/skills-registry.js +172 -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 +13 -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/peer-to-peer-multiplayer.md +99 -0
- 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/asset-pipeline/asset-strategy/SKILL.md +8 -0
- package/skills/audio/audio-direction/SKILL.md +258 -0
- package/skills/catalog.yaml +82 -0
- package/skills/character-controllers/fps-controller/SKILL.md +27 -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/peer-to-peer-multiplayer/SKILL.md +378 -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/3d-lighting/SKILL.md +20 -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/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 +280 -0
|
@@ -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
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tune-performance
|
|
3
|
+
description: Use when the user reports the game is slow, drops framerate, stutters, takes forever to start, or runs poorly on specific hardware. Profiles the running game via summer_get_diagnostics, identifies hotspots (rendering / physics / scripting), and proposes specific fixes with before/after metric expectations. Trigger on "slow", "lag", "fps drop", "stuttering", "performance", "optimize", "profile", "framerate".
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: [Cursor, Claude Code, Windsurf, Codex]
|
|
6
|
+
category: performance
|
|
7
|
+
user-invocable: true
|
|
8
|
+
allowed-tools: Read Grep Glob Edit summer_get_diagnostics summer_get_console summer_get_scene_tree summer_inspect_node summer_inspect_resource summer_play summer_stop summer_is_running summer_clear_console summer_set_prop summer_project_setting
|
|
9
|
+
paths: ["**/*.gd", "**/*.tscn", "**/*.tres", "project.godot"]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# /tune-performance — Profile, Diagnose, Fix
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
Performance tuning without measurement is gambling. This skill enforces a measure-first loop: read diagnostics, identify the dominant cost (rendering / physics / scripting / startup), drill into the specific pattern, propose one fix, verify the metric moved. No shotgun optimization.
|
|
17
|
+
|
|
18
|
+
**Core principle:** the engine's diagnostics tell you which subsystem is bleeding. Don't optimize a different subsystem.
|
|
19
|
+
|
|
20
|
+
## Steps
|
|
21
|
+
|
|
22
|
+
### 1. Get the user's symptom precisely
|
|
23
|
+
|
|
24
|
+
> What's slow? Pick the closest: framerate drops in scene X / startup is long / freezes for a moment / runs fine on my machine but bad on hardware Y.
|
|
25
|
+
|
|
26
|
+
Wait. The answer narrows the fix domain by 5x:
|
|
27
|
+
|
|
28
|
+
| Symptom | Likely subsystem |
|
|
29
|
+
|---|---|
|
|
30
|
+
| Framerate drops as more enemies spawn | Scripting (`_process` per-instance) or physics |
|
|
31
|
+
| Framerate is fine standing still, drops looking at level X | Rendering (overdraw, draw calls, lights) |
|
|
32
|
+
| Stutter every N seconds | GC pause, autoload loop, or async load |
|
|
33
|
+
| Long startup | Asset import, autoload `_ready` work, shader compilation |
|
|
34
|
+
| Runs fine on dev machine, bad on user machine | Resolution, GPU features (compute, GI), shadow quality |
|
|
35
|
+
|
|
36
|
+
### 2. Take a baseline measurement
|
|
37
|
+
|
|
38
|
+
Don't guess. Read the engine's actual numbers.
|
|
39
|
+
|
|
40
|
+
**Preferred (Summer MCP):**
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
summer_clear_console
|
|
44
|
+
summer_play
|
|
45
|
+
# user reproduces the slowdown for 5–10 seconds
|
|
46
|
+
summer_get_diagnostics
|
|
47
|
+
summer_get_console # check for warnings (e.g. "shader compilation hot-path")
|
|
48
|
+
summer_stop
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`summer_get_diagnostics` returns the aggregate metrics. Note (write down before fixing anything):
|
|
52
|
+
|
|
53
|
+
- Average FPS during the slow section
|
|
54
|
+
- Frame time (ms) — anything over 16.67 ms = below 60 fps
|
|
55
|
+
- Draw calls per frame
|
|
56
|
+
- Active physics bodies
|
|
57
|
+
- Active GPUParticles instances
|
|
58
|
+
- Script time vs. physics time vs. render time
|
|
59
|
+
|
|
60
|
+
**Fallback (no MCP):** ask the user to enable Godot's built-in monitor (`Debug → Monitor` or in-game with `Performance.get_monitor`) and paste numbers. At minimum: FPS, frame time, draw calls, physics active objects.
|
|
61
|
+
|
|
62
|
+
### 3. Identify the dominant cost
|
|
63
|
+
|
|
64
|
+
The biggest number wins your attention. Don't optimize the second-biggest — that's a 5% win when 30% is sitting next to it.
|
|
65
|
+
|
|
66
|
+
Decision tree:
|
|
67
|
+
|
|
68
|
+
- **Render time > 50% frame budget** → rendering hotspot (Step 4a)
|
|
69
|
+
- **Physics time > 30% frame budget** → physics hotspot (Step 4b)
|
|
70
|
+
- **Script time > 30% frame budget** → GDScript hotspot (Step 4c)
|
|
71
|
+
- **Frame time spikes (jitter) but average is fine** → GC / shader compile / async (Step 4d)
|
|
72
|
+
- **FPS is fine but startup is slow** → import / autoload (Step 4e)
|
|
73
|
+
|
|
74
|
+
### 4a. Rendering hotspot
|
|
75
|
+
|
|
76
|
+
Common Godot 4.5 rendering costs, ordered by frequency:
|
|
77
|
+
|
|
78
|
+
| Pattern | Symptom | Fix |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| Too many MeshInstance3D | Draw calls > 1000 | MultiMeshInstance3D for repeated meshes (foliage, debris). 5000 trees = 1 draw call. |
|
|
81
|
+
| Overdraw (transparent stacked) | Render time spikes when looking at particles/glass/UI | Reduce particle `amount`, disable `transparent` on opaque materials, check Compositor settings |
|
|
82
|
+
| Real-time lights > 4 visible | Frame time doubles in well-lit rooms | Bake static lighting (`LightmapGI`), make distant lights `shadow_enabled = false` |
|
|
83
|
+
| Shadow map size too high | Render time spikes near light sources | DirectionalLight3D `directional_shadow_max_distance` lower, OmniLight3D shadow_bias up, shadow size 2048 → 1024 |
|
|
84
|
+
| No occlusion culling | Far rooms drawn through walls | OccluderInstance3D + bake. Painful but high-impact for indoor scenes. |
|
|
85
|
+
| Missing LOD on complex meshes | Drop in framerate when far away | MeshInstance3D `visibility_range_begin` + manual LOD swaps |
|
|
86
|
+
|
|
87
|
+
To inspect a suspect node:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
summer_inspect_node "./World/Enemy"
|
|
91
|
+
summer_inspect_resource "./World/Enemy" # for mesh/material/shape details
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 4b. Physics hotspot
|
|
95
|
+
|
|
96
|
+
Top patterns:
|
|
97
|
+
|
|
98
|
+
| Pattern | Fix |
|
|
99
|
+
|---|---|
|
|
100
|
+
| 100+ active RigidBody3D | Sleep them when idle (`can_sleep = true`), or convert to Area3D where collision response isn't needed |
|
|
101
|
+
| Concave collision shapes on dynamic bodies | Convert to ConvexPolygonShape3D or compound primitives (sphere/capsule/box). Concave costs 5–20x more. |
|
|
102
|
+
| Physics tick at 120 Hz default | Lower `physics/common/physics_ticks_per_second` to 60 if visual smoothness allows |
|
|
103
|
+
| Per-frame raycast in `_physics_process` | Cache the result, raycast every N frames, or use ShapeCast3D |
|
|
104
|
+
| Hundreds of overlap queries | Use Area3D with `monitoring = true` only on the bodies that need to detect — turn it off on environment |
|
|
105
|
+
|
|
106
|
+
### 4c. GDScript hotspot
|
|
107
|
+
|
|
108
|
+
Per-frame work in `_process` is the #1 GDScript cost. Read the script, look for:
|
|
109
|
+
|
|
110
|
+
```gdscript
|
|
111
|
+
# BAD — runs every frame
|
|
112
|
+
func _process(delta: float) -> void:
|
|
113
|
+
var enemies = get_tree().get_nodes_in_group("enemies") # tree walk!
|
|
114
|
+
for enemy in enemies:
|
|
115
|
+
var dist = global_position.distance_to(enemy.global_position)
|
|
116
|
+
if dist < 10.0:
|
|
117
|
+
...
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Fixes:
|
|
121
|
+
|
|
122
|
+
- Cache the group lookup in `_ready()` if the group composition is stable.
|
|
123
|
+
- Move per-frame work to a timer at 10 Hz (`_on_timer_timeout`), not 60 Hz.
|
|
124
|
+
- Replace `distance_to` with `distance_squared_to` in comparisons (no sqrt).
|
|
125
|
+
- Push hot loops to a `@tool`-able C# script or GDExtension if it's truly per-frame.
|
|
126
|
+
|
|
127
|
+
### 4d. Frame time spikes (jitter)
|
|
128
|
+
|
|
129
|
+
If FPS is good but frame time spikes:
|
|
130
|
+
|
|
131
|
+
- **Shader compilation**: precompile shaders during loading screen via `RenderingServer.shader_compile_from_code` or by warming up the scene off-screen.
|
|
132
|
+
- **GC / object allocation**: `var x := []` inside `_process` allocates each frame. Pre-allocate.
|
|
133
|
+
- **Async resource load**: ensure `ResourceLoader.load_threaded_request` is finished before referencing.
|
|
134
|
+
|
|
135
|
+
Check `summer_get_console` for shader-compile warnings.
|
|
136
|
+
|
|
137
|
+
### 4e. Startup time
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
summer_clear_console
|
|
141
|
+
summer_play
|
|
142
|
+
# stop after first scene loads
|
|
143
|
+
summer_stop
|
|
144
|
+
summer_get_console
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Look for:
|
|
148
|
+
|
|
149
|
+
- Long autoload `_ready()` work (move to `call_deferred` or background thread).
|
|
150
|
+
- Asset import on first run (cache `.import` files in source control).
|
|
151
|
+
- `preload()` of huge scenes at script-parse time — switch to `load()` on demand.
|
|
152
|
+
|
|
153
|
+
### 5. Propose ONE fix with a metric expectation
|
|
154
|
+
|
|
155
|
+
Don't bundle five fixes. Pick the highest-leverage one.
|
|
156
|
+
|
|
157
|
+
> The dominant cost is **rendering** at 14.2 ms / frame (85% of budget). The biggest source is 240 individual MeshInstance3D for foliage. Replacing them with one MultiMeshInstance3D should drop render time to ~3 ms (estimate: 5–7 ms). May I refactor `./World/Foliage` to use MultiMeshInstance3D?
|
|
158
|
+
|
|
159
|
+
Format the proposal as: (a) what the dominant cost is, (b) the specific source, (c) the fix, (d) the expected metric movement, (e) the ask.
|
|
160
|
+
|
|
161
|
+
### 6. Apply, re-measure, report
|
|
162
|
+
|
|
163
|
+
After the user approves:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
# apply the fix (Edit / summer_add_node / summer_set_prop / etc)
|
|
167
|
+
summer_save_scene
|
|
168
|
+
summer_clear_console
|
|
169
|
+
summer_play
|
|
170
|
+
# user reproduces the same scenario
|
|
171
|
+
summer_get_diagnostics
|
|
172
|
+
summer_stop
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Compare: was the metric movement at least 50% of what was promised? If yes, ship it. If no, the hypothesis was wrong — go back to step 3 with the new numbers.
|
|
176
|
+
|
|
177
|
+
> Before: 14.2 ms render / 22 fps. After: 4.1 ms render / 58 fps. Shipped.
|
|
178
|
+
|
|
179
|
+
## Common mistakes
|
|
180
|
+
|
|
181
|
+
| Don't | Do | Why |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| Optimize without baseline metrics | Always `summer_get_diagnostics` before and after | "Felt faster" is not a measurement |
|
|
184
|
+
| Fix the second-biggest cost | Fix the biggest first, re-measure, re-prioritize | Amdahl's law — the small win evaporates next to the big one |
|
|
185
|
+
| Bundle 5 fixes into one PR | One fix at a time, verify each | Bundles hide regressions. Hard to attribute the win or the loss. |
|
|
186
|
+
| `_process` for everything | 10 Hz timer for AI / distance checks / range polls | 60 Hz AI is wasted CPU |
|
|
187
|
+
| `distance_to` in hot loop | `distance_squared_to` in comparisons | sqrt is expensive at scale |
|
|
188
|
+
| 200 RigidBody3D for debris | MultiMeshInstance3D + manual physics, or bake | Each rigid body = solver work every tick |
|
|
189
|
+
| Real-time GI everywhere | Bake static lighting; real-time only for movable lights | Real-time GI is a per-frame budget killer |
|
|
190
|
+
| Shadow map at 4096 on every light | 4096 for sun, 1024 or off for fillers | Shadow map writes are hidden render passes |
|
|
191
|
+
| Concave shape on dynamic body | Compound primitives (sphere + box + capsule) | Concave shape physics is 5–20x slower |
|
|
192
|
+
| `get_nodes_in_group` every frame | Cache once in `_ready()` | Tree walks aren't free |
|
|
193
|
+
|
|
194
|
+
## Anti-patterns from the field
|
|
195
|
+
|
|
196
|
+
- **"Just disable VSync to see real FPS."** Disabling VSync helps you measure, but performance work targets perceived smoothness (16.7 ms, locked). Re-enable before shipping.
|
|
197
|
+
- **Premature C++ rewrite.** A GDScript hotpath at 5% of frame time isn't the bottleneck. Profile first.
|
|
198
|
+
- **"It's slow on user machines, must be the GPU."** Often it's resolution scale + shadow quality + GI settings. Add a quality preset switcher first.
|
|
199
|
+
- **Threading gameplay code.** Godot's gameplay layer isn't thread-safe. Threading is for asset load, network, AI compute — not `_physics_process`.
|
|
200
|
+
|
|
201
|
+
## Collaborative protocol
|
|
202
|
+
|
|
203
|
+
This skill makes scene/resource/code changes. Always ask before each fix is applied. See `_shared/collaborative-protocol.md`.
|
|
204
|
+
|
|
205
|
+
## Want a working starter?
|
|
206
|
+
|
|
207
|
+
No template — this is a workflow. Performance tuning is project-specific by definition.
|
|
208
|
+
|
|
209
|
+
## See also
|
|
210
|
+
|
|
211
|
+
- `_shared/mcp-tools-reference.md` — full MCP tool list
|
|
212
|
+
- `_shared/godot-version.md` — Godot 4.5 renderer notes (Compositor, RenderSceneBuffers churn)
|
|
213
|
+
- `_shared/collaborative-protocol.md` — "May I write" pattern
|
|
214
|
+
- `_shared/gd-style.md` — GDScript conventions (avoid bare types, use `:=`)
|
|
215
|
+
- `performance/profiling-godot/SKILL.md` — deeper Godot profiler usage
|
|
216
|
+
- `performance/draw-call-batching/SKILL.md` — MultiMeshInstance3D patterns
|
|
217
|
+
- `performance/lod-and-culling/SKILL.md` — LOD setup
|
|
218
|
+
- `debugging/debug/SKILL.md` — bug triage (related but different)
|
|
219
|
+
- `rendering-and-lighting/baked-vs-realtime-lighting/SKILL.md` — bake decisions
|
|
@@ -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`.
|