@ericrisco/rsc 0.1.35 → 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 +8 -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/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/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/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/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
|
@@ -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`.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# GDScript style & formatting cheat-sheet (Godot 4.x)
|
|
2
|
+
|
|
3
|
+
Mirrors the official GDScript style guide. When in doubt, run the editor's built-in formatter
|
|
4
|
+
(Ctrl/Cmd+Shift+I) and `gdformat` (from `gdtoolkit`) in CI.
|
|
5
|
+
|
|
6
|
+
## Naming
|
|
7
|
+
|
|
8
|
+
| Thing | Case | Example |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| File (script/scene) | `snake_case` | `player_controller.gd`, `main_menu.tscn` |
|
|
11
|
+
| `class_name` / type | `PascalCase` | `class_name EnemyStats` |
|
|
12
|
+
| Node name in tree | `PascalCase` | `HealthBar`, `AttackTimer` |
|
|
13
|
+
| Variable / function | `snake_case` | `var max_health`, `func take_damage()` |
|
|
14
|
+
| Signal | `snake_case`, past tense | `signal health_depleted` |
|
|
15
|
+
| Constant | `CONSTANT_CASE` | `const MAX_SPEED := 400.0` |
|
|
16
|
+
| Enum type / members | `PascalCase` / `CONSTANT_CASE` | `enum State { IDLE, RUN }` |
|
|
17
|
+
| Private (convention) | leading underscore | `var _internal_timer` |
|
|
18
|
+
| Virtual/lifecycle | leading underscore | `_ready`, `_process`, `_on_body_entered` |
|
|
19
|
+
|
|
20
|
+
## Formatting
|
|
21
|
+
|
|
22
|
+
- **Tabs** for indentation (not spaces). Editor default.
|
|
23
|
+
- Lines **< 100 columns**.
|
|
24
|
+
- One space around binary operators (`a + b`, `x := 1`), none inside call parens.
|
|
25
|
+
- Two blank lines between functions at top level; one inside classes as needed.
|
|
26
|
+
- Prefer `:=` (inferred) when the RHS makes the type obvious; `: Type =` when it does not.
|
|
27
|
+
|
|
28
|
+
## Typed by default
|
|
29
|
+
|
|
30
|
+
```gdscript
|
|
31
|
+
var speed: float = 300.0 # explicit annotation
|
|
32
|
+
var dir := Vector2.ZERO # inferred
|
|
33
|
+
var slots: Array[Item] = [] # typed collection
|
|
34
|
+
const GRAVITY := 980.0
|
|
35
|
+
|
|
36
|
+
func heal(amount: int) -> void: # typed params AND return
|
|
37
|
+
_health = mini(_health + amount, _max_health)
|
|
38
|
+
|
|
39
|
+
func find_target() -> Node2D: # return type declared
|
|
40
|
+
return get_tree().get_first_node_in_group("player")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- Declare `-> void` on functions that return nothing; declare the real type otherwise.
|
|
44
|
+
- Use `as` for safe downcasts: `var e := body as Enemy` → `null` on mismatch (no crash).
|
|
45
|
+
- Avoid untyped `var x` and `Variant` params — they disable the compiler's checks and the VM's
|
|
46
|
+
typed fast paths.
|
|
47
|
+
|
|
48
|
+
## Script layout order (recommended top-to-bottom)
|
|
49
|
+
|
|
50
|
+
1. `@tool` (if any) — must be the very first line.
|
|
51
|
+
2. `class_name` then `extends` (or `extends` alone).
|
|
52
|
+
3. `## docstring` comment.
|
|
53
|
+
4. `signal` declarations.
|
|
54
|
+
5. `enum` / `const`.
|
|
55
|
+
6. `@export` vars, then plain member vars, then `@onready` vars.
|
|
56
|
+
7. Built-in virtuals: `_init`, `_ready`, `_process`, `_physics_process`, `_input`, …
|
|
57
|
+
8. Public methods, then private (`_underscore`) methods, then signal handlers (`_on_*`).
|
|
58
|
+
|
|
59
|
+
```gdscript
|
|
60
|
+
@tool
|
|
61
|
+
class_name Turret extends Node2D
|
|
62
|
+
## Auto-aiming turret; @export the fire rate in the inspector.
|
|
63
|
+
|
|
64
|
+
signal fired(at: Vector2)
|
|
65
|
+
|
|
66
|
+
const MAX_RANGE := 600.0
|
|
67
|
+
enum Mode { IDLE, TRACKING }
|
|
68
|
+
|
|
69
|
+
@export var fire_rate := 2.0
|
|
70
|
+
var _cooldown := 0.0
|
|
71
|
+
@onready var muzzle: Marker2D = %Muzzle
|
|
72
|
+
|
|
73
|
+
func _physics_process(delta: float) -> void:
|
|
74
|
+
_cooldown = maxf(_cooldown - delta, 0.0)
|
|
75
|
+
|
|
76
|
+
func _on_area_entered(_area: Area2D) -> void:
|
|
77
|
+
pass
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Common built-in math helpers (Godot 4 names)
|
|
81
|
+
|
|
82
|
+
`absf/absi`, `minf/mini`, `maxf/maxi`, `clampf/clampi`, `snappedf`, `lerpf`, `move_toward`,
|
|
83
|
+
`deg_to_rad` / `rad_to_deg`, `randf_range`, `randi_range`. The unsuffixed `abs/min/max/clamp` still
|
|
84
|
+
work on Variants but the typed suffixed forms are faster and clearer.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Godot 3 → 4.x migration ban-list (full table)
|
|
2
|
+
|
|
3
|
+
Godot 4 is a hard break from 3.x. The engine ships a project converter, but it does **not** catch
|
|
4
|
+
everything — and an agent generating fresh code must simply never emit the left column. Use this as
|
|
5
|
+
the authoritative reference when porting or reviewing.
|
|
6
|
+
|
|
7
|
+
## Won't-parse renames (hard errors)
|
|
8
|
+
|
|
9
|
+
| Godot 3 | Godot 4.x | Note |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `yield(obj, "signal")` | `await obj.signal` | Coroutines are now `await`; the function must not need `yield`'s return-value trick. |
|
|
12
|
+
| `yield(get_tree().create_timer(1),"timeout")` | `await get_tree().create_timer(1.0).timeout` | |
|
|
13
|
+
| `onready var` | `@onready var` | Annotation. |
|
|
14
|
+
| `export var` / `export(int) var` | `@export var` / `@export_range(...) var` | Typed annotations replace the `export(Type)` hint syntax. |
|
|
15
|
+
| `tool` | `@tool` | Must be the first line. |
|
|
16
|
+
| `remote`/`master`/`puppet func` | `@rpc(...)` annotations | Networking model changed entirely. |
|
|
17
|
+
| `setget set_fn, get_fn` | `set(v): ...` / `get: ...` inline accessors | |
|
|
18
|
+
|
|
19
|
+
## Node / class renames
|
|
20
|
+
|
|
21
|
+
| Godot 3 | Godot 4.x |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `Spatial` | `Node3D` |
|
|
24
|
+
| `KinematicBody` / `KinematicBody2D` | `CharacterBody3D` / `CharacterBody2D` |
|
|
25
|
+
| `Area` | `Area3D` |
|
|
26
|
+
| `RigidBody` / `StaticBody` | `RigidBody3D` / `StaticBody3D` |
|
|
27
|
+
| `Sprite` | `Sprite2D` |
|
|
28
|
+
| `Sprite3D` | `Sprite3D` (unchanged) |
|
|
29
|
+
| `AnimatedSprite` | `AnimatedSprite2D` |
|
|
30
|
+
| `CollisionShape` | `CollisionShape3D` |
|
|
31
|
+
| `Camera` | `Camera3D` |
|
|
32
|
+
| `Light2D` (old) / `Light` | `PointLight2D` / `DirectionalLight2D` / `OmniLight3D` etc. |
|
|
33
|
+
| `Viewport` (as node) | `SubViewport` (embedded) |
|
|
34
|
+
| `YSort` (node) | `y_sort_enabled` property on `Node2D`/`CanvasItem` |
|
|
35
|
+
| `ARVROrigin` / `ARVRCamera` | `XROrigin3D` / `XRCamera3D` |
|
|
36
|
+
| `Reference` | `RefCounted` |
|
|
37
|
+
| `TileMap` | `TileMapLayer` (one per layer; `TileMap` deprecated 4.3+) |
|
|
38
|
+
|
|
39
|
+
## Method / API changes
|
|
40
|
+
|
|
41
|
+
| Godot 3 | Godot 4.x |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `scene.instance()` | `scene.instantiate()` |
|
|
44
|
+
| `move_and_slide(velocity, up_direction)` | set `velocity` property → `move_and_slide()` (no args) |
|
|
45
|
+
| `move_and_slide_with_snap(...)` | set `floor_snap_length` → `move_and_slide()` |
|
|
46
|
+
| `connect("sig", obj, "method", [args])` | `obj.sig.connect(method.bind(args))` (Callable) |
|
|
47
|
+
| `disconnect("sig", obj, "method")` | `obj.sig.disconnect(method)` |
|
|
48
|
+
| `emit_signal("sig", a)` | `sig.emit(a)` (string form still exists but prefer the property) |
|
|
49
|
+
| `is_connected("sig", obj, "m")` | `sig.is_connected(m)` |
|
|
50
|
+
| `call_deferred("method", a)` | `method.bind(a).call_deferred()` or `call_deferred("method", a)` |
|
|
51
|
+
| `File.new()` + `open()` | `FileAccess.open(path, FileAccess.READ)` |
|
|
52
|
+
| `Directory.new()` | `DirAccess.open(path)` / `DirAccess.dir_exists_absolute(...)` |
|
|
53
|
+
| `OS.get_ticks_msec()` | `Time.get_ticks_msec()` |
|
|
54
|
+
| `OS.get_datetime()` | `Time.get_datetime_dict_from_system()` |
|
|
55
|
+
| `PoolByteArray` / `PoolStringArray` / `PoolVector2Array` | `PackedByteArray` / `PackedStringArray` / `PackedVector2Array` |
|
|
56
|
+
| `Tween` node + `interpolate_property` | `var t := create_tween(); t.tween_property(node, "position", target, 0.5)` |
|
|
57
|
+
| `rand_range(a, b)` | `randf_range(a, b)` (float) / `randi_range(a, b)` (int) |
|
|
58
|
+
| `deg2rad` / `rad2deg` | `deg_to_rad` / `rad_to_deg` |
|
|
59
|
+
| `stepify(x, s)` | `snapped(x, s)` |
|
|
60
|
+
| `.empty()` | `.is_empty()` |
|
|
61
|
+
| `get_node("Path")` still valid | prefer `%UniqueName` for refactor-safety |
|
|
62
|
+
| `KEY_*` / input constants mostly renamed | check `Key`, `MouseButton` enums |
|
|
63
|
+
|
|
64
|
+
## Silent behavior changes (compile clean, act wrong)
|
|
65
|
+
|
|
66
|
+
- **`Array.slice(begin, end, step, deep)`** — `end` is **exclusive** in Godot 4 (was inclusive in 3).
|
|
67
|
+
`[10,20,30,40].slice(1,3)` → `[20,30]`.
|
|
68
|
+
- **`Camera2D.zoom`** — meaning **inverted**. Larger value = zoom **in**. `zoom = Vector2(2,2)`
|
|
69
|
+
magnifies 2×; in Godot 3 that zoomed out. To zoom out now, use values **below** 1.
|
|
70
|
+
- **Y-sort** is a property (`y_sort_enabled`), not a node — porting a `YSort` parent silently loses
|
|
71
|
+
sorting unless you set the property.
|
|
72
|
+
- **Physics interpolation / gravity** — read gravity via `get_gravity()` (2D/3D) or
|
|
73
|
+
`ProjectSettings`; the old default constants changed.
|
|
74
|
+
- **Signal argument order** on some built-in signals changed; verify against the 4.x docs, don't
|
|
75
|
+
assume the 3.x signature.
|
|
76
|
+
- **`_ready`/`_process` no longer auto-call the base** — you must `super()` when extending a script
|
|
77
|
+
that defines them.
|
|
78
|
+
- **Integer division** with typed ints truncates as expected; mixing float/int in Godot 4 follows
|
|
79
|
+
stricter typed rules — annotate to avoid surprises.
|
|
80
|
+
|
|
81
|
+
## Porting workflow
|
|
82
|
+
|
|
83
|
+
1. Open the Godot 3 project in Godot 4 → run the built-in **Project Converter** (backs up first).
|
|
84
|
+
2. Fix red parse errors top-down (renames above).
|
|
85
|
+
3. Search the codebase for each string-form `connect(`, `yield(`, `.instance()`, `File.new`,
|
|
86
|
+
`Directory.new`, `Pool*Array`, `move_and_slide(` with args — the converter misses some.
|
|
87
|
+
4. Fix the silent-breakers deliberately (slice bounds, Camera2D zoom, y-sort) — tests or a manual
|
|
88
|
+
pass, since nothing errors.
|
|
89
|
+
5. Re-open every scene once; a converter-mangled `.tscn` fails loudly in the editor.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Nodes, scenes, scripts & custom Resources (Godot 4.x)
|
|
2
|
+
|
|
3
|
+
The four building blocks and when each is the right tool. Getting this decomposition right is what
|
|
4
|
+
separates a maintainable Godot project from a monolithic scene that nobody can touch.
|
|
5
|
+
|
|
6
|
+
## The mental model
|
|
7
|
+
|
|
8
|
+
- **Node** — a live object in the running tree. It has a transform (2D/3D), can render, process,
|
|
9
|
+
collide, receive input. Everything on screen is a node. Pick the most specific typed subclass
|
|
10
|
+
(`Sprite2D`, `Area3D`, `CharacterBody2D`, `Timer`, `AudioStreamPlayer`).
|
|
11
|
+
- **Scene** (`.tscn`) — a saved tree of nodes with a designated **root**, instanceable N times. The
|
|
12
|
+
reusable unit of a Godot game. A scene can instance other scenes (composition).
|
|
13
|
+
- **Script** (`.gd` / `.cs`) — code attached to one node, giving it behavior. Attach to the scene
|
|
14
|
+
root; treat children as parts the root drives.
|
|
15
|
+
- **Resource** (`.tres` / `.res`) — a reference-counted **data** object, not in the tree. Textures,
|
|
16
|
+
audio, materials are Resources; so are your **custom** data classes.
|
|
17
|
+
|
|
18
|
+
## Composition over inheritance / deep trees
|
|
19
|
+
|
|
20
|
+
Prefer assembling small scenes over one giant tree or a deep script inheritance chain.
|
|
21
|
+
|
|
22
|
+
- A `Player` scene = `CharacterBody2D` root + `Sprite2D` + `CollisionShape2D` + `Camera2D` +
|
|
23
|
+
instanced `HealthComponent` + `HurtboxComponent` sub-scenes.
|
|
24
|
+
- Behaviors as **components**: a `HealthComponent` (its own scene/script exposing `signal died`) can
|
|
25
|
+
be dropped onto a player, an enemy, or a crate — no shared base class needed.
|
|
26
|
+
- Reach for `extends MyBase` only for genuine "is-a" specialization; reach for composed child nodes
|
|
27
|
+
for "has-a" behavior. The latter is the Godot-idiomatic default.
|
|
28
|
+
|
|
29
|
+
## `class_name` — when to register a global type
|
|
30
|
+
|
|
31
|
+
```gdscript
|
|
32
|
+
class_name HealthComponent extends Node
|
|
33
|
+
signal died
|
|
34
|
+
@export var max_health := 100
|
|
35
|
+
var _current := max_health
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- Register (`class_name`) reusable scripts and **all** custom Resources — it enables `is`/`as`
|
|
39
|
+
checks, `HealthComponent.new()`, and a proper inspector type/slot.
|
|
40
|
+
- Skip it for one-off scene-root scripts that nothing else references by type.
|
|
41
|
+
- Names are global and must be unique across the project.
|
|
42
|
+
|
|
43
|
+
## Custom Resources — typed game data
|
|
44
|
+
|
|
45
|
+
Use a custom `Resource` instead of loose `Dictionary`/JSON whenever you have structured game data:
|
|
46
|
+
stats, items, dialogue lines, wave definitions, ability configs. You get inspector editing, type
|
|
47
|
+
safety, `@export` slots, and save/load for free.
|
|
48
|
+
|
|
49
|
+
```gdscript
|
|
50
|
+
# item.gd
|
|
51
|
+
class_name Item extends Resource
|
|
52
|
+
@export var display_name: String = ""
|
|
53
|
+
@export var icon: Texture2D
|
|
54
|
+
@export var max_stack: int = 99
|
|
55
|
+
@export var value: int = 0
|
|
56
|
+
```
|
|
57
|
+
```gdscript
|
|
58
|
+
# Use it as a typed @export slot on any node:
|
|
59
|
+
@export var starting_items: Array[Item] = []
|
|
60
|
+
|
|
61
|
+
# Create/save/load in code:
|
|
62
|
+
var potion := Item.new()
|
|
63
|
+
potion.display_name = "Potion"
|
|
64
|
+
ResourceSaver.save(potion, "res://data/potion.tres")
|
|
65
|
+
var loaded := load("res://data/potion.tres") as Item
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Gotcha — shared vs unique**: a Resource assigned in the inspector is **shared by reference** across
|
|
69
|
+
all instances by default. If each node must own its copy (e.g. runtime-mutated stats), enable
|
|
70
|
+
**Local to Scene** on the resource, or `duplicate()` it in `_ready()`. Otherwise mutating one
|
|
71
|
+
enemy's `stats.health` mutates every enemy sharing that `.tres`.
|
|
72
|
+
|
|
73
|
+
## `preload` vs `load`
|
|
74
|
+
|
|
75
|
+
| | `preload("res://x")` | `load("res://x")` |
|
|
76
|
+
| --- | --- | --- |
|
|
77
|
+
| Resolved | parse/compile time | runtime, when the line runs |
|
|
78
|
+
| Path | must be a constant literal | can be a variable/computed |
|
|
79
|
+
| Use for | assets you always need; breaks fast if the path is wrong | dynamic/optional assets, avoiding circular preloads |
|
|
80
|
+
| Returns | the Resource/`PackedScene` | same |
|
|
81
|
+
|
|
82
|
+
```gdscript
|
|
83
|
+
const BULLET := preload("res://bullet.tscn") # PackedScene, baked in
|
|
84
|
+
func shoot() -> void:
|
|
85
|
+
var b := BULLET.instantiate() # -> a Node
|
|
86
|
+
add_child(b)
|
|
87
|
+
|
|
88
|
+
# runtime path
|
|
89
|
+
var scene := load("res://enemies/%s.tscn" % enemy_id) as PackedScene
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Instancing & freeing lifecycle
|
|
93
|
+
|
|
94
|
+
- `PackedScene.instantiate()` → a detached node; add it with `add_child(node)` (or
|
|
95
|
+
`add_sibling`) to bring it into the tree and trigger `_ready`.
|
|
96
|
+
- Free with `queue_free()` (deferred to end of frame — safe during signals/physics). Avoid `free()`
|
|
97
|
+
unless you know the node is out of the tree and not mid-callback.
|
|
98
|
+
- After freeing, guard any retained reference with `is_instance_valid(node)` before touching it.
|
|
99
|
+
- `add_child(node, true)` forces a readable unique name; useful when spawning many of one scene.
|
|
100
|
+
|
|
101
|
+
## `.tscn` / `.tres` are strict text — validation rule
|
|
102
|
+
|
|
103
|
+
Both formats are line-oriented with a header (`[gd_scene load_steps=N format=3 ...]`),
|
|
104
|
+
`[ext_resource ...]` / `[sub_resource ...]` blocks, and `[node ...]` / property lines. A wrong
|
|
105
|
+
`load_steps` count, a dangling `ExtResource("x")` id, or a malformed `[node]` path makes Godot
|
|
106
|
+
refuse to open the file. **Do not machine-generate or hand-edit these beyond trivial value tweaks,
|
|
107
|
+
and always open the project in the editor once before relying on a changed scene** — the editor is
|
|
108
|
+
the only reliable validator. Build data-Resources in code + `ResourceSaver.save()` instead of
|
|
109
|
+
writing `.tres` by hand.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: level-design
|
|
3
|
+
description: "Use when designing a game level, map, arena, or encounter — laying out a blockout/greybox, fixing pacing that drags or spikes, guiding players without hand-holding, placing secrets and rewards, or diagnosing a level that plays as boring, confusing, or where players get lost or stuck. Engine-agnostic craft: the blockout->greybox->playtest->art pipeline, sightlines and landmarks, tension/release rhythm, the introduce->develop->twist->combine encounter progression, readability via light/color/composition, and traversal heatmaps. Triggers: 'design this level', 'my level is boring/confusing', 'players keep getting lost', 'how do I pace this', 'blockout', 'greybox', 'encounter design', 'where do I put the secret'. NOT systemic mechanics or economy tuning (that's game-design), NOT narrative or story beats (that's game-storytelling), and NOT engine-specific level tooling — nav meshes, occlusion, tilemap editors, lightmap baking (that's the godot/unity/unreal skill)."
|
|
4
|
+
tags: [level-design, blockout, pacing, encounter, playtest]
|
|
5
|
+
recommends: [game-design, godot, unity, unreal]
|
|
6
|
+
profiles: [full]
|
|
7
|
+
origin: risco
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Level design — space that plays before it's pretty
|
|
11
|
+
|
|
12
|
+
*Shape the space, the rhythm, and the encounters so the player always knows where to go, never gets bored, and feels smart for exploring. Prove it in grey boxes; add art last.*
|
|
13
|
+
|
|
14
|
+
This skill owns the **craft of physical space and its play**: how a level is laid out, how it reads, how its intensity rises and falls, how encounters teach, and where reward hides. It is **engine-agnostic** — every technique here is validated in a blockout, then handed to an engine skill for the actual tooling (nav meshes, occlusion, lightmaps, tilemaps). It does not design the mechanics the level exercises (that's the systemic layer) or write the story it tells.
|
|
15
|
+
|
|
16
|
+
## When to use / When NOT to use
|
|
17
|
+
|
|
18
|
+
**Use when:** blocking out a new level/map/arena; a level playtests as boring, confusing, or too hard/easy; players get lost or stuck; you need to pace a level, space encounters, or add breathers; you're guiding players without arrows/waypoints; placing secrets, rewards, or shortcuts; teaching a mechanic through the space itself.
|
|
19
|
+
|
|
20
|
+
**Do NOT use for:**
|
|
21
|
+
|
|
22
|
+
- **Systemic / mechanic design** — the verbs, rules, economy, and numbers the player brings *into* the level (jump height as a rule, weapon balance, loot tables, progression systems). That's `game-design`. This skill *exercises* those verbs in space; it doesn't invent or tune them.
|
|
23
|
+
- **Narrative, quests, dialogue, environmental story beats** — what the space *says*. That's `game-storytelling`. Level design places the beat; storytelling writes it.
|
|
24
|
+
- **Engine-specific level tooling** — nav-mesh baking, occlusion culling, terrain/tilemap/ProBuilder editors, lightmap/GI bakes, streaming volumes, collision setup. Defer to `godot` / `unity` / `unreal`. This skill decides *what* the space must do; the engine skill implements *how*.
|
|
25
|
+
|
|
26
|
+
## The craft laws (non-negotiable)
|
|
27
|
+
|
|
28
|
+
1. **Art last.** Never model or texture a space you haven't proven fun in blockout. Art-first means reworking expensive assets when the layout inevitably changes.
|
|
29
|
+
2. **Metrics before geometry.** Lock the player's dimensions (height, jump, reach, cover) first; build everything as multiples of them. → `references/blockout-to-art.md`
|
|
30
|
+
3. **The player always knows where to go, never how it ends.** Guide the critical path; leave exploration to earn its own reward. Lostness is a readability bug, not player error.
|
|
31
|
+
4. **Watch, don't tell.** A level is only as good as it plays for someone you didn't coach. Every claim ("this is obvious", "the fight is fun") is a hypothesis until a fresh player validates it.
|
|
32
|
+
5. **Rhythm, not a flat line.** Intensity must rise and fall. Sustained peak = fatigue; sustained calm = boredom. Design the graph, not just the rooms.
|
|
33
|
+
6. **Teach through space, not text.** Introduce a threat where it's safe to learn it, before it can kill.
|
|
34
|
+
|
|
35
|
+
## The pipeline: blockout -> greybox -> playtest -> art (never art-first)
|
|
36
|
+
|
|
37
|
+
| Stage | You build | You answer | Gate to next |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| **Blockout** | Primitive geometry only (boxes/planes at true metrics). No materials, no props. | Does the space flow? Are timings/distances right? Can I find my way? | Layout & traversal feel right. |
|
|
40
|
+
| **Greybox** | Blockout + temp neutral materials, placeholder lighting, stand-in enemies/interactables. | Does it *read*? Does the fight/encounter work? Is pacing right? | Encounters and readability hold up in playtest. |
|
|
41
|
+
| **Playtest** | Nothing — you watch fresh players. | Where do they hesitate, get lost, die, skip? | Fixes are cheap because it's still grey. |
|
|
42
|
+
| **Art pass** | Meshes, materials, VFX, final lighting, audio — *serving* the proven layout. | Does art reinforce the guidance the greybox already established? | Ship. |
|
|
43
|
+
|
|
44
|
+
Iterate blockout↔playtest many times before art. Changing a grey box is minutes; changing a finished art set-piece is days. Full stage detail, the metrics block, and the art-pass handoff checklist → `references/blockout-to-art.md`.
|
|
45
|
+
|
|
46
|
+
## Spatial composition — leading the eye
|
|
47
|
+
|
|
48
|
+
- **Sightlines are your steering wheel.** Control what's visible from each point: frame the objective in a doorway/arch, block distracting views, open a reveal at the moment of a decision.
|
|
49
|
+
- **Landmarks (the "weenie").** One large, distinct, distant silhouette per area that the player orients toward (tower, spire, mountain) — Disney's term for the visual magnet that pulls you forward. Self-locating beats a minimap.
|
|
50
|
+
- **Lead the eye** with converging lines (rails, beams), framing (foreground arches), contrast (a bright end to a dark corridor), and motion. The eye goes to the brightest, highest-contrast, most-different thing — put that on the path you want taken.
|
|
51
|
+
- **Silhouette readability.** Enemies, interactables, and hazards must read by shape alone at a glance. Two things sharing a silhouette is an unintended trap.
|
|
52
|
+
- **Golden path vs exploration.** The **critical path** is the always-legible guaranteed route to the objective; **exploration** is optional branches and loops off it. Prefer loops that return the player forward (avoid dead-end backtracking); breadcrumb side routes with a visible reward.
|
|
53
|
+
|
|
54
|
+
Framing, contrast, and layout topologies (loops, hubs, gates) → `references/guidance-and-readability.md`.
|
|
55
|
+
|
|
56
|
+
## Pacing & rhythm — tension and release
|
|
57
|
+
|
|
58
|
+
Design the **intensity graph** for the whole level, not room by room. A good level is a sawtooth trending upward: each peak (combat, platforming gauntlet, chase) followed by a valley — a **safe room / breather** with no threat, low stimulus, often a save/restock.
|
|
59
|
+
|
|
60
|
+
- **Space encounters in time and distance, and *vary* both** — three identical-length fights back to back feel like one long slog.
|
|
61
|
+
- **Breathers earn the next peak.** The calm between (Halo's cadence is often described as "thirty seconds of fun" on repeat) is what makes the fun read as fun. Use it to let adrenaline drop, reward observation, and telegraph the next escalation.
|
|
62
|
+
- **Difficulty within the level** trends up but resets at checkpoints — re-enter competent, then ramp. End on the hardest, most spectacular test, not a whimper.
|
|
63
|
+
- **Contrast the modes** (fast↔slow, loud↔quiet, open↔tight, combat↔traversal↔puzzle). Monotony of *any* mode reads as boredom even if each moment is individually good.
|
|
64
|
+
|
|
65
|
+
Intensity-graph template, breather patterns, and difficulty-curve worked example → `references/pacing-and-encounters.md`.
|
|
66
|
+
|
|
67
|
+
## Encounter design — introduce, develop, twist, combine
|
|
68
|
+
|
|
69
|
+
The four-beat progression teaches a mechanic/threat through space (the pattern behind Portal, Half-Life 2, and Mario level craft):
|
|
70
|
+
|
|
71
|
+
1. **Introduce** — meet the new element somewhere it *can't* hurt you. A single turret across a safe gap; one platform of the new hazard. Learn it risk-free.
|
|
72
|
+
2. **Develop** — use it for real, low stakes. The turret now guards a corridor you must cross.
|
|
73
|
+
3. **Twist** — recontextualize it. Two turrets flanking; the turret is *behind* glass; you must use the turret against enemies. The rule bends.
|
|
74
|
+
4. **Combine** — the mastery test: the new element plus everything learned earlier, at full intensity.
|
|
75
|
+
|
|
76
|
+
Design rules for the **arena** (the shape a fight happens in):
|
|
77
|
+
|
|
78
|
+
- **Match sightline length to weapon/threat range** — long lanes favor ranged, tight rooms favor melee/shotgun. The geometry *is* the difficulty knob.
|
|
79
|
+
- **Cover rhythm, flanks, verticality.** Space cover so the player advances in beats; give at least one flank so a fight isn't a static peek-war; use height for pressure and reward (the Doom-2016 arena: multi-tier, resources placed to pull the player *through* the space, not hunker).
|
|
80
|
+
- **Chokepoints need release valves** — pair a choke with an exit or a way to break the standoff, or the fight stalls.
|
|
81
|
+
- **An encounter = enemy composition × space × objective × resources.** Change any one for a "new" encounter without new assets.
|
|
82
|
+
|
|
83
|
+
Encounter template, arena patterns, and a full introduce→develop→twist→combine example → `references/pacing-and-encounters.md`.
|
|
84
|
+
|
|
85
|
+
## Guidance & readability — direct without hand-holding
|
|
86
|
+
|
|
87
|
+
Guide with the environment so you never need a floating arrow:
|
|
88
|
+
|
|
89
|
+
- **Light leads.** Players move toward light (the moth effect) and higher value-contrast. Light the path, objective, and exit; keep dead-ends dim. Placeholder lighting is a greybox *design* tool, not a final-art afterthought.
|
|
90
|
+
- **Reserve one accent color for "you can interact / go here"** (Mirror's Edge runner red, Dishonored's usable glint) and **never** use it decoratively. Consistency is the whole point; break it once and you've lied to the player.
|
|
91
|
+
- **Composition** — framing, leading lines, and negative space point the eye like a photographer's shot. The critical path should be the most-framed, highest-contrast route from any decision point.
|
|
92
|
+
- **Affordances & signifiers** (Norman/Gibson): a ledge that looks grabbable must be grabbable. Establish the visual language early and hold it — if a trim means "climb here", it appears *only* where you can climb. Inconsistent affordances are the #1 cause of "I didn't know I could do that."
|
|
93
|
+
- **Deny wrong turns visually** (subtle dead-ends, unlit gaps, a "not-a-path" material) rather than with invisible collision.
|
|
94
|
+
|
|
95
|
+
Full catalog — lighting/color/composition, the affordance language, the "critical-path readability" self-test → `references/guidance-and-readability.md`.
|
|
96
|
+
|
|
97
|
+
## Metrics & playtesting — where players get lost and stuck
|
|
98
|
+
|
|
99
|
+
You cannot see your own level fresh. **Observe, don't coach:**
|
|
100
|
+
|
|
101
|
+
- **Watch a first-timer play, silent.** Note every hesitation (readability gap), wrong turn (navigation gap), repeated death (telegraph/difficulty gap), and skipped content (reward-placement gap). Do **not** answer "where do I go?" — that question *is* the finding.
|
|
102
|
+
- **Traversal telemetry** turns anecdote into pattern: **position heatmaps** (cold zones players never enter = wasted level), **death heatmaps** (clusters = unfair/unreadable threats), **path traces** (golden path or lost?), **dwell time** at forks (long dwell = unclear guidance), and **look/aim heatmaps** (did they even see the landmark?).
|
|
103
|
+
- **Read the map:** a cold region means the space doesn't pull the player there — re-light, add a landmark/reward, or cut it. A death cluster means fix the telegraph or arena, not the player. A branch nobody takes needs a visible reward at its mouth.
|
|
104
|
+
|
|
105
|
+
Playtest protocol, question scripts, and turning heatmaps into fixes → `references/guidance-and-readability.md`.
|
|
106
|
+
|
|
107
|
+
## Reward & secret placement, risk vs reward
|
|
108
|
+
|
|
109
|
+
- **Reward the wandering eye.** Put secrets where a *curious* player naturally looks — behind the waterfall, up the ledge you already lit and framed, down the branch you breadcrumbed. Reward observation, not pixel-hunting; a secret nobody can reasonably find just frustrates.
|
|
110
|
+
- **Breadcrumb the secret** with a glint, an out-of-place object, a suspicious seam, an "impossible" bit of geometry visible from the path. The player should feel *clever* — which means the game showed them the thread first.
|
|
111
|
+
- **Risk vs reward.** Gate the best optional reward behind the biggest optional risk or detour. Keep the *critical path* rewarding on its own so risk-averse players aren't punished; put the *extra* behind the extra effort, both visible before committing.
|
|
112
|
+
- **Never hide mandatory progression like a secret** — a required key/door disguised as optional is a stuck-player generator. Secrets are optional by definition.
|
|
113
|
+
- **Vary reward types**: power/upgrade, resources, lore/collectible, a **shortcut unlock** (best long-term — reshapes the level), and pure spectacle.
|
|
114
|
+
|
|
115
|
+
Reward taxonomy, secret-signposting, and risk/reward tuning → `references/guidance-and-readability.md`.
|
|
116
|
+
|
|
117
|
+
## Anti-patterns / rationalizations -> STOP
|
|
118
|
+
|
|
119
|
+
| Rationalization | Reality / Fix |
|
|
120
|
+
|---|---|
|
|
121
|
+
| "Let's make it look good first, then tune the layout." | Art-first burns days reworking assets when the layout changes. Blockout, playtest, *then* art. |
|
|
122
|
+
| "It's obvious where to go." | Obvious to *you*, the author. Fresh players get lost. Watch one before you believe it. |
|
|
123
|
+
| "I'll just add a waypoint arrow / big text prompt." | That's a patch over a readability failure. Fix it with light, framing, and a landmark first. |
|
|
124
|
+
| "The whole level is intense — it's exciting!" | Sustained peak = fatigue and it all flattens into noise. Add breathers so the peaks read. |
|
|
125
|
+
| "Backtracking reuses the space for free." | Repeated dead-end backtracking reads as padding. Use loops that return the player forward. |
|
|
126
|
+
| "Players will figure the new mechanic out." | Not while it's killing them. Introduce it where it's safe, then develop/twist/combine. |
|
|
127
|
+
| "Yellow paint everywhere so they see the ledges." | Only if yellow means *exactly* climbable and appears nowhere else. Inconsistent affordances lie. |
|
|
128
|
+
| "The secret's hidden well — no one will find it." | A secret with no breadcrumb is just frustration. Show the thread; reward the curious eye. |
|
|
129
|
+
| "The gap/cover/door width feels about right." | Guessing metrics breaks traversal later. Set the metrics block first; build to multiples. |
|
|
130
|
+
|
|
131
|
+
## Quick reference
|
|
132
|
+
|
|
133
|
+
| Lever | Default | Where |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| Build order | blockout → greybox → playtest → art (art last) | `references/blockout-to-art.md` |
|
|
136
|
+
| Metrics block | lock player height/jump/reach/cover first; build to multiples | `references/blockout-to-art.md` |
|
|
137
|
+
| Navigation | one dominant landmark per area; legible critical path + breadcrumbed loops | `references/guidance-and-readability.md` |
|
|
138
|
+
| Pacing | sawtooth: peak → breather (safe room) → higher peak | `references/pacing-and-encounters.md` |
|
|
139
|
+
| Encounter teaching | introduce → develop → twist → combine | `references/pacing-and-encounters.md` |
|
|
140
|
+
| Arena shape | sightline length matched to threat range; flanks + verticality | `references/pacing-and-encounters.md` |
|
|
141
|
+
| Guidance | light leads; one reserved interact color; consistent affordances | `references/guidance-and-readability.md` |
|
|
142
|
+
| Playtest | watch silent; log hesitation/lost/death/skip; read heatmaps | `references/guidance-and-readability.md` |
|
|
143
|
+
| Secrets / risk | breadcrumb them; best optional reward behind biggest optional risk | `references/guidance-and-readability.md` |
|
|
144
|
+
|
|
145
|
+
## Related skills
|
|
146
|
+
|
|
147
|
+
- **`game-design`** — the systemic layer: mechanics, verbs, economy, progression, difficulty *systems*. Level design exercises those verbs in space; if the request is about the rules themselves, route there.
|
|
148
|
+
- **`game-storytelling`** — narrative, quests, dialogue, and the meaning of environmental beats. Level design places the beat; storytelling authors it.
|
|
149
|
+
- **`godot` / `unity` / `unreal`** — the engine tooling that *implements* a proven layout: nav meshes, occlusion, terrain/tilemap editors, lightmap/GI bakes, streaming, collision. Hand off here after the blockout plays well.
|
|
150
|
+
- **`harness`** — the `02-DOCS` Karpathy-wiki this skill records level conventions into.
|
|
151
|
+
- **References:** `references/blockout-to-art.md` (pipeline, metrics block, art handoff, playtesting the blockout); `references/pacing-and-encounters.md` (intensity graph, encounter template, arena patterns, difficulty curve); `references/guidance-and-readability.md` (light/color/composition, affordances, playtest protocol, telemetry, reward & secrets).
|
|
152
|
+
|
|
153
|
+
## Checklist
|
|
154
|
+
|
|
155
|
+
- [ ] Player metrics block locked (height, jump, reach, cover, door/corridor widths) before geometry.
|
|
156
|
+
- [ ] Layout proven in blockout/greybox — never modeled or textured before it plays well.
|
|
157
|
+
- [ ] Critical path is legible from every decision point; a fresh player can find the objective without being told.
|
|
158
|
+
- [ ] One dominant landmark orients each area; light and framing lead the eye along the intended path.
|
|
159
|
+
- [ ] Intensity graph is a sawtooth (peaks + breathers), not a flat line; encounters vary in length and spacing.
|
|
160
|
+
- [ ] At least one safe room / breather follows each major peak.
|
|
161
|
+
- [ ] Every new mechanic/threat is introduced safely, then developed, twisted, and combined.
|
|
162
|
+
- [ ] Arenas match sightline length to threat range and offer a flank/verticality option, not a static peek-war.
|
|
163
|
+
- [ ] One reserved accent color for interactables/path, used consistently and never decoratively; affordances hold everywhere.
|
|
164
|
+
- [ ] Played by a fresh, uncoached tester (and/or telemetry): hesitations/lost/deaths/skips logged, no dead cold-zones, no unfair death clusters, no ignored branches.
|
|
165
|
+
- [ ] Secrets are breadcrumbed and reward observation; no mandatory progression hidden as a secret; best optional reward sits behind the biggest optional risk.
|
|
166
|
+
|
|
167
|
+
## Project grounding (02-DOCS + CLAUDE.md)
|
|
168
|
+
|
|
169
|
+
When this skill runs in a project with a `02-DOCS/` layer (the [`harness`](../harness/SKILL.md) Karpathy wiki), record the level-design conventions there and index them from the root `CLAUDE.md`, so the next agent inherits them instead of re-deriving:
|
|
170
|
+
|
|
171
|
+
1. **Find the article** `02-DOCS/wiki/stack/level-design.md`, indexed in `02-DOCS/wiki/index.md` (the Knowledge map).
|
|
172
|
+
2. **If missing or stale**, create/update it with the project's real conventions — the metrics block, the reserved interact-color, the pacing/breather cadence, the encounter-teaching pattern, and any per-level intensity graphs — then index it.
|
|
173
|
+
3. **Read it first on every use** and stay consistent; when a convention changes, update the article (bump its `Updated` date) in the same change.
|
|
174
|
+
|
|
175
|
+
No `02-DOCS/` layer? Skip silently (optionally suggest `harness`). Conventions are *recorded, not gated* — never block the design work on this.
|