bmad-method 4.43.0 → 4.44.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/CONTRIBUTING.md +2 -9
- package/README.md +0 -80
- package/bmad-core/tasks/validate-next-story.md +1 -1
- package/dist/agents/dev.txt +1 -1
- package/dist/agents/po.txt +1 -1
- package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-developer.txt +1 -1
- package/dist/expansion-packs/bmad-2d-unity-game-dev/teams/unity-2d-game-team.txt +1 -1
- package/dist/expansion-packs/bmad-godot-game-dev/agents/bmad-orchestrator.txt +1513 -0
- package/dist/expansion-packs/bmad-godot-game-dev/agents/game-analyst.txt +3190 -0
- package/dist/expansion-packs/bmad-godot-game-dev/agents/game-architect.txt +4499 -0
- package/dist/expansion-packs/bmad-godot-game-dev/agents/game-designer.txt +3925 -0
- package/dist/expansion-packs/bmad-godot-game-dev/agents/game-developer.txt +666 -0
- package/dist/expansion-packs/bmad-godot-game-dev/agents/game-pm.txt +2381 -0
- package/dist/expansion-packs/bmad-godot-game-dev/agents/game-po.txt +1612 -0
- package/dist/expansion-packs/bmad-godot-game-dev/agents/game-qa.txt +1745 -0
- package/dist/expansion-packs/bmad-godot-game-dev/agents/game-sm.txt +1208 -0
- package/dist/expansion-packs/bmad-godot-game-dev/agents/game-ux-expert.txt +958 -0
- package/dist/expansion-packs/bmad-godot-game-dev/teams/godot-game-team.txt +27721 -0
- package/dist/teams/team-all.txt +1 -1
- package/dist/teams/team-fullstack.txt +1 -1
- package/dist/teams/team-ide-minimal.txt +1 -1
- package/dist/teams/team-no-ui.txt +1 -1
- package/docs/GUIDING-PRINCIPLES.md +3 -3
- package/docs/flattener.md +91 -0
- package/docs/versions.md +1 -1
- package/docs/working-in-the-brownfield.md +15 -6
- package/expansion-packs/bmad-godot-game-dev/README.md +244 -0
- package/expansion-packs/bmad-godot-game-dev/agent-teams/godot-game-team.yaml +18 -0
- package/expansion-packs/bmad-godot-game-dev/agents/bmad-orchestrator.md +147 -0
- package/expansion-packs/bmad-godot-game-dev/agents/game-analyst.md +84 -0
- package/expansion-packs/bmad-godot-game-dev/agents/game-architect.md +146 -0
- package/expansion-packs/bmad-godot-game-dev/agents/game-designer.md +78 -0
- package/expansion-packs/bmad-godot-game-dev/agents/game-developer.md +124 -0
- package/expansion-packs/bmad-godot-game-dev/agents/game-pm.md +82 -0
- package/expansion-packs/bmad-godot-game-dev/agents/game-po.md +115 -0
- package/expansion-packs/bmad-godot-game-dev/agents/game-qa.md +160 -0
- package/expansion-packs/bmad-godot-game-dev/agents/game-sm.md +66 -0
- package/expansion-packs/bmad-godot-game-dev/agents/game-ux-expert.md +75 -0
- package/expansion-packs/bmad-godot-game-dev/checklists/game-architect-checklist.md +377 -0
- package/expansion-packs/bmad-godot-game-dev/checklists/game-change-checklist.md +250 -0
- package/expansion-packs/bmad-godot-game-dev/checklists/game-design-checklist.md +225 -0
- package/expansion-packs/bmad-godot-game-dev/checklists/game-po-checklist.md +448 -0
- package/expansion-packs/bmad-godot-game-dev/checklists/game-story-dod-checklist.md +202 -0
- package/expansion-packs/bmad-godot-game-dev/config.yaml +30 -0
- package/expansion-packs/bmad-godot-game-dev/data/bmad-kb.md +811 -0
- package/expansion-packs/bmad-godot-game-dev/data/brainstorming-techniques.md +36 -0
- package/expansion-packs/bmad-godot-game-dev/data/development-guidelines.md +893 -0
- package/expansion-packs/bmad-godot-game-dev/data/elicitation-methods.md +156 -0
- package/expansion-packs/bmad-godot-game-dev/data/technical-preferences.md +3 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/advanced-elicitation.md +110 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/apply-qa-fixes.md +224 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/brownfield-create-epic.md +162 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/brownfield-create-story.md +149 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/correct-course-game.md +159 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/create-deep-research-prompt.md +278 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/create-doc.md +103 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/create-game-story.md +202 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/document-project.md +343 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/execute-checklist.md +88 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/facilitate-brainstorming-session.md +136 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/game-brownfield-create-epic.md +160 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/game-brownfield-create-story.md +147 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/game-design-brainstorming.md +290 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/game-risk-profile.md +368 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/game-test-design.md +219 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/generate-ai-frontend-prompt.md +51 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/kb-mode-interaction.md +77 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/review-game-story.md +364 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/shard-doc.md +187 -0
- package/expansion-packs/bmad-godot-game-dev/tasks/validate-game-story.md +208 -0
- package/expansion-packs/bmad-godot-game-dev/templates/brainstorming-output-tmpl.yaml +156 -0
- package/expansion-packs/bmad-godot-game-dev/templates/brownfield-prd-tmpl.yaml +281 -0
- package/expansion-packs/bmad-godot-game-dev/templates/competitor-analysis-tmpl.yaml +306 -0
- package/expansion-packs/bmad-godot-game-dev/templates/game-architecture-tmpl.yaml +1111 -0
- package/expansion-packs/bmad-godot-game-dev/templates/game-brief-tmpl.yaml +356 -0
- package/expansion-packs/bmad-godot-game-dev/templates/game-design-doc-tmpl.yaml +724 -0
- package/expansion-packs/bmad-godot-game-dev/templates/game-prd-tmpl.yaml +209 -0
- package/expansion-packs/bmad-godot-game-dev/templates/game-qa-gate-tmpl.yaml +186 -0
- package/expansion-packs/bmad-godot-game-dev/templates/game-story-tmpl.yaml +406 -0
- package/expansion-packs/bmad-godot-game-dev/templates/game-ui-spec-tmpl.yaml +601 -0
- package/expansion-packs/bmad-godot-game-dev/templates/level-design-doc-tmpl.yaml +620 -0
- package/expansion-packs/bmad-godot-game-dev/templates/market-research-tmpl.yaml +418 -0
- package/expansion-packs/bmad-godot-game-dev/utils/bmad-doc-template.md +327 -0
- package/expansion-packs/bmad-godot-game-dev/utils/workflow-management.md +71 -0
- package/expansion-packs/bmad-godot-game-dev/workflows/game-dev-greenfield.yaml +245 -0
- package/expansion-packs/bmad-godot-game-dev/workflows/game-prototype.yaml +213 -0
- package/package.json +1 -1
- package/release_notes.md +11 -2
- package/tools/flattener/ignoreRules.js +2 -0
- package/tools/installer/bin/bmad.js +2 -1
- package/tools/installer/config/install.config.yaml +16 -7
- package/tools/installer/lib/ide-setup.js +192 -80
- package/tools/installer/package.json +1 -1
- package/tools/upgraders/v3-to-v4-upgrader.js +1 -0
- package/test.md +0 -1
- /package/{implement-fork-friendly-ci.sh → tools/implement-fork-friendly-ci.sh} +0 -0
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
# Game Development Guidelines (Godot, GDScript & C#)
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This document establishes coding standards, architectural patterns, and development practices for game development using Godot Engine with GDScript and C#. These guidelines ensure consistency, performance (60+ FPS target), maintainability, and enforce Test-Driven Development (TDD) across all game development stories.
|
|
6
|
+
|
|
7
|
+
## Performance Philosophy
|
|
8
|
+
|
|
9
|
+
Following John Carmack's principles:
|
|
10
|
+
|
|
11
|
+
- **"Measure, don't guess"** - Profile everything with Godot's built-in profiler
|
|
12
|
+
- **"Focus on what matters: framerate and responsiveness"** - 60+ FPS is the minimum, not the target
|
|
13
|
+
- **"The best code is no code"** - Simplicity beats cleverness
|
|
14
|
+
- **"Think about cache misses, not instruction counts"** - Memory access patterns matter most
|
|
15
|
+
|
|
16
|
+
## GDScript Standards
|
|
17
|
+
|
|
18
|
+
### Naming Conventions
|
|
19
|
+
|
|
20
|
+
**Classes and Scripts:**
|
|
21
|
+
|
|
22
|
+
- PascalCase for class names: `PlayerController`, `GameData`, `InventorySystem`
|
|
23
|
+
- Snake_case for file names: `player_controller.gd`, `game_data.gd`
|
|
24
|
+
- Descriptive names that indicate purpose: `GameStateManager` not `GSM`
|
|
25
|
+
|
|
26
|
+
**Functions and Methods:**
|
|
27
|
+
|
|
28
|
+
- Snake_case for functions: `calculate_damage()`, `process_input()`
|
|
29
|
+
- Descriptive verb phrases: `activate_shield()` not `shield()`
|
|
30
|
+
- Private methods prefix with underscore: `_update_health()`
|
|
31
|
+
|
|
32
|
+
**Variables and Properties:**
|
|
33
|
+
|
|
34
|
+
- Snake_case for variables: `player_health`, `movement_speed`
|
|
35
|
+
- Constants in UPPER_SNAKE_CASE: `MAX_HEALTH`, `GRAVITY_FORCE`
|
|
36
|
+
- Export variables with clear names: `@export var jump_height: float = 5.0`
|
|
37
|
+
- Boolean variables with is/has/can prefix: `is_alive`, `has_key`, `can_jump`
|
|
38
|
+
- Signal names in snake_case: `health_changed`, `level_completed`
|
|
39
|
+
|
|
40
|
+
### Static Typing (MANDATORY for Performance)
|
|
41
|
+
|
|
42
|
+
**Always use static typing for 10-20% performance gain:**
|
|
43
|
+
|
|
44
|
+
```gdscript
|
|
45
|
+
# GOOD - Static typing
|
|
46
|
+
extends CharacterBody2D
|
|
47
|
+
|
|
48
|
+
@export var max_health: int = 100
|
|
49
|
+
@export var movement_speed: float = 300.0
|
|
50
|
+
|
|
51
|
+
var current_health: int
|
|
52
|
+
var velocity_multiplier: float = 1.0
|
|
53
|
+
|
|
54
|
+
func take_damage(amount: int) -> void:
|
|
55
|
+
current_health -= amount
|
|
56
|
+
if current_health <= 0:
|
|
57
|
+
_die()
|
|
58
|
+
|
|
59
|
+
func _die() -> void:
|
|
60
|
+
queue_free()
|
|
61
|
+
|
|
62
|
+
# BAD - Dynamic typing (avoid)
|
|
63
|
+
var health = 100 # No type specified
|
|
64
|
+
func take_damage(amount): # No parameter or return type
|
|
65
|
+
health -= amount
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## C# Standards (for Performance-Critical Systems)
|
|
69
|
+
|
|
70
|
+
### When to Use C# vs GDScript
|
|
71
|
+
|
|
72
|
+
**Use C# for:**
|
|
73
|
+
|
|
74
|
+
- Complex algorithms (pathfinding, procedural generation)
|
|
75
|
+
- Heavy mathematical computations
|
|
76
|
+
- Performance-critical systems identified by profiler
|
|
77
|
+
- External .NET library integration
|
|
78
|
+
- Large-scale data processing
|
|
79
|
+
|
|
80
|
+
**Use GDScript for:**
|
|
81
|
+
|
|
82
|
+
- Rapid prototyping and iteration
|
|
83
|
+
- UI and menu systems
|
|
84
|
+
- Simple game logic
|
|
85
|
+
- Editor tools and scene management
|
|
86
|
+
- Quick gameplay tweaks
|
|
87
|
+
|
|
88
|
+
### C# Naming Conventions
|
|
89
|
+
|
|
90
|
+
```csharp
|
|
91
|
+
using Godot;
|
|
92
|
+
|
|
93
|
+
public partial class PlayerController : CharacterBody2D
|
|
94
|
+
{
|
|
95
|
+
// Public fields (use sparingly, prefer properties)
|
|
96
|
+
[Export] public float MoveSpeed = 300.0f;
|
|
97
|
+
|
|
98
|
+
// Private fields with underscore prefix
|
|
99
|
+
private int _currentHealth;
|
|
100
|
+
private float _jumpVelocity;
|
|
101
|
+
|
|
102
|
+
// Properties with PascalCase
|
|
103
|
+
public int MaxHealth { get; set; } = 100;
|
|
104
|
+
|
|
105
|
+
// Methods with PascalCase
|
|
106
|
+
public void TakeDamage(int amount)
|
|
107
|
+
{
|
|
108
|
+
_currentHealth -= amount;
|
|
109
|
+
if (_currentHealth <= 0)
|
|
110
|
+
{
|
|
111
|
+
Die();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private void Die()
|
|
116
|
+
{
|
|
117
|
+
QueueFree();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Godot Architecture Patterns
|
|
123
|
+
|
|
124
|
+
### Node-Based Architecture
|
|
125
|
+
|
|
126
|
+
**Scene Composition Over Inheritance:**
|
|
127
|
+
|
|
128
|
+
```gdscript
|
|
129
|
+
# Player.tscn structure:
|
|
130
|
+
# Player (CharacterBody2D)
|
|
131
|
+
# ├── Sprite2D
|
|
132
|
+
# ├── CollisionShape2D
|
|
133
|
+
# ├── PlayerHealth (Node)
|
|
134
|
+
# ├── PlayerMovement (Node)
|
|
135
|
+
# └── PlayerInput (Node)
|
|
136
|
+
|
|
137
|
+
# PlayerHealth.gd - Single responsibility component
|
|
138
|
+
extends Node
|
|
139
|
+
class_name PlayerHealth
|
|
140
|
+
|
|
141
|
+
signal health_changed(new_health: int)
|
|
142
|
+
signal died
|
|
143
|
+
|
|
144
|
+
@export var max_health: int = 100
|
|
145
|
+
var current_health: int
|
|
146
|
+
|
|
147
|
+
func _ready() -> void:
|
|
148
|
+
current_health = max_health
|
|
149
|
+
|
|
150
|
+
func take_damage(amount: int) -> void:
|
|
151
|
+
current_health = max(0, current_health - amount)
|
|
152
|
+
health_changed.emit(current_health)
|
|
153
|
+
if current_health == 0:
|
|
154
|
+
died.emit()
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Signal-Based Communication
|
|
158
|
+
|
|
159
|
+
**Decouple Systems with Signals:**
|
|
160
|
+
|
|
161
|
+
```gdscript
|
|
162
|
+
# GameManager.gd - Singleton/Autoload
|
|
163
|
+
extends Node
|
|
164
|
+
|
|
165
|
+
signal game_started
|
|
166
|
+
signal game_over
|
|
167
|
+
signal level_completed
|
|
168
|
+
|
|
169
|
+
var score: int = 0
|
|
170
|
+
var current_level: int = 1
|
|
171
|
+
|
|
172
|
+
func start_game() -> void:
|
|
173
|
+
score = 0
|
|
174
|
+
current_level = 1
|
|
175
|
+
game_started.emit()
|
|
176
|
+
get_tree().change_scene_to_file("res://scenes/levels/level_1.tscn")
|
|
177
|
+
|
|
178
|
+
# Player.gd - Connects to signals
|
|
179
|
+
extends CharacterBody2D
|
|
180
|
+
|
|
181
|
+
func _ready() -> void:
|
|
182
|
+
GameManager.game_over.connect(_on_game_over)
|
|
183
|
+
|
|
184
|
+
func _on_game_over() -> void:
|
|
185
|
+
set_physics_process(false) # Stop player movement
|
|
186
|
+
$AnimationPlayer.play("death")
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Resource-Based Data Management
|
|
190
|
+
|
|
191
|
+
**Use Custom Resources for Game Data:**
|
|
192
|
+
|
|
193
|
+
```gdscript
|
|
194
|
+
# WeaponData.gd - Custom Resource
|
|
195
|
+
extends Resource
|
|
196
|
+
class_name WeaponData
|
|
197
|
+
|
|
198
|
+
@export var weapon_name: String = "Sword"
|
|
199
|
+
@export var damage: int = 10
|
|
200
|
+
@export var attack_speed: float = 1.0
|
|
201
|
+
@export var sprite: Texture2D
|
|
202
|
+
|
|
203
|
+
# Weapon.gd - Uses the resource
|
|
204
|
+
extends Node2D
|
|
205
|
+
class_name Weapon
|
|
206
|
+
|
|
207
|
+
@export var weapon_data: WeaponData
|
|
208
|
+
|
|
209
|
+
func _ready() -> void:
|
|
210
|
+
if weapon_data:
|
|
211
|
+
$Sprite2D.texture = weapon_data.sprite
|
|
212
|
+
|
|
213
|
+
func attack() -> int:
|
|
214
|
+
return weapon_data.damage if weapon_data else 0
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Performance Optimization
|
|
218
|
+
|
|
219
|
+
### Object Pooling (MANDATORY for Spawned Objects)
|
|
220
|
+
|
|
221
|
+
```gdscript
|
|
222
|
+
# ObjectPool.gd - Generic pooling system
|
|
223
|
+
extends Node
|
|
224
|
+
class_name ObjectPool
|
|
225
|
+
|
|
226
|
+
@export var pool_scene: PackedScene
|
|
227
|
+
@export var initial_size: int = 20
|
|
228
|
+
|
|
229
|
+
var _pool: Array[Node] = []
|
|
230
|
+
|
|
231
|
+
func _ready() -> void:
|
|
232
|
+
for i in initial_size:
|
|
233
|
+
var instance := pool_scene.instantiate()
|
|
234
|
+
instance.set_process(false)
|
|
235
|
+
instance.set_physics_process(false)
|
|
236
|
+
instance.visible = false
|
|
237
|
+
add_child(instance)
|
|
238
|
+
_pool.append(instance)
|
|
239
|
+
|
|
240
|
+
func get_object() -> Node:
|
|
241
|
+
for obj in _pool:
|
|
242
|
+
if not obj.visible:
|
|
243
|
+
obj.visible = true
|
|
244
|
+
obj.set_process(true)
|
|
245
|
+
obj.set_physics_process(true)
|
|
246
|
+
return obj
|
|
247
|
+
|
|
248
|
+
# Expand pool if needed
|
|
249
|
+
var new_obj := pool_scene.instantiate()
|
|
250
|
+
add_child(new_obj)
|
|
251
|
+
_pool.append(new_obj)
|
|
252
|
+
return new_obj
|
|
253
|
+
|
|
254
|
+
func return_object(obj: Node) -> void:
|
|
255
|
+
obj.set_process(false)
|
|
256
|
+
obj.set_physics_process(false)
|
|
257
|
+
obj.visible = false
|
|
258
|
+
obj.position = Vector2.ZERO
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Process Optimization
|
|
262
|
+
|
|
263
|
+
**Use Appropriate Process Methods:**
|
|
264
|
+
|
|
265
|
+
```gdscript
|
|
266
|
+
extends Node2D
|
|
267
|
+
|
|
268
|
+
# For physics calculations (fixed timestep)
|
|
269
|
+
func _physics_process(delta: float) -> void:
|
|
270
|
+
# Movement, collision detection
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
# For visual updates and input
|
|
274
|
+
func _process(delta: float) -> void:
|
|
275
|
+
# Animations, UI updates
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
# Use timers or signals instead of checking every frame
|
|
279
|
+
func _ready() -> void:
|
|
280
|
+
var timer := Timer.new()
|
|
281
|
+
timer.wait_time = 1.0
|
|
282
|
+
timer.timeout.connect(_check_condition)
|
|
283
|
+
add_child(timer)
|
|
284
|
+
timer.start()
|
|
285
|
+
|
|
286
|
+
func _check_condition() -> void:
|
|
287
|
+
# Check something once per second instead of 60 times
|
|
288
|
+
pass
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Memory Management
|
|
292
|
+
|
|
293
|
+
**Prevent Memory Leaks:**
|
|
294
|
+
|
|
295
|
+
```gdscript
|
|
296
|
+
extends Node
|
|
297
|
+
|
|
298
|
+
var _connections: Array[Callable] = []
|
|
299
|
+
|
|
300
|
+
func _ready() -> void:
|
|
301
|
+
# Store connections for cleanup
|
|
302
|
+
var callable := GameManager.score_changed.connect(_on_score_changed)
|
|
303
|
+
_connections.append(callable)
|
|
304
|
+
|
|
305
|
+
func _exit_tree() -> void:
|
|
306
|
+
# Clean up connections
|
|
307
|
+
for connection in _connections:
|
|
308
|
+
if connection.is_valid():
|
|
309
|
+
connection.disconnect()
|
|
310
|
+
_connections.clear()
|
|
311
|
+
|
|
312
|
+
# Use queue_free() not free() for nodes
|
|
313
|
+
func remove_enemy(enemy: Node) -> void:
|
|
314
|
+
enemy.queue_free() # Safe deletion
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Test-Driven Development (MANDATORY)
|
|
318
|
+
|
|
319
|
+
### GUT (Godot Unit Test) for GDScript
|
|
320
|
+
|
|
321
|
+
**Write Tests FIRST:**
|
|
322
|
+
|
|
323
|
+
```gdscript
|
|
324
|
+
# test/unit/test_player_health.gd
|
|
325
|
+
extends GutTest
|
|
326
|
+
|
|
327
|
+
var player_health: PlayerHealth
|
|
328
|
+
|
|
329
|
+
func before_each() -> void:
|
|
330
|
+
player_health = PlayerHealth.new()
|
|
331
|
+
player_health.max_health = 100
|
|
332
|
+
|
|
333
|
+
func test_take_damage_reduces_health() -> void:
|
|
334
|
+
# Arrange
|
|
335
|
+
player_health.current_health = 100
|
|
336
|
+
|
|
337
|
+
# Act
|
|
338
|
+
player_health.take_damage(30)
|
|
339
|
+
|
|
340
|
+
# Assert
|
|
341
|
+
assert_eq(player_health.current_health, 70, "Health should be reduced by damage amount")
|
|
342
|
+
|
|
343
|
+
func test_health_cannot_go_negative() -> void:
|
|
344
|
+
# Arrange
|
|
345
|
+
player_health.current_health = 10
|
|
346
|
+
|
|
347
|
+
# Act
|
|
348
|
+
player_health.take_damage(20)
|
|
349
|
+
|
|
350
|
+
# Assert
|
|
351
|
+
assert_eq(player_health.current_health, 0, "Health should not go below 0")
|
|
352
|
+
|
|
353
|
+
func test_died_signal_emitted_at_zero_health() -> void:
|
|
354
|
+
# Arrange
|
|
355
|
+
player_health.current_health = 10
|
|
356
|
+
watch_signals(player_health)
|
|
357
|
+
|
|
358
|
+
# Act
|
|
359
|
+
player_health.take_damage(10)
|
|
360
|
+
|
|
361
|
+
# Assert
|
|
362
|
+
assert_signal_emitted(player_health, "died")
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### GoDotTest for C#
|
|
366
|
+
|
|
367
|
+
```csharp
|
|
368
|
+
using Godot;
|
|
369
|
+
using GoDotTest;
|
|
370
|
+
|
|
371
|
+
[TestClass]
|
|
372
|
+
public class PlayerControllerTests : TestClass
|
|
373
|
+
{
|
|
374
|
+
private PlayerController _player;
|
|
375
|
+
|
|
376
|
+
[TestInitialize]
|
|
377
|
+
public void Setup()
|
|
378
|
+
{
|
|
379
|
+
_player = new PlayerController();
|
|
380
|
+
_player.MaxHealth = 100;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
[Test]
|
|
384
|
+
public void TakeDamage_ReducesHealth()
|
|
385
|
+
{
|
|
386
|
+
// Arrange
|
|
387
|
+
_player.CurrentHealth = 100;
|
|
388
|
+
|
|
389
|
+
// Act
|
|
390
|
+
_player.TakeDamage(30);
|
|
391
|
+
|
|
392
|
+
// Assert
|
|
393
|
+
AssertThat(_player.CurrentHealth).IsEqualTo(70);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
[Test]
|
|
397
|
+
public void TakeDamage_EmitsDiedSignal_WhenHealthReachesZero()
|
|
398
|
+
{
|
|
399
|
+
// Arrange
|
|
400
|
+
_player.CurrentHealth = 10;
|
|
401
|
+
var signalEmitted = false;
|
|
402
|
+
_player.Died += () => signalEmitted = true;
|
|
403
|
+
|
|
404
|
+
// Act
|
|
405
|
+
_player.TakeDamage(10);
|
|
406
|
+
|
|
407
|
+
// Assert
|
|
408
|
+
AssertThat(signalEmitted).IsTrue();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Input Handling
|
|
414
|
+
|
|
415
|
+
### Godot Input System
|
|
416
|
+
|
|
417
|
+
**Input Map Configuration:**
|
|
418
|
+
|
|
419
|
+
```gdscript
|
|
420
|
+
# Configure in Project Settings -> Input Map
|
|
421
|
+
# Actions: "move_left", "move_right", "jump", "attack"
|
|
422
|
+
|
|
423
|
+
extends CharacterBody2D
|
|
424
|
+
|
|
425
|
+
@export var speed: float = 300.0
|
|
426
|
+
@export var jump_velocity: float = -400.0
|
|
427
|
+
|
|
428
|
+
func _physics_process(delta: float) -> void:
|
|
429
|
+
# Add gravity
|
|
430
|
+
if not is_on_floor():
|
|
431
|
+
velocity.y += ProjectSettings.get_setting("physics/2d/default_gravity") * delta
|
|
432
|
+
|
|
433
|
+
# Handle jump
|
|
434
|
+
if Input.is_action_just_pressed("jump") and is_on_floor():
|
|
435
|
+
velocity.y = jump_velocity
|
|
436
|
+
|
|
437
|
+
# Handle movement
|
|
438
|
+
var direction := Input.get_axis("move_left", "move_right")
|
|
439
|
+
velocity.x = direction * speed
|
|
440
|
+
|
|
441
|
+
move_and_slide()
|
|
442
|
+
|
|
443
|
+
# For responsive input (use _unhandled_input for UI priority)
|
|
444
|
+
func _unhandled_input(event: InputEvent) -> void:
|
|
445
|
+
if event.is_action_pressed("attack"):
|
|
446
|
+
_perform_attack()
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
## Scene Management
|
|
450
|
+
|
|
451
|
+
### Scene Loading and Transitions
|
|
452
|
+
|
|
453
|
+
```gdscript
|
|
454
|
+
# SceneManager.gd - Autoload singleton
|
|
455
|
+
extends Node
|
|
456
|
+
|
|
457
|
+
var current_scene: Node = null
|
|
458
|
+
|
|
459
|
+
func _ready() -> void:
|
|
460
|
+
var root := get_tree().root
|
|
461
|
+
current_scene = root.get_child(root.get_child_count() - 1)
|
|
462
|
+
|
|
463
|
+
func change_scene(path: String) -> void:
|
|
464
|
+
call_deferred("_deferred_change_scene", path)
|
|
465
|
+
|
|
466
|
+
func _deferred_change_scene(path: String) -> void:
|
|
467
|
+
# Free current scene
|
|
468
|
+
current_scene.queue_free()
|
|
469
|
+
|
|
470
|
+
# Load new scene
|
|
471
|
+
var new_scene := ResourceLoader.load(path) as PackedScene
|
|
472
|
+
current_scene = new_scene.instantiate()
|
|
473
|
+
get_tree().root.add_child(current_scene)
|
|
474
|
+
get_tree().current_scene = current_scene
|
|
475
|
+
|
|
476
|
+
# With loading screen
|
|
477
|
+
func change_scene_with_loading(path: String) -> void:
|
|
478
|
+
# Show loading screen
|
|
479
|
+
var loading_screen := preload("res://scenes/ui/loading_screen.tscn").instantiate()
|
|
480
|
+
get_tree().root.add_child(loading_screen)
|
|
481
|
+
|
|
482
|
+
# Load in background
|
|
483
|
+
ResourceLoader.load_threaded_request(path)
|
|
484
|
+
|
|
485
|
+
# Wait for completion
|
|
486
|
+
while ResourceLoader.load_threaded_get_status(path) != ResourceLoader.THREAD_LOAD_LOADED:
|
|
487
|
+
await get_tree().process_frame
|
|
488
|
+
|
|
489
|
+
# Switch scenes
|
|
490
|
+
loading_screen.queue_free()
|
|
491
|
+
change_scene(path)
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
## Project Structure
|
|
495
|
+
|
|
496
|
+
```
|
|
497
|
+
res://
|
|
498
|
+
├── scenes/
|
|
499
|
+
│ ├── main/
|
|
500
|
+
│ │ ├── main_menu.tscn
|
|
501
|
+
│ │ └── game.tscn
|
|
502
|
+
│ ├── levels/
|
|
503
|
+
│ │ ├── level_1.tscn
|
|
504
|
+
│ │ └── level_2.tscn
|
|
505
|
+
│ ├── player/
|
|
506
|
+
│ │ └── player.tscn
|
|
507
|
+
│ └── ui/
|
|
508
|
+
│ ├── hud.tscn
|
|
509
|
+
│ └── pause_menu.tscn
|
|
510
|
+
├── scripts/
|
|
511
|
+
│ ├── player/
|
|
512
|
+
│ │ ├── player_controller.gd
|
|
513
|
+
│ │ └── player_health.gd
|
|
514
|
+
│ ├── enemies/
|
|
515
|
+
│ │ └── enemy_base.gd
|
|
516
|
+
│ ├── systems/
|
|
517
|
+
│ │ ├── game_manager.gd
|
|
518
|
+
│ │ └── scene_manager.gd
|
|
519
|
+
│ └── ui/
|
|
520
|
+
│ └── hud_controller.gd
|
|
521
|
+
├── resources/
|
|
522
|
+
│ ├── weapons/
|
|
523
|
+
│ │ └── sword_data.tres
|
|
524
|
+
│ └── enemies/
|
|
525
|
+
│ └── slime_data.tres
|
|
526
|
+
├── assets/
|
|
527
|
+
│ ├── sprites/
|
|
528
|
+
│ ├── audio/
|
|
529
|
+
│ └── fonts/
|
|
530
|
+
├── tests/
|
|
531
|
+
│ ├── unit/
|
|
532
|
+
│ │ └── test_player_health.gd
|
|
533
|
+
│ └── integration/
|
|
534
|
+
│ └── test_level_loading.gd
|
|
535
|
+
└── project.godot
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## Development Workflow
|
|
539
|
+
|
|
540
|
+
### TDD Story Implementation Process
|
|
541
|
+
|
|
542
|
+
1. **Read Story Requirements:**
|
|
543
|
+
- Understand acceptance criteria
|
|
544
|
+
- Identify performance requirements (60+ FPS)
|
|
545
|
+
- Determine GDScript vs C# needs
|
|
546
|
+
|
|
547
|
+
2. **Write Tests FIRST (Red Phase):**
|
|
548
|
+
- Write failing unit tests in GUT/GoDotTest
|
|
549
|
+
- Define expected behavior
|
|
550
|
+
- Run tests to confirm they fail
|
|
551
|
+
|
|
552
|
+
3. **Implement Feature (Green Phase):**
|
|
553
|
+
- Write minimal code to pass tests
|
|
554
|
+
- Follow Godot patterns and conventions
|
|
555
|
+
- Use static typing in GDScript
|
|
556
|
+
- Choose appropriate language (GDScript/C#)
|
|
557
|
+
|
|
558
|
+
4. **Refactor (Refactor Phase):**
|
|
559
|
+
- Optimize for performance
|
|
560
|
+
- Clean up code structure
|
|
561
|
+
- Ensure 60+ FPS maintained
|
|
562
|
+
- Run profiler to validate
|
|
563
|
+
|
|
564
|
+
5. **Integration Testing:**
|
|
565
|
+
- Test scene interactions
|
|
566
|
+
- Validate performance targets
|
|
567
|
+
- Test on all platforms
|
|
568
|
+
|
|
569
|
+
6. **Update Documentation:**
|
|
570
|
+
- Mark story checkboxes complete
|
|
571
|
+
- Document performance metrics
|
|
572
|
+
- Update File List
|
|
573
|
+
|
|
574
|
+
### Performance Checklist
|
|
575
|
+
|
|
576
|
+
- [ ] Stable 60+ FPS achieved
|
|
577
|
+
- [ ] Static typing used in all GDScript
|
|
578
|
+
- [ ] Object pooling for spawned entities
|
|
579
|
+
- [ ] No memory leaks detected
|
|
580
|
+
- [ ] Draw calls optimized
|
|
581
|
+
- [ ] Appropriate process methods used
|
|
582
|
+
- [ ] Signals properly connected/disconnected
|
|
583
|
+
- [ ] Tests written FIRST (TDD)
|
|
584
|
+
- [ ] 80%+ test coverage
|
|
585
|
+
|
|
586
|
+
## Performance Targets
|
|
587
|
+
|
|
588
|
+
### Frame Rate Requirements
|
|
589
|
+
|
|
590
|
+
- **Desktop**: 60+ FPS minimum (144 FPS for high-refresh)
|
|
591
|
+
- **Mobile**: 60 FPS on mid-range devices
|
|
592
|
+
- **Web**: 60 FPS with appropriate export settings
|
|
593
|
+
- **Frame Time**: <16.67ms consistently
|
|
594
|
+
|
|
595
|
+
### Memory Management
|
|
596
|
+
|
|
597
|
+
- **Scene Memory**: Keep under platform limits
|
|
598
|
+
- **Texture Memory**: Optimize imports, use compression
|
|
599
|
+
- **Object Pooling**: Required for bullets, particles, enemies
|
|
600
|
+
- **Reference Cleanup**: Prevent memory leaks
|
|
601
|
+
|
|
602
|
+
### Optimization Priorities
|
|
603
|
+
|
|
604
|
+
1. **Profile First**: Use Godot profiler to identify bottlenecks
|
|
605
|
+
2. **Optimize Algorithms**: Better algorithms beat micro-optimizations
|
|
606
|
+
3. **Reduce Draw Calls**: Batch rendering, use atlases
|
|
607
|
+
4. **Static Typing**: 10-20% performance gain in GDScript
|
|
608
|
+
5. **Language Choice**: Use C# for compute-heavy operations
|
|
609
|
+
|
|
610
|
+
## General Optimization
|
|
611
|
+
|
|
612
|
+
### Anti-Patterns
|
|
613
|
+
|
|
614
|
+
1. **Security Holes**
|
|
615
|
+
- Buffer overflows
|
|
616
|
+
- SQL injection vectors
|
|
617
|
+
- Unvalidated user input
|
|
618
|
+
- Timing attacks
|
|
619
|
+
- Memory disclosure
|
|
620
|
+
- Race conditions with security impact
|
|
621
|
+
|
|
622
|
+
2. **Platform Sabotage**
|
|
623
|
+
- Fighting Godot's scene system
|
|
624
|
+
- Reimplementing platform features
|
|
625
|
+
- Ignoring hardware capabilities
|
|
626
|
+
|
|
627
|
+
## GDScript Optimization
|
|
628
|
+
|
|
629
|
+
### Performance Destroyers
|
|
630
|
+
|
|
631
|
+
1. **Type System Crimes**
|
|
632
|
+
- Dynamic typing anywhere (10-20% performance loss)
|
|
633
|
+
- Variant usage in hot paths
|
|
634
|
+
- Dictionary/Array without typed variants
|
|
635
|
+
- Missing return type hints
|
|
636
|
+
- Untyped function parameters
|
|
637
|
+
|
|
638
|
+
2. **Allocation Disasters**
|
|
639
|
+
- Creating Arrays/Dictionaries in loops
|
|
640
|
+
- String concatenation with +
|
|
641
|
+
- Unnecessary Node instantiation
|
|
642
|
+
- Resource loading in game loop
|
|
643
|
+
- Signal connections without caching
|
|
644
|
+
|
|
645
|
+
3. **Process Method Abuse**
|
|
646
|
+
- \_process() when \_physics_process() suffices
|
|
647
|
+
- Frame-by-frame checks for rare events
|
|
648
|
+
- get_node() calls every frame
|
|
649
|
+
- Node path resolution in loops
|
|
650
|
+
- Unnecessary process enabling
|
|
651
|
+
|
|
652
|
+
### GDScript Death Sentences
|
|
653
|
+
|
|
654
|
+
```gdscript
|
|
655
|
+
# CRIME: Dynamic typing
|
|
656
|
+
var health = 100 # Dies. var health: int = 100
|
|
657
|
+
|
|
658
|
+
# CRIME: String concatenation in loop
|
|
659
|
+
for i in range(1000):
|
|
660
|
+
text += str(i) # Dies. Use StringBuffer or Array.join()
|
|
661
|
+
|
|
662
|
+
# CRIME: get_node every frame
|
|
663
|
+
func _process(delta):
|
|
664
|
+
$UI/Score.text = str(score) # Dies. Cache the node reference
|
|
665
|
+
|
|
666
|
+
# CRIME: Creating objects in loop
|
|
667
|
+
for enemy in enemies:
|
|
668
|
+
var bullet = Bullet.new() # Dies. Object pool
|
|
669
|
+
|
|
670
|
+
# CRIME: Untyped arrays
|
|
671
|
+
var enemies = [] # Dies. var enemies: Array[Enemy] = []
|
|
672
|
+
|
|
673
|
+
# CRIME: Path finding every frame
|
|
674
|
+
func _process(delta):
|
|
675
|
+
find_node("Player") # Dies. Store reference in _ready()
|
|
676
|
+
|
|
677
|
+
# CRIME: Signal spam
|
|
678
|
+
for i in range(100):
|
|
679
|
+
emit_signal("updated", i) # Dies. Batch updates
|
|
680
|
+
|
|
681
|
+
# CRIME: Resource loading in game
|
|
682
|
+
func shoot():
|
|
683
|
+
var bullet_scene = load("res://bullet.tscn") # Dies. Preload
|
|
684
|
+
|
|
685
|
+
# CRIME: Checking rare conditions every frame
|
|
686
|
+
func _process(delta):
|
|
687
|
+
if player_died: # Dies. Use signals
|
|
688
|
+
game_over()
|
|
689
|
+
|
|
690
|
+
# CRIME: Node creation without pooling
|
|
691
|
+
func spawn_particle():
|
|
692
|
+
var p = Particle.new() # Dies. Pool everything spawned
|
|
693
|
+
add_child(p)
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### The Only Acceptable GDScript Patterns
|
|
697
|
+
|
|
698
|
+
```gdscript
|
|
699
|
+
# GOOD: Static typing everywhere
|
|
700
|
+
var health: int = 100
|
|
701
|
+
var speed: float = 300.0
|
|
702
|
+
var enemies: Array[Enemy] = []
|
|
703
|
+
|
|
704
|
+
# GOOD: Cached node references
|
|
705
|
+
@onready var score_label: Label = $UI/Score
|
|
706
|
+
@onready var health_bar: ProgressBar = $UI/HealthBar
|
|
707
|
+
|
|
708
|
+
# GOOD: Preloaded resources
|
|
709
|
+
const BULLET_SCENE: PackedScene = preload("res://bullet.tscn")
|
|
710
|
+
const EXPLOSION_SOUND: AudioStream = preload("res://explosion.ogg")
|
|
711
|
+
|
|
712
|
+
# GOOD: Object pooling
|
|
713
|
+
var bullet_pool: Array[Bullet] = []
|
|
714
|
+
func _ready() -> void:
|
|
715
|
+
for i in 50:
|
|
716
|
+
var bullet := BULLET_SCENE.instantiate() as Bullet
|
|
717
|
+
bullet.visible = false
|
|
718
|
+
bullet_pool.append(bullet)
|
|
719
|
+
|
|
720
|
+
# GOOD: Typed dictionaries
|
|
721
|
+
var player_stats: Dictionary = {
|
|
722
|
+
"health": 100,
|
|
723
|
+
"armor": 50,
|
|
724
|
+
"speed": 300.0
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
# GOOD: Efficient string building
|
|
728
|
+
func build_text(count: int) -> String:
|
|
729
|
+
var parts: PackedStringArray = []
|
|
730
|
+
for i in count:
|
|
731
|
+
parts.append(str(i))
|
|
732
|
+
return "".join(parts)
|
|
733
|
+
|
|
734
|
+
# GOOD: Timer-based checks
|
|
735
|
+
func _ready() -> void:
|
|
736
|
+
var timer := Timer.new()
|
|
737
|
+
timer.wait_time = 1.0
|
|
738
|
+
timer.timeout.connect(_check_rare_condition)
|
|
739
|
+
add_child(timer)
|
|
740
|
+
timer.start()
|
|
741
|
+
|
|
742
|
+
# GOOD: Batch operations
|
|
743
|
+
var updates_pending: Array[int] = []
|
|
744
|
+
func queue_update(value: int) -> void:
|
|
745
|
+
updates_pending.append(value)
|
|
746
|
+
if updates_pending.size() == 1:
|
|
747
|
+
call_deferred("_process_updates")
|
|
748
|
+
|
|
749
|
+
func _process_updates() -> void:
|
|
750
|
+
# Process all updates at once
|
|
751
|
+
for value in updates_pending:
|
|
752
|
+
# Do work
|
|
753
|
+
pass
|
|
754
|
+
updates_pending.clear()
|
|
755
|
+
|
|
756
|
+
# GOOD: Const for compile-time optimization
|
|
757
|
+
const MAX_ENEMIES: int = 100
|
|
758
|
+
const GRAVITY: float = 980.0
|
|
759
|
+
const DEBUG_MODE: bool = false
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### GDScript-Specific Optimization Rules
|
|
763
|
+
|
|
764
|
+
1. **ALWAYS use static typing** - Non-negotiable 10-20% free performance
|
|
765
|
+
2. **NEVER use get_node() in loops** - Cache everything in @onready
|
|
766
|
+
3. **NEVER load() in gameplay** - preload() or ResourceLoader
|
|
767
|
+
4. **NEVER create nodes without pooling** - Pool or die
|
|
768
|
+
5. **NEVER concatenate strings in loops** - PackedStringArray.join()
|
|
769
|
+
6. **ALWAYS use const for constants** - Compile-time optimization
|
|
770
|
+
7. **ALWAYS specify Array types** - Array[Type] not Array
|
|
771
|
+
8. **NEVER check conditions every frame** - Use signals and timers
|
|
772
|
+
9. **ALWAYS batch similar operations** - One update, not many
|
|
773
|
+
10. **NEVER trust the profiler isn't watching** - It always is
|
|
774
|
+
|
|
775
|
+
## Godot C# Optimization
|
|
776
|
+
|
|
777
|
+
### Anti-Patterns
|
|
778
|
+
|
|
779
|
+
1. **Performance Destroyers**
|
|
780
|
+
- ANY allocation in render/game loop
|
|
781
|
+
- String operations in hot paths
|
|
782
|
+
- LINQ anywhere (it allocates, period)
|
|
783
|
+
- Boxing/unboxing in performance code
|
|
784
|
+
- Virtual calls when direct calls possible
|
|
785
|
+
- Cache-hostile data layouts
|
|
786
|
+
- Synchronous I/O blocking computation
|
|
787
|
+
2. **Algorithmic Incompetence**
|
|
788
|
+
- O(n²) when O(n log n) exists
|
|
789
|
+
- O(n³) = fired
|
|
790
|
+
- Linear search in sorted data
|
|
791
|
+
- Recalculating invariants
|
|
792
|
+
- Branches in SIMD loops
|
|
793
|
+
- Random memory access patterns
|
|
794
|
+
|
|
795
|
+
3. **Architectural Cancer**
|
|
796
|
+
- Abstractions that don't eliminate code
|
|
797
|
+
- Single-implementation interfaces
|
|
798
|
+
- Factory factories
|
|
799
|
+
- 3+ levels of indirection
|
|
800
|
+
- Reflection in performance paths
|
|
801
|
+
- Manager classes (lazy design)
|
|
802
|
+
- Event systems for direct calls
|
|
803
|
+
- Not using SIMD where available
|
|
804
|
+
- Thread-unsafe code in parallel contexts
|
|
805
|
+
|
|
806
|
+
## C#/GODOT SPECIFIC DEATH SENTENCES
|
|
807
|
+
|
|
808
|
+
### Instant Rejection Patterns
|
|
809
|
+
|
|
810
|
+
```csharp
|
|
811
|
+
// CRIME: LINQ in game code
|
|
812
|
+
units.Where(u => u.IsAlive).ToList() // Dies. Pre-filtered array.
|
|
813
|
+
|
|
814
|
+
// CRIME: String operations
|
|
815
|
+
$"Player {name} scored {score}" // Dies. StringBuilder or byte buffer.
|
|
816
|
+
|
|
817
|
+
// CRIME: Boxing
|
|
818
|
+
object value = 42; // Dies. Generic or specific type.
|
|
819
|
+
|
|
820
|
+
// CRIME: Foreach on List<T>
|
|
821
|
+
foreach(var item in list) // Dies. for(int i = 0; i < list.Count; i++)
|
|
822
|
+
|
|
823
|
+
// CRIME: Properties doing work
|
|
824
|
+
public int Count => CalculateCount(); // Dies. Cache or field.
|
|
825
|
+
|
|
826
|
+
// CRIME: Virtual by default
|
|
827
|
+
public virtual void Update() // Dies. Sealed unless NEEDED.
|
|
828
|
+
|
|
829
|
+
// CRIME: Events for direct calls
|
|
830
|
+
public event Action OnUpdate; // Dies. Direct method call.
|
|
831
|
+
|
|
832
|
+
// CRIME: Reflection
|
|
833
|
+
typeof(T).GetMethod("Update") // Dies. Direct call or delegates.
|
|
834
|
+
|
|
835
|
+
// CRIME: Async in game loop
|
|
836
|
+
await LoadDataAsync(); // Dies. Preload or synchronous.
|
|
837
|
+
|
|
838
|
+
// CRIME: GD.Print in production
|
|
839
|
+
GD.Print($"Debug: {value}"); // Dies. Conditional compilation.
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### Godot-Specific Crimes
|
|
843
|
+
|
|
844
|
+
```csharp
|
|
845
|
+
// CRIME: GetNode every frame
|
|
846
|
+
GetNode<Label>("UI/Score") // Dies. Cache in _Ready().
|
|
847
|
+
|
|
848
|
+
// CRIME: Creating Nodes dynamically
|
|
849
|
+
var bullet = bulletScene.Instantiate(); // Dies. Object pool.
|
|
850
|
+
|
|
851
|
+
// CRIME: Signal connections in loops
|
|
852
|
+
unit.HealthChanged += OnHealthChanged; // Dies. Batch updates.
|
|
853
|
+
|
|
854
|
+
// CRIME: _Process without need
|
|
855
|
+
public override void _Process(double delta) // Dies. Use _PhysicsProcess or events.
|
|
856
|
+
|
|
857
|
+
// CRIME: Autoload abuse
|
|
858
|
+
GetNode<GameManager>("/root/GameManager") // Dies. Direct reference.
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
### The Only Acceptable Patterns
|
|
862
|
+
|
|
863
|
+
```csharp
|
|
864
|
+
// GOOD: Pre-allocated buffers
|
|
865
|
+
private readonly Unit[] _units = new Unit[MAX_UNITS];
|
|
866
|
+
private readonly int[] _indices = new int[MAX_UNITS];
|
|
867
|
+
|
|
868
|
+
// GOOD: Struct over class
|
|
869
|
+
public struct UnitData { public int Health; public Vector2I Position; }
|
|
870
|
+
|
|
871
|
+
// GOOD: Data-oriented design
|
|
872
|
+
public struct Units {
|
|
873
|
+
public int[] Health;
|
|
874
|
+
public Vector2I[] Positions;
|
|
875
|
+
public bool[] IsAlive;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// GOOD: Zero-allocation update
|
|
879
|
+
public void Update() {
|
|
880
|
+
int count = _activeCount;
|
|
881
|
+
for (int i = 0; i < count; i++) {
|
|
882
|
+
ref Unit unit = ref _units[i];
|
|
883
|
+
unit.Position += unit.Velocity;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// GOOD: Compile-time elimination
|
|
888
|
+
#if DEBUG
|
|
889
|
+
GD.Print("Debug info");
|
|
890
|
+
#endif
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
These guidelines ensure consistent, high-quality Godot game development that meets performance targets, maintains code quality, and follows TDD practices across all implementation stories.
|