@ericrisco/rsc 0.1.34 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -2
- package/manifest.json +241 -2
- package/package.json +1 -1
- package/scripts/lib/domains.js +1 -0
- package/scripts/lib/recommend.js +4 -0
- package/skills/agent-eval/SKILL.md +1 -1
- package/skills/analyze/SKILL.md +1 -1
- package/skills/brand-voice/SKILL.md +1 -1
- package/skills/brand-voice/references/voice-guide-template.md +14 -0
- package/skills/building-agents/SKILL.md +2 -2
- package/skills/competitor-watch/SKILL.md +9 -4
- package/skills/competitor-watch/references/tracker-schema.md +19 -0
- package/skills/constitution/references/constitution-template.md +10 -0
- package/skills/content-engine/SKILL.md +1 -1
- package/skills/content-engine/references/brief-and-pipeline.md +15 -1
- package/skills/course-builder/references/grounding-and-scoping.md +23 -0
- package/skills/course-storytelling/references/learner-grounding.md +22 -2
- package/skills/decision-records/SKILL.md +14 -1
- package/skills/decision-records/references/templates.md +20 -0
- package/skills/design/references/brand-grounding.md +11 -1
- package/skills/game-design/SKILL.md +170 -0
- package/skills/game-design/evals/README.md +13 -0
- package/skills/game-design/evals/cases.yaml +47 -0
- package/skills/game-design/references/mechanics-and-loops.md +78 -0
- package/skills/game-design/references/playtesting-and-scope.md +58 -0
- package/skills/game-design/references/progression-and-economy.md +70 -0
- package/skills/game-storytelling/SKILL.md +159 -0
- package/skills/game-storytelling/evals/README.md +28 -0
- package/skills/game-storytelling/evals/cases.yaml +51 -0
- package/skills/game-storytelling/references/branching-and-state.md +134 -0
- package/skills/game-storytelling/references/dialogue-tools.md +117 -0
- package/skills/game-storytelling/references/environmental-storytelling.md +71 -0
- package/skills/gamedev-multiplayer/SKILL.md +250 -0
- package/skills/gamedev-multiplayer/evals/README.md +12 -0
- package/skills/gamedev-multiplayer/evals/cases.yaml +44 -0
- package/skills/gamedev-multiplayer/references/godot-high-level-multiplayer.md +133 -0
- package/skills/gamedev-multiplayer/references/prediction-and-latency.md +104 -0
- package/skills/gamedev-multiplayer/references/unity-and-unreal-netcode.md +166 -0
- package/skills/gamedev-pathing/SKILL.md +242 -0
- package/skills/gamedev-pathing/evals/README.md +14 -0
- package/skills/gamedev-pathing/evals/cases.yaml +49 -0
- package/skills/gamedev-pathing/references/navmesh-workflow.md +110 -0
- package/skills/gamedev-pathing/references/search-algorithms.md +135 -0
- package/skills/gamedev-pathing/references/steering-and-avoidance.md +120 -0
- package/skills/gamedev-physics/SKILL.md +225 -0
- package/skills/gamedev-physics/evals/README.md +12 -0
- package/skills/gamedev-physics/evals/cases.yaml +54 -0
- package/skills/gamedev-physics/references/character-controllers.md +120 -0
- package/skills/gamedev-physics/references/determinism-and-queries.md +124 -0
- package/skills/gamedev-physics/references/layers-and-masks.md +115 -0
- package/skills/gamedev-shaders/SKILL.md +252 -0
- package/skills/gamedev-shaders/evals/README.md +14 -0
- package/skills/gamedev-shaders/evals/cases.yaml +45 -0
- package/skills/gamedev-shaders/references/effect-recipes.md +205 -0
- package/skills/gamedev-shaders/references/godot-shading-language.md +133 -0
- package/skills/gamedev-shaders/references/unity-and-unreal-shaders.md +169 -0
- package/skills/gamedev-shipping/SKILL.md +236 -0
- package/skills/gamedev-shipping/evals/README.md +9 -0
- package/skills/gamedev-shipping/evals/cases.yaml +55 -0
- package/skills/gamedev-shipping/references/mobile-and-desktop.md +127 -0
- package/skills/gamedev-shipping/references/stores-and-ci.md +162 -0
- package/skills/gamedev-shipping/references/web-export-headers.md +140 -0
- package/skills/godot/SKILL.md +319 -0
- package/skills/godot/evals/README.md +13 -0
- package/skills/godot/evals/cases.yaml +50 -0
- package/skills/godot/references/export-and-testing.md +102 -0
- package/skills/godot/references/gdscript-style.md +84 -0
- package/skills/godot/references/godot3-to-4-traps.md +89 -0
- package/skills/godot/references/nodes-scenes-resources.md +109 -0
- package/skills/harness/SKILL.md +3 -3
- package/skills/harness/references/daily-curation-automation.md +8 -6
- package/skills/harness/references/obsidian-scaffolding.md +16 -9
- package/skills/harness/references/wiki-article-template.md +31 -14
- package/skills/harness/references/wiki-index-template.md +7 -3
- package/skills/harness/references/wiki-protocol.md +120 -54
- package/skills/harness/references/wiki-worklog-template.md +5 -1
- package/skills/instagram-api/SKILL.md +8 -1
- package/skills/instagram-api/references/insights-metrics.md +8 -1
- package/skills/knowledge-ops/SKILL.md +9 -9
- package/skills/knowledge-ops/evals/cases.yaml +1 -1
- package/skills/knowledge-ops/references/gardening-playbook.md +18 -8
- package/skills/level-design/SKILL.md +175 -0
- package/skills/level-design/evals/README.md +35 -0
- package/skills/level-design/evals/cases.yaml +58 -0
- package/skills/level-design/references/blockout-to-art.md +118 -0
- package/skills/level-design/references/guidance-and-readability.md +152 -0
- package/skills/level-design/references/pacing-and-encounters.md +142 -0
- package/skills/linkedin-api/SKILL.md +10 -1
- package/skills/linkedin-strategy/SKILL.md +12 -0
- package/skills/linkedin-strategy/references/wiki-records.md +27 -1
- package/skills/marketing/references/brand-grounding.md +22 -2
- package/skills/medium-strategy/SKILL.md +12 -0
- package/skills/medium-strategy/references/wiki-records.md +39 -1
- package/skills/medium-writing/SKILL.md +19 -0
- package/skills/plan/references/plan-template.md +12 -1
- package/skills/roast-me/tools/adapters/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/base.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/claude.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/codex.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/gemini.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/registry.cpython-312.pyc +0 -0
- package/skills/shortform-packaging/SKILL.md +7 -2
- package/skills/shortform-packaging/references/package-templates.md +13 -2
- package/skills/shortform-strategy/SKILL.md +30 -6
- package/skills/shortform-strategy/references/learning-loop-template.md +9 -1
- package/skills/specify/references/spec-template.md +12 -1
- package/skills/tiktok-api/SKILL.md +6 -2
- package/skills/tiktok-api/references/wiki-schema.md +38 -4
- package/skills/unity/SKILL.md +282 -0
- package/skills/unity/evals/README.md +8 -0
- package/skills/unity/evals/cases.yaml +43 -0
- package/skills/unity/references/build-and-platforms.md +65 -0
- package/skills/unity/references/input-system.md +102 -0
- package/skills/unity/references/lifecycle-deep-dive.md +75 -0
- package/skills/unreal/SKILL.md +270 -0
- package/skills/unreal/evals/README.md +12 -0
- package/skills/unreal/evals/cases.yaml +45 -0
- package/skills/unreal/references/blueprint-cpp-parity.md +109 -0
- package/skills/unreal/references/gameplay-framework.md +95 -0
- package/skills/unreal/references/packaging.md +83 -0
- package/skills/verify/SKILL.md +11 -1
- package/skills/youtube-api/SKILL.md +7 -2
- package/skills/youtube-api/references/wiki-schema.md +25 -4
- package/skills/youtube-packaging/SKILL.md +10 -2
- package/skills/youtube-packaging/references/description-and-chapters.md +70 -2
- package/skills/youtube-strategy/SKILL.md +9 -2
- package/skills/youtube-strategy/references/wiki-records.md +39 -6
- package/skills/youtube-strategy/scripts/verify.sh +4 -0
- package/skills/youtube-thumbnails/SKILL.md +10 -5
- package/skills/youtube-thumbnails/references/experiment-log-format.md +28 -2
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Web export — cross-origin isolation, headers, and hosts
|
|
2
|
+
|
|
3
|
+
Back to the entrypoint: `../SKILL.md`. This is the single deepest source of "my web build
|
|
4
|
+
works locally but breaks when hosted" bugs. Read it before you host a threaded WebAssembly game.
|
|
5
|
+
|
|
6
|
+
## Why the headers exist
|
|
7
|
+
|
|
8
|
+
Modern engines run the game loop on Web Workers and share memory between them with
|
|
9
|
+
**`SharedArrayBuffer`**. After Spectre/Meltdown, browsers only expose `SharedArrayBuffer` (and
|
|
10
|
+
high-resolution timers) to pages that are **cross-origin isolated**. A page becomes isolated only
|
|
11
|
+
when the server sends **both**:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
15
|
+
Cross-Origin-Embedder-Policy: require-corp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- **COOP: same-origin** severs the page from cross-origin windows/openers (its own browsing-context group).
|
|
19
|
+
- **COEP: require-corp** requires every subresource to explicitly opt in to being embedded (via CORP or CORS), so no untrusted cross-origin resource shares the isolated process.
|
|
20
|
+
|
|
21
|
+
With both present, `self.crossOriginIsolated === true` and `SharedArrayBuffer` is defined. Miss
|
|
22
|
+
either and you get one of: `SharedArrayBuffer is not defined`, a JS exception during engine init,
|
|
23
|
+
an infinite loading bar, or a blank canvas. The **files are correct** — the *response headers* are the bug.
|
|
24
|
+
|
|
25
|
+
Quick self-test in the browser console on the hosted page:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
console.log(self.crossOriginIsolated); // must be true for threaded builds
|
|
29
|
+
console.log(typeof SharedArrayBuffer); // must be "function"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Do you even need the headers?
|
|
33
|
+
|
|
34
|
+
| Situation | Headers needed? |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| Godot 4 web export with **Thread Support ON** | Yes — both. |
|
|
37
|
+
| Godot 4 web export with Thread Support OFF (single-thread) | No — but audio/perf are worse. |
|
|
38
|
+
| Unity WebGL with **multithreading** / threads enabled | Yes — both. |
|
|
39
|
+
| Unity WebGL single-threaded (default older setups) | No. |
|
|
40
|
+
| A build that only *uses* WASM but not threads/SAB | No. |
|
|
41
|
+
|
|
42
|
+
If you can host somewhere that sets headers, **keep threads on** — it's a real perf win. Only
|
|
43
|
+
disable threads when your host genuinely can't set headers and the shim below isn't viable.
|
|
44
|
+
|
|
45
|
+
## Setting the headers per host
|
|
46
|
+
|
|
47
|
+
**Netlify** — `public/_headers` (or the publish dir):
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
/*
|
|
51
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
52
|
+
Cross-Origin-Embedder-Policy: require-corp
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Vercel** — `vercel.json`:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"headers": [{
|
|
60
|
+
"source": "/(.*)",
|
|
61
|
+
"headers": [
|
|
62
|
+
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
63
|
+
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
|
|
64
|
+
]
|
|
65
|
+
}]
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**nginx**:
|
|
70
|
+
|
|
71
|
+
```nginx
|
|
72
|
+
location / {
|
|
73
|
+
add_header Cross-Origin-Opener-Policy same-origin always;
|
|
74
|
+
add_header Cross-Origin-Embedder-Policy require-corp always;
|
|
75
|
+
# serve pre-compressed wasm/pck if present
|
|
76
|
+
gzip_static on;
|
|
77
|
+
}
|
|
78
|
+
# correct MIME + long cache for the payload
|
|
79
|
+
types { application/wasm wasm; }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Apache** — `.htaccess`:
|
|
83
|
+
|
|
84
|
+
```apache
|
|
85
|
+
Header set Cross-Origin-Opener-Policy "same-origin"
|
|
86
|
+
Header set Cross-Origin-Embedder-Policy "require-corp"
|
|
87
|
+
AddType application/wasm .wasm
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Cloudflare Pages** — a `_headers` file like Netlify's, or a Transform Rule adding both response headers.
|
|
91
|
+
|
|
92
|
+
**itch.io** — no file needed: in the upload settings tick **"This file will be played in the
|
|
93
|
+
browser"**, then check **"SharedArrayBuffer support"**. itch serves your build from a domain that
|
|
94
|
+
sends COOP/COEP for you. This is the fastest correct web host for a jam/demo.
|
|
95
|
+
|
|
96
|
+
**GitHub Pages / plain static CDN with no header control** — you **cannot** set response headers.
|
|
97
|
+
Two escapes:
|
|
98
|
+
|
|
99
|
+
1. Export **without** threads (single-threaded) — always works, no headers, some perf loss.
|
|
100
|
+
2. The **`coi-serviceworker`** shim: a small service worker (`coi-serviceworker.js`) you include
|
|
101
|
+
from the page; on first load it registers, reloads once, and thereafter re-serves every
|
|
102
|
+
response with COOP/COEP injected, faking cross-origin isolation client-side. Caveats: it needs
|
|
103
|
+
HTTPS, adds a reload, and won't help resources it can't intercept. Good enough for GitHub Pages demos.
|
|
104
|
+
|
|
105
|
+
## COEP fallout — the second-order breakage
|
|
106
|
+
|
|
107
|
+
`COEP: require-corp` also applies to **every** subresource the page loads. Anything cross-origin
|
|
108
|
+
(web fonts, analytics scripts, remote images, iframes, a leaderboard API on another domain) is
|
|
109
|
+
**blocked** unless it sends `Cross-Origin-Resource-Policy: cross-origin` (or proper CORS). Symptoms:
|
|
110
|
+
fonts vanish, a third-party embed 404s in the network tab with a CORP error.
|
|
111
|
+
|
|
112
|
+
Fixes, in order of preference:
|
|
113
|
+
1. **Self-host** the asset (same origin ⇒ no CORP needed). Best for fonts and small scripts.
|
|
114
|
+
2. Ask the third party to send `Cross-Origin-Resource-Policy: cross-origin` (or use their CORS endpoint).
|
|
115
|
+
3. Switch that page to **`COEP: credentialless`** — a looser mode that loads cross-origin resources
|
|
116
|
+
without credentials and still grants isolation in supporting browsers. Narrower support than `require-corp`; test.
|
|
117
|
+
|
|
118
|
+
## Engine specifics
|
|
119
|
+
|
|
120
|
+
**Godot 4 web**
|
|
121
|
+
- `Editor ▸ Manage Export Templates` must match the editor build; then a **Web** export preset.
|
|
122
|
+
- **Thread Support** toggle drives whether you need the headers. **Extensions Support** enables GDExtension on web.
|
|
123
|
+
- Output: `index.html` + `.js` + `.wasm` + `.pck` (+ worker/audio files). Serve `.wasm` as `application/wasm` and enable gzip/brotli — the `.wasm`/`.pck` are the bulk of the download.
|
|
124
|
+
- **C#/.NET edition has no web export in Godot 4.x** (planned for a later release with .NET 10). If the project is C#, either port the web-facing build to GDScript or drop web as a target. Do not promise a C# web build.
|
|
125
|
+
- Headless CI export: `godot --headless --export-release "Web" build/index.html`.
|
|
126
|
+
|
|
127
|
+
**Unity WebGL**
|
|
128
|
+
- `File ▸ Build Profiles ▸ Web`; Player Settings ▸ Publishing: enable/disable threads, set the
|
|
129
|
+
**compression format** (Brotli/Gzip) and keep **Decompression Fallback** on if the host can't
|
|
130
|
+
send `Content-Encoding` — otherwise the browser can't inflate the payload.
|
|
131
|
+
- Threaded WebGL ⇒ same COOP/COEP requirement.
|
|
132
|
+
|
|
133
|
+
## Checklist
|
|
134
|
+
|
|
135
|
+
- [ ] Decide threads on/off; if on, both headers are mandatory.
|
|
136
|
+
- [ ] Headers set at the **host** (or itch's SAB checkbox, or the shim on GitHub Pages).
|
|
137
|
+
- [ ] `self.crossOriginIsolated === true` verified in the console on the deployed URL.
|
|
138
|
+
- [ ] `.wasm` served with `application/wasm` MIME and gzip/brotli.
|
|
139
|
+
- [ ] No cross-origin subresource silently blocked by COEP (check the network tab).
|
|
140
|
+
- [ ] Godot + web ⇒ confirmed GDScript, not C#/.NET.
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: godot
|
|
3
|
+
description: "Use when writing, reviewing, or debugging Godot 4.x games in GDScript (typed) or C# — nodes/scenes/custom Resources, autoload singletons and an EventBus, signals via Callable, @onready/@export/@tool, static typing, CharacterBody2D/3D + velocity + move_and_slide(), _process vs _physics_process, .tscn/.tres text formats, and headless GUT/gdUnit4 tests. Enforces Godot 4 APIs and rejects Godot 3 leftovers: yield→await, onready→@onready, .instance()→.instantiate(), KinematicBody→CharacterBody, Spatial→Node3D, string connect(\"sig\",obj,\"m\")→Callable. Triggers: 'write a Godot player controller', 'my move_and_slide does nothing', 'port this Godot 3 script to 4', 'connect a signal in Godot', 'GDScript won't type-check', 'CharacterBody2D won't move', 'add an autoload', 'escribe un script de Godot', 'por qué no se mueve mi personaje'. NOT Unity MonoBehaviour or Unreal (route to unity/unreal); NOT pure C++/CMake tooling unrelated to GDExtension (that is cpp); NOT shader code in .gdshader (that is gamedev-shaders)."
|
|
4
|
+
tags: [godot, gdscript, game-dev, csharp, godot4]
|
|
5
|
+
recommends: [gamedev-shaders, gamedev-multiplayer, gamedev-physics, gamedev-pathing, gamedev-shipping, game-design, cpp]
|
|
6
|
+
profiles: [full]
|
|
7
|
+
origin: risco
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Godot 4.x (GDScript + C#)
|
|
11
|
+
|
|
12
|
+
Write, review, and debug Godot **4.x** games — 2D and 3D — the way the engine is designed:
|
|
13
|
+
scenes as reusable units, composition over deep node trees, signals for decoupling, static
|
|
14
|
+
typing for speed and safety. GDScript (typed) is primary; C# parity snippets sit beside it.
|
|
15
|
+
|
|
16
|
+
## Version contract — read first
|
|
17
|
+
|
|
18
|
+
**This is Godot 4.x. Never emit Godot 3 APIs.** Godot 4 renamed core nodes, moved annotations
|
|
19
|
+
behind `@`, replaced `yield` with `await`, and switched signal/file/tween APIs. A Godot 3 snippet
|
|
20
|
+
will not even parse in 4.x. Before writing or accepting any line, check it against this ban-list:
|
|
21
|
+
|
|
22
|
+
| Never (Godot 3) | Always (Godot 4.x) |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `yield(timer, "timeout")` | `await timer.timeout` |
|
|
25
|
+
| `onready var x = ...` | `@onready var x = ...` |
|
|
26
|
+
| `export var hp = 3` | `@export var hp := 3` |
|
|
27
|
+
| `tool` (script mode line) | `@tool` (annotation, first line) |
|
|
28
|
+
| `KinematicBody` / `KinematicBody2D` | `CharacterBody3D` / `CharacterBody2D` |
|
|
29
|
+
| `Spatial` | `Node3D` |
|
|
30
|
+
| `Area` / `RigidBody` / `StaticBody` | `Area3D` / `RigidBody3D` / `StaticBody3D` |
|
|
31
|
+
| `Sprite` | `Sprite2D` |
|
|
32
|
+
| `scene.instance()` | `scene.instantiate()` |
|
|
33
|
+
| `move_and_slide(velocity, UP)` (positional) | set `velocity` property, then `move_and_slide()` — **no args** |
|
|
34
|
+
| `connect("hit", self, "_on_hit")` | `node.hit.connect(_on_hit)` (Callable) |
|
|
35
|
+
| `File.new()` / `Directory.new()` | `FileAccess.open(...)` / `DirAccess.open(...)` |
|
|
36
|
+
| standalone `Tween` node + `interpolate_property` | `create_tween()` → `tween.tween_property(...)` |
|
|
37
|
+
| `OS.get_ticks_msec` for gameplay timing | `Time.get_ticks_msec()` (OS timing moved to `Time`) |
|
|
38
|
+
| `PoolByteArray` / `PoolVector2Array` | `PackedByteArray` / `PackedVector2Array` |
|
|
39
|
+
|
|
40
|
+
**Lifecycle overrides must chain the parent** with `super()` (Godot 3 called it implicitly; Godot 4
|
|
41
|
+
does not). If you override `_ready`, `_process`, `_init`, etc. in a script that `extends` another
|
|
42
|
+
script defining them, call `super()` / `super._ready()` or the base logic silently never runs.
|
|
43
|
+
|
|
44
|
+
**Silent-breakers** (compile fine, behave wrong — the dangerous class):
|
|
45
|
+
|
|
46
|
+
- `Array.slice(begin, end)` — `end` is now **exclusive** (was inclusive in Godot 3). `[1,2,3,4].slice(1,3)` → `[2,3]`.
|
|
47
|
+
- `Camera2D.zoom` is **inverted** vs Godot 3: a **larger** zoom now means zoomed **in** (magnified). `Vector2(2,2)` = 2× magnification, not half.
|
|
48
|
+
- `TileMap` is **deprecated** → use one `TileMapLayer` node per layer (since 4.3).
|
|
49
|
+
- Angles are radians; `_process(delta)` delta is a `float` (GDScript) / `double` (C#) in **seconds**.
|
|
50
|
+
|
|
51
|
+
Full table with every rename → `references/godot3-to-4-traps.md`.
|
|
52
|
+
|
|
53
|
+
## Project & scene organization
|
|
54
|
+
|
|
55
|
+
- The **scene** (`.tscn`) is the reusable unit — a self-contained tree you instance many times
|
|
56
|
+
(a Player, a Bullet, a HUD). Prefer **composition**: small scenes/nodes assembled, not one
|
|
57
|
+
60-node monolith. If a subtree has its own behavior, make it its own scene.
|
|
58
|
+
- One responsibility per script. Attach behavior to the scene's **root**; child nodes are parts.
|
|
59
|
+
- `class_name Foo` registers a global type usable in the inspector and as `Foo.new()`. Use it for
|
|
60
|
+
reusable scripts and custom Resources; skip it for one-off scene scripts.
|
|
61
|
+
- **Files**: `snake_case.gd` / `snake_case.tscn` for scenes and scripts; `PascalCase` for node
|
|
62
|
+
names in the tree and for `class_name`. Group by feature (`player/`, `enemy/`, `ui/`), not by type.
|
|
63
|
+
|
|
64
|
+
## Nodes vs scenes vs scripts vs custom Resources
|
|
65
|
+
|
|
66
|
+
| You need | Use |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| A thing in the tree that renders / moves / collides / processes | a **Node** (typed subclass) |
|
|
69
|
+
| A reusable, instanceable bundle of nodes | a **scene** (`.tscn`) |
|
|
70
|
+
| Behavior attached to a node | a **script** (`.gd` / `.cs`) |
|
|
71
|
+
| Pure data (stats, items, dialogue, level config) with no place in the tree | a **custom `Resource`** (`.tres`) |
|
|
72
|
+
|
|
73
|
+
Custom `Resource`s are Godot's typed, savable, inspector-editable data objects — reach for them
|
|
74
|
+
instead of loose Dictionaries or JSON for game data. See `references/nodes-scenes-resources.md`.
|
|
75
|
+
|
|
76
|
+
```gdscript
|
|
77
|
+
class_name EnemyStats extends Resource
|
|
78
|
+
@export var max_health: int = 30
|
|
79
|
+
@export var speed: float = 120.0
|
|
80
|
+
@export var loot_table: Array[ItemDrop] = []
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Autoloads / singletons + EventBus
|
|
84
|
+
|
|
85
|
+
Register a script or scene as an **autoload** (Project → Project Settings → Globals/Autoload) to get
|
|
86
|
+
one always-present instance reachable by name from anywhere. Use it for cross-cutting state (save
|
|
87
|
+
game, audio, run config) — not as a dumping ground.
|
|
88
|
+
|
|
89
|
+
The **EventBus** pattern decouples unrelated systems: an autoload that owns only signals. Emitters
|
|
90
|
+
and listeners never reference each other, just the bus.
|
|
91
|
+
|
|
92
|
+
```gdscript
|
|
93
|
+
# event_bus.gd (autoload named "Events")
|
|
94
|
+
extends Node
|
|
95
|
+
signal enemy_died(position: Vector2, xp: int)
|
|
96
|
+
signal score_changed(new_score: int)
|
|
97
|
+
```
|
|
98
|
+
```gdscript
|
|
99
|
+
# emitter # listener (anywhere)
|
|
100
|
+
Events.enemy_died.emit(global_position, 10)
|
|
101
|
+
Events.enemy_died.connect(_on_enemy_died)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Keep gameplay logic in nodes; let the bus carry the *notification*, not the behavior.
|
|
105
|
+
|
|
106
|
+
## Node access & lifecycle
|
|
107
|
+
|
|
108
|
+
- `_init()` runs at construction (no tree, no `@onready` yet). `_ready()` runs once the node and
|
|
109
|
+
all children are in the tree — do node wiring here.
|
|
110
|
+
- `@onready var x = $Path` defers the assignment to `_ready`, so the child exists. Never grab
|
|
111
|
+
children in `_init`.
|
|
112
|
+
- **Prefer unique names**: mark a node **Unique Name in Owner** (`%`) and access `%HealthBar`
|
|
113
|
+
instead of the fragile, refactor-breaking `get_node("../../UI/HealthBar")`. `$Foo` is fine for a
|
|
114
|
+
direct child.
|
|
115
|
+
- Cache node lookups in `@onready` vars; don't call `get_node` every frame.
|
|
116
|
+
|
|
117
|
+
```gdscript
|
|
118
|
+
extends CharacterBody2D
|
|
119
|
+
@onready var sprite: Sprite2D = $Sprite2D
|
|
120
|
+
@onready var health_bar: ProgressBar = %HealthBar # unique name, position-independent
|
|
121
|
+
|
|
122
|
+
func _ready() -> void:
|
|
123
|
+
super() # chain the parent's _ready if the base defines one
|
|
124
|
+
health_bar.value = 100
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Signals
|
|
128
|
+
|
|
129
|
+
Signals are Godot's decoupling primitive. In Godot 4 you connect a **Callable**, not strings.
|
|
130
|
+
|
|
131
|
+
- **Declare** with typed params; **name in the past tense** for facts that happened
|
|
132
|
+
(`health_depleted`, `item_collected`), present-tense imperative only for requests.
|
|
133
|
+
- **Connect**: `node.signal_name.connect(_on_thing)` — a direct method reference, checked at parse
|
|
134
|
+
time. Add `CONNECT_ONE_SHOT` for auto-disconnect after one fire.
|
|
135
|
+
- **Disconnect discipline**: a connection to a node that gets freed is cleaned up automatically, but
|
|
136
|
+
connections you make to *long-lived* objects (autoloads, the bus) from a short-lived node must be
|
|
137
|
+
disconnected in `_exit_tree()`, or use `CONNECT_ONE_SHOT`, to avoid calls into freed instances.
|
|
138
|
+
|
|
139
|
+
```gdscript
|
|
140
|
+
signal health_depleted
|
|
141
|
+
signal health_changed(current: int, max: int)
|
|
142
|
+
|
|
143
|
+
func take_damage(amount: int) -> void:
|
|
144
|
+
_health -= amount
|
|
145
|
+
health_changed.emit(_health, _max_health)
|
|
146
|
+
if _health <= 0:
|
|
147
|
+
health_depleted.emit()
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## @export / @tool (inspector config)
|
|
151
|
+
|
|
152
|
+
`@export` exposes a variable in the Inspector so designers tune it without touching code. Use ranges,
|
|
153
|
+
groups, and typed exports so the inspector gives real widgets and validation.
|
|
154
|
+
|
|
155
|
+
```gdscript
|
|
156
|
+
@export var title: String = "Level 1"
|
|
157
|
+
@export_range(0.0, 1.0, 0.05) var volume := 0.8
|
|
158
|
+
@export_group("Movement")
|
|
159
|
+
@export var speed: float = 300.0
|
|
160
|
+
@export var jump_velocity: float = -400.0
|
|
161
|
+
@export var projectile: PackedScene # drag a .tscn in the inspector
|
|
162
|
+
@export var stats: EnemyStats # a custom Resource slot
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`@tool` at the top of a script runs it **in the editor** too — for gizmos, procedural previews, or
|
|
166
|
+
validating exported data. Guard runtime-only code with `if Engine.is_editor_hint(): return`.
|
|
167
|
+
|
|
168
|
+
## Static (typed) GDScript
|
|
169
|
+
|
|
170
|
+
Type everything. Typed GDScript is faster (the VM skips dynamic dispatch) and catches errors at parse
|
|
171
|
+
time. Use `:=` when the type is inferable, `: Type` when it isn't, and avoid `Variant`/untyped.
|
|
172
|
+
|
|
173
|
+
```gdscript
|
|
174
|
+
var speed: float = 300.0 # explicit
|
|
175
|
+
var dir := Vector2.ZERO # inferred
|
|
176
|
+
var enemies: Array[Enemy] = [] # typed array
|
|
177
|
+
|
|
178
|
+
func distance_to(target: Node2D) -> float:
|
|
179
|
+
return global_position.distance_to(target.global_position)
|
|
180
|
+
|
|
181
|
+
func _on_body_entered(body: Node) -> void:
|
|
182
|
+
var enemy := body as Enemy # safe cast → null if wrong type, no crash
|
|
183
|
+
if enemy:
|
|
184
|
+
enemy.take_damage(10)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Naming: `snake_case` vars/funcs/signals, `PascalCase` types/`class_name`/nodes, `CONSTANT_CASE`
|
|
188
|
+
consts, tabs for indent, lines < 100 cols. Cheat-sheet → `references/gdscript-style.md`.
|
|
189
|
+
|
|
190
|
+
## `_process` vs `_physics_process`
|
|
191
|
+
|
|
192
|
+
- `_physics_process(delta)` — **fixed** tick (default 60 Hz), the same every step. All movement,
|
|
193
|
+
`move_and_slide()`, forces, and collision-dependent logic go here.
|
|
194
|
+
- `_process(delta)` — runs **once per rendered frame** (variable rate). Use for visuals, UI, and
|
|
195
|
+
non-physics polish.
|
|
196
|
+
- **Always scale rate-based change by `delta`** so behavior is framerate-independent. `move_and_slide()`
|
|
197
|
+
and `move_and_collide()` already fold in `delta` internally — do **not** multiply the velocity you
|
|
198
|
+
hand them by `delta` again.
|
|
199
|
+
|
|
200
|
+
## Resources & data — `.tres` / `.tscn` are strict text formats
|
|
201
|
+
|
|
202
|
+
`.tscn` and `.tres` are line-oriented text with a strict header/section grammar. **Do not hand-edit
|
|
203
|
+
them past trivial value tweaks, and never launch on a file you hand-authored without validating** —
|
|
204
|
+
one bad `ext_resource` id, `[node]` line, or `load_steps` count corrupts the whole scene and Godot
|
|
205
|
+
refuses to open it. Prefer editing through the editor or building Resources in code and `ResourceSaver.save()`.
|
|
206
|
+
|
|
207
|
+
- **`preload("res://x.tscn")`** resolves at **parse/compile** time — the dependency is baked in; use
|
|
208
|
+
for assets you always need.
|
|
209
|
+
- **`load("res://x.tscn")`** resolves at **runtime** — use for dynamic/optional paths (and to avoid
|
|
210
|
+
circular preloads). Both return a `PackedScene`; call `.instantiate()` to get a node.
|
|
211
|
+
|
|
212
|
+
## 2D / 3D bodies quickstart
|
|
213
|
+
|
|
214
|
+
The move-anything-controllable body is `CharacterBody2D` / `CharacterBody3D`. The Godot 4 flow is:
|
|
215
|
+
**write the `velocity` property, then call `move_and_slide()` with no arguments.**
|
|
216
|
+
|
|
217
|
+
```gdscript
|
|
218
|
+
extends CharacterBody2D
|
|
219
|
+
@export var speed: float = 300.0
|
|
220
|
+
@export var jump_velocity: float = -400.0
|
|
221
|
+
|
|
222
|
+
func _physics_process(delta: float) -> void:
|
|
223
|
+
if not is_on_floor():
|
|
224
|
+
velocity += get_gravity() * delta # get_gravity(): project-configured vector
|
|
225
|
+
if Input.is_action_just_pressed("jump") and is_on_floor():
|
|
226
|
+
velocity.y = jump_velocity
|
|
227
|
+
var dir := Input.get_axis("move_left", "move_right")
|
|
228
|
+
velocity.x = dir * speed
|
|
229
|
+
move_and_slide() # NO args in Godot 4 — reads the velocity property
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
3D is identical with `CharacterBody3D`, `Vector3`, and an X/Z input plane; body/area suffixes are
|
|
233
|
+
`3D`. Deeper body/physics tuning → `gamedev-physics`.
|
|
234
|
+
|
|
235
|
+
## Language parity — GDScript ↔ C#
|
|
236
|
+
|
|
237
|
+
Same engine, same nodes; C# uses PascalCase members, `partial` classes, and attributes. Signals
|
|
238
|
+
become C# `event`s (generated by source-gen). C# support requires the **.NET (Mono) build** of Godot.
|
|
239
|
+
|
|
240
|
+
```gdscript
|
|
241
|
+
# GDScript
|
|
242
|
+
extends Node
|
|
243
|
+
signal health_depleted
|
|
244
|
+
@export var speed: float = 300.0
|
|
245
|
+
|
|
246
|
+
func _ready() -> void:
|
|
247
|
+
health_depleted.connect(_on_depleted)
|
|
248
|
+
health_depleted.emit()
|
|
249
|
+
|
|
250
|
+
func _on_depleted() -> void:
|
|
251
|
+
print("dead")
|
|
252
|
+
```
|
|
253
|
+
```csharp
|
|
254
|
+
// C# — same node, .NET build
|
|
255
|
+
using Godot;
|
|
256
|
+
|
|
257
|
+
public partial class Player : Node
|
|
258
|
+
{
|
|
259
|
+
[Signal] public delegate void HealthDepletedEventHandler();
|
|
260
|
+
[Export] public float Speed { get; set; } = 300.0f;
|
|
261
|
+
|
|
262
|
+
public override void _Ready()
|
|
263
|
+
{
|
|
264
|
+
base._Ready(); // chain the parent (== super())
|
|
265
|
+
HealthDepleted += OnDepleted; // connect via the generated event
|
|
266
|
+
EmitSignal(SignalName.HealthDepleted);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private void OnDepleted() => GD.Print("dead");
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`move_and_slide()`→`MoveAndSlide()`, `$Node`→`GetNode<T>("Node")`, `%Node`→`GetNode<T>("%Node")`,
|
|
274
|
+
`preload`→`GD.Load<T>(...)`. Full parity table → `references/export-and-testing.md`.
|
|
275
|
+
|
|
276
|
+
**GDExtension / C++**: for hot native code, build a `godot-cpp` GDExtension (`.gdextension` file,
|
|
277
|
+
`GDREGISTER_CLASS`, `_bind_methods()`) rather than a Godot module — no engine recompile, and it loads
|
|
278
|
+
like any other library. That is native-tooling territory → pair with `cpp`.
|
|
279
|
+
|
|
280
|
+
## Export & testing
|
|
281
|
+
|
|
282
|
+
- **Test with GUT 9.x** (GDScript, `extends GutTest`) or **gdUnit4** (GDScript + C#). Put tests
|
|
283
|
+
under `test/` or `res://tests/`; assert behavior, not private state.
|
|
284
|
+
- **Run headless in CI**: `godot --headless -s addons/gut/gut_cmdln.gd -gdir=res://test -gexit`
|
|
285
|
+
(GUT). Export via templates: `godot --headless --export-release "Linux/X11" build/game.x86_64`.
|
|
286
|
+
- Validate a project before shipping: open in the editor once (catches broken `.tscn`/`.tres`),
|
|
287
|
+
then export-check per platform. Details → `references/export-and-testing.md`.
|
|
288
|
+
|
|
289
|
+
## Guardrails / gotchas
|
|
290
|
+
|
|
291
|
+
- Freeing: call `queue_free()` (safe, end of frame), not `free()` mid-signal. Guard reused refs with
|
|
292
|
+
`is_instance_valid(node)`.
|
|
293
|
+
- Never busy-wait; `await get_tree().create_timer(1.0).timeout` or a signal.
|
|
294
|
+
- `get_node`/`$` on a missing path returns `null` and errors — prefer `%UniqueName` and typed `@onready`.
|
|
295
|
+
- Don't multiply by `delta` twice (see `move_and_slide`). Don't put physics in `_process`.
|
|
296
|
+
- One `TileMapLayer` per layer; `TileMap` is deprecated.
|
|
297
|
+
- Editor-touching code must guard on `Engine.is_editor_hint()` under `@tool`.
|
|
298
|
+
|
|
299
|
+
## Related skills
|
|
300
|
+
|
|
301
|
+
- [`game-design`](../game-design/SKILL.md) — mechanics, loops, and feel *before* you script them.
|
|
302
|
+
- [`gamedev-shaders`](../gamedev-shaders/SKILL.md) — `.gdshader` / visual shaders (out of scope here).
|
|
303
|
+
- [`gamedev-physics`](../gamedev-physics/SKILL.md) — joints, RigidBody tuning, deep collision layers.
|
|
304
|
+
- [`gamedev-pathing`](../gamedev-pathing/SKILL.md) — NavigationAgent, A*, steering beyond the basics.
|
|
305
|
+
- [`gamedev-multiplayer`](../gamedev-multiplayer/SKILL.md) — `MultiplayerSynchronizer`/RPC/netcode.
|
|
306
|
+
- [`gamedev-shipping`](../gamedev-shipping/SKILL.md) — store builds, signing, platform export at scale.
|
|
307
|
+
- [`cpp`](../cpp/SKILL.md) — the GDExtension / `godot-cpp` native side and its CMake/build tooling.
|
|
308
|
+
|
|
309
|
+
## Checklist
|
|
310
|
+
|
|
311
|
+
- [ ] Zero Godot 3 APIs — checked every line against the ban-list (`yield`/`onready`/`instance()`/`KinematicBody`/`Spatial`/string `connect`/`File`).
|
|
312
|
+
- [ ] `move_and_slide()` called with **no args**; `velocity` set as a property; not multiplied by `delta` twice.
|
|
313
|
+
- [ ] Overridden lifecycle methods call `super()` where a base defines them.
|
|
314
|
+
- [ ] Signals connected via Callable (`x.sig.connect(_on_x)`), named past-tense, disconnected/one-shot where needed.
|
|
315
|
+
- [ ] Everything typed (`:` / `:=`, typed params/returns, typed Arrays); no stray `Variant`.
|
|
316
|
+
- [ ] Physics in `_physics_process`, visuals in `_process`; rate-based values scaled by `delta`.
|
|
317
|
+
- [ ] Node access via `@onready` + `%UniqueName`, not deep `get_node("../..")` chains.
|
|
318
|
+
- [ ] `.tscn`/`.tres` not hand-corrupted; project opens in-editor before export.
|
|
319
|
+
- [ ] C# snippets use `partial`, `[Export]`/`[Signal]`, `base._Ready()`, PascalCase (if C# requested).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Eval harness — `godot` skill
|
|
2
|
+
|
|
3
|
+
These cases are read by the rsc skill-eval harness (a Claude agent with the full skill catalog
|
|
4
|
+
loaded) or by hand. They assert two things. **Triggering**: the `godot` description fires on each
|
|
5
|
+
`should_trigger` prompt — including symptom-only and Spanish phrasings — and stays quiet on the
|
|
6
|
+
`should_not_trigger` near-misses, routing each to its named sibling (`unity`, `unreal`,
|
|
7
|
+
`gamedev-shaders`, `cpp`, `game-design`, `gamedev-multiplayer`) instead. **Capability**: with
|
|
8
|
+
`SKILL.md` and its references loaded, the generated Godot code satisfies every
|
|
9
|
+
`capability.must_include` bullet — the Godot 4 flow (`velocity` property + no-arg `move_and_slide()`),
|
|
10
|
+
typed GDScript, Callable signals, `@export`/`@onready`, zero Godot 3 leftovers, and a correct C#
|
|
11
|
+
parity block — and clearly beats the no-skill baseline. Because LLM routing is non-deterministic,
|
|
12
|
+
run 3–5 trials per prompt and pass on a majority. No network is required; the capability rubric is
|
|
13
|
+
judged by reading the output against the bullets.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
skill: godot
|
|
2
|
+
|
|
3
|
+
should_trigger:
|
|
4
|
+
- prompt: "Write a Godot 4 top-down player controller for a CharacterBody2D with 8-direction movement and a dash."
|
|
5
|
+
why: "Core use case: authoring a typed GDScript CharacterBody2D using the velocity + move_and_slide() flow."
|
|
6
|
+
- prompt: "My CharacterBody2D just won't move — I call move_and_slide(velocity) every frame and nothing happens. What's wrong?"
|
|
7
|
+
why: "Symptom-only trigger routing here via the Godot 4 velocity-property + no-arg move_and_slide fix (they passed the Godot 3 positional arg)."
|
|
8
|
+
- prompt: "Convert this Godot 3 script to Godot 4 — it uses onready, yield, .instance() and connect(\"hit\", self, \"_on_hit\")."
|
|
9
|
+
why: "The version-contract ban-list is the skill's center of gravity; each named API has a modern replacement here."
|
|
10
|
+
- prompt: "How do I connect a signal between two nodes in Godot and pass the health value along?"
|
|
11
|
+
why: "Signals via Callable (node.sig.connect / .emit) with typed params is a named trigger."
|
|
12
|
+
- prompt: "Set up an autoload EventBus in Godot so my enemies can broadcast that they died without referencing the score UI."
|
|
13
|
+
why: "Autoload/singleton + EventBus decoupling pattern is explicitly owned by this skill."
|
|
14
|
+
- prompt: "My GDScript won't type-check — how do I properly type an Array of a custom class and cast a colliding body?"
|
|
15
|
+
why: "Static typed GDScript (typed Arrays, `as` casts, typed params/returns) is a core section."
|
|
16
|
+
- prompt: "Show me the same Godot player script in both GDScript and C#, including how the signal and export differ."
|
|
17
|
+
why: "GDScript ↔ C# parity (partial class, [Export]/[Signal], base._Ready()) is a first-class deliverable."
|
|
18
|
+
- prompt: "Escribe un script de Godot 4 para que un CharacterBody3D salte y tenga gravedad."
|
|
19
|
+
why: "Spanish trigger for a Godot 4 CharacterBody3D controller — squarely this skill."
|
|
20
|
+
|
|
21
|
+
should_not_trigger:
|
|
22
|
+
- prompt: "Write a Unity C# MonoBehaviour that moves a Rigidbody with FixedUpdate and input."
|
|
23
|
+
route_to: "unity"
|
|
24
|
+
why: "Unity's MonoBehaviour/Rigidbody/FixedUpdate model is a different engine; C# alone doesn't make it Godot."
|
|
25
|
+
- prompt: "Set up an Unreal Engine character with a Blueprint jump and a C++ movement component."
|
|
26
|
+
route_to: "unreal"
|
|
27
|
+
why: "Unreal Blueprint/UE C++ is a separate engine skill."
|
|
28
|
+
- prompt: "Write a .gdshader that does a dissolve effect with a noise texture and an edge glow."
|
|
29
|
+
why: "Shader authoring in .gdshader/visual shaders is delegated to gamedev-shaders, not GDScript gameplay code."
|
|
30
|
+
route_to: "gamedev-shaders"
|
|
31
|
+
- prompt: "Build a godot-cpp GDExtension purely as a CMake project and set up the compiler toolchain and SCons/CMake flags."
|
|
32
|
+
route_to: "cpp"
|
|
33
|
+
why: "The native C++ build/tooling side is cpp; this skill only points at GDExtension from the gameplay side."
|
|
34
|
+
- prompt: "Design the core game loop and progression economy for my roguelike before I build anything."
|
|
35
|
+
route_to: "game-design"
|
|
36
|
+
why: "Mechanics/loop/feel design precedes scripting and belongs to game-design."
|
|
37
|
+
- prompt: "Implement client-side prediction and server reconciliation for my Godot multiplayer shooter's netcode."
|
|
38
|
+
route_to: "gamedev-multiplayer"
|
|
39
|
+
why: "Deep netcode/RPC/synchronizer work is the multiplayer sibling; this skill stops at single-player node/signal basics."
|
|
40
|
+
|
|
41
|
+
capability:
|
|
42
|
+
- scenario: "Author a Godot 4.x 2D platformer player: a typed CharacterBody2D with gravity, run, and a floor-only jump, that emits a signal when it dies, plus the equivalent C# version."
|
|
43
|
+
must_include:
|
|
44
|
+
- "extends CharacterBody2D and sets the `velocity` property, then calls `move_and_slide()` with NO arguments (not move_and_slide(velocity, ...))"
|
|
45
|
+
- "Applies gravity via get_gravity() * delta (or a project-settings gravity) scaled by delta, inside _physics_process, and does not multiply the velocity passed to move_and_slide by delta again"
|
|
46
|
+
- "Uses gravity/jump only when appropriate with is_on_floor(), and reads input via Input.get_axis / Input.is_action_just_pressed"
|
|
47
|
+
- "Declares a signal (e.g. `signal died`) and emits it with `died.emit()`, never emit_signal string form as the primary API"
|
|
48
|
+
- "Is statically typed: typed params (delta: float), typed vars (:= / : Type), and a -> void / typed return where relevant"
|
|
49
|
+
- "Uses @export for tunables (speed, jump_velocity) and @onready + $ or %UniqueName for node refs — no onready/export without @, no yield, no .instance()"
|
|
50
|
+
- "C# version uses `public partial class ... : CharacterBody2D`, `_PhysicsProcess(double delta)` calling `base` where a base defines it, `[Export]`, `[Signal] ...EventHandler`, `EmitSignal(SignalName....)`, sets `Velocity` then `MoveAndSlide()`"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Export, testing & GDScript↔C# parity (Godot 4.x)
|
|
2
|
+
|
|
3
|
+
## GDScript ↔ C# full parity table
|
|
4
|
+
|
|
5
|
+
C# needs the **.NET (Mono) build** of Godot 4. Members are PascalCase; scripts are `partial` classes
|
|
6
|
+
extending the node type; annotations become attributes; signals become source-generated `event`s.
|
|
7
|
+
|
|
8
|
+
| Concept | GDScript | C# |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| Class decl | `extends Node2D` (+ `class_name Foo`) | `public partial class Foo : Node2D` |
|
|
11
|
+
| Ready | `func _ready():` | `public override void _Ready()` |
|
|
12
|
+
| Physics | `func _physics_process(delta):` (`delta: float`) | `public override void _PhysicsProcess(double delta)` |
|
|
13
|
+
| Chain parent | `super()` / `super._ready()` | `base._Ready()` |
|
|
14
|
+
| Export | `@export var speed := 300.0` | `[Export] public float Speed { get; set; } = 300f;` |
|
|
15
|
+
| Export range | `@export_range(0,1) var v := 0.5` | `[Export(PropertyHint.Range, "0,1")] public float V { get; set; }` |
|
|
16
|
+
| Signal decl | `signal hit(dmg: int)` | `[Signal] public delegate void HitEventHandler(int dmg);` |
|
|
17
|
+
| Emit | `hit.emit(10)` | `EmitSignal(SignalName.Hit, 10)` |
|
|
18
|
+
| Connect | `node.hit.connect(_on_hit)` | `node.Hit += OnHit;` |
|
|
19
|
+
| Disconnect | `node.hit.disconnect(_on_hit)` | `node.Hit -= OnHit;` |
|
|
20
|
+
| Child (direct) | `$Sprite2D` | `GetNode<Sprite2D>("Sprite2D")` |
|
|
21
|
+
| Child (unique) | `%HealthBar` | `GetNode<ProgressBar>("%HealthBar")` |
|
|
22
|
+
| onready | `@onready var s := $Sprite2D` | assign in `_Ready()` |
|
|
23
|
+
| Preload | `const B := preload("res://b.tscn")` | `GD.Load<PackedScene>("res://b.tscn")` |
|
|
24
|
+
| Instance | `B.instantiate()` | `b.Instantiate<Node2D>()` |
|
|
25
|
+
| Free | `queue_free()` | `QueueFree()` |
|
|
26
|
+
| Print | `print(x)` | `GD.Print(x)` |
|
|
27
|
+
| Move | set `velocity` → `move_and_slide()` | set `Velocity` → `MoveAndSlide()` |
|
|
28
|
+
| Await signal | `await obj.sig` | `await ToSignal(obj, Node.SignalName.Sig)` |
|
|
29
|
+
| Await frame | `await get_tree().process_frame` | `await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame)` |
|
|
30
|
+
| Group | `add_to_group("enemies")` | `AddToGroup("enemies")` |
|
|
31
|
+
| Random | `randf_range(0,1)` | `GD.RandRange(0,1)` |
|
|
32
|
+
|
|
33
|
+
C# gotchas: use the generated `SignalName`/`PropertyName`/`MethodName` string caches, not raw
|
|
34
|
+
strings; C# `Vector2`/`Vector3` fields can't be mutated through a property getter (copy → mutate →
|
|
35
|
+
assign back, like `var v = Velocity; v.X = 1; Velocity = v;`); `[Export]` needs a public
|
|
36
|
+
property or field of a Godot-marshalable type.
|
|
37
|
+
|
|
38
|
+
## GDExtension / C++ (native code)
|
|
39
|
+
|
|
40
|
+
For CPU-hot systems (procedural gen, heavy sims), write a **GDExtension** with `godot-cpp` instead of
|
|
41
|
+
recompiling the engine as a module:
|
|
42
|
+
|
|
43
|
+
- A `.gdextension` manifest points at the compiled shared lib per platform.
|
|
44
|
+
- Classes extend a Godot type and register with `GDREGISTER_CLASS(MyClass)`; expose methods/props via
|
|
45
|
+
`static void _bind_methods()` using `ClassDB::bind_method(...)` and `ADD_PROPERTY(...)`.
|
|
46
|
+
- Build with SCons or CMake against the `godot-cpp` bindings; it loads like any resource, no engine
|
|
47
|
+
rebuild. This is native C++ tooling → pair with the `cpp` skill for the build side.
|
|
48
|
+
|
|
49
|
+
## Testing
|
|
50
|
+
|
|
51
|
+
Two mature frameworks for Godot 4:
|
|
52
|
+
|
|
53
|
+
- **GUT 9.x** (Godot Unit Test) — GDScript. Test scripts `extends GutTest`; methods named
|
|
54
|
+
`test_*`; assertions like `assert_eq`, `assert_true`, `assert_signal_emitted`. Install via the
|
|
55
|
+
Asset Library or a git submodule under `addons/gut`.
|
|
56
|
+
- **gdUnit4** — GDScript **and** C#, with scene-runner helpers for driving input and awaiting frames;
|
|
57
|
+
ships CI templates and a fluent `assert_that(...)` API.
|
|
58
|
+
|
|
59
|
+
```gdscript
|
|
60
|
+
# test/test_health.gd
|
|
61
|
+
extends GutTest
|
|
62
|
+
|
|
63
|
+
func test_takes_damage() -> void:
|
|
64
|
+
var h := HealthComponent.new()
|
|
65
|
+
h.max_health = 100
|
|
66
|
+
watch_signals(h)
|
|
67
|
+
h.take_damage(30)
|
|
68
|
+
assert_eq(h.current, 70)
|
|
69
|
+
|
|
70
|
+
func test_emits_died_at_zero() -> void:
|
|
71
|
+
var h := HealthComponent.new()
|
|
72
|
+
watch_signals(h)
|
|
73
|
+
h.take_damage(999)
|
|
74
|
+
assert_signal_emitted(h, "died")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Test **behavior through the public surface** (signals, exported state, method results), not private
|
|
78
|
+
`_underscore` fields. For node-tree tests, `add_child_autofree(node)` so GUT frees it.
|
|
79
|
+
|
|
80
|
+
## Headless CI & export
|
|
81
|
+
|
|
82
|
+
Godot runs without a display via `--headless` — the backbone of CI:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Run the GUT suite headless and exit with the pass/fail code
|
|
86
|
+
godot --headless -s addons/gut/gut_cmdln.gd -gdir=res://test -ginclude_subdirs -gexit
|
|
87
|
+
|
|
88
|
+
# gdUnit4 headless
|
|
89
|
+
godot --headless -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd -a res://test
|
|
90
|
+
|
|
91
|
+
# Import/validate assets once (populates .godot/, catches broken resources) — good first CI step
|
|
92
|
+
godot --headless --import
|
|
93
|
+
|
|
94
|
+
# Export a release build for a preset defined in export_presets.cfg
|
|
95
|
+
godot --headless --export-release "Linux/X11" build/game.x86_64
|
|
96
|
+
godot --headless --export-release "Web" build/index.html
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- Export **templates** for the matching engine version must be installed on the CI runner.
|
|
100
|
+
- Run `--import` before `--export-*` on a clean checkout so `.godot/` is built.
|
|
101
|
+
- A green editor open + a passing headless suite + a successful export per target = shippable; deep
|
|
102
|
+
store/signing/platform work belongs to `gamedev-shipping`.
|