Use when writing, reviewing, or debugging Godot 4.x games in GDScript or C# — scenes, nodes, custom Resources, autoload/EventBus signals, typed GDScript, CharacterBody2D/3D movement, GUT/gdUnit4 — or porting Godot 3 APIs to 4. NOT Unity/Unreal (that is `unity`/`unreal`), NOT `.gdshader` code (`gamedev-shaders`), NOT GDExtension C++ tooling (`cpp`).
npx skills add https://github.com/ericrisco/rsc-harness --skill godot
Build 2D and 3D the way the engine is designed: scenes as reusable units, composition over deep node
trees, signals for decoupling, static typing for speed and safety. GDScript (typed) is primary; C#
parity snippets sit beside it.
This is Godot 4.x. Never emit Godot 3 APIs. Godot 4 renamed core nodes, moved annotations
behind @, replaced yield with await, and switched signal/file/tween APIs. A Godot 3 snippet
will not even parse in 4.x. Before writing or accepting any line, check it against this ban-list:
| Never (Godot 3) | Always (Godot 4.x) |
| --- | --- |
| yield(timer, "timeout") | await timer.timeout |
| onready var x = ... | @onready var x = ... |
| export var hp = 3 | @export var hp := 3 |
| tool (script mode line) | @tool (annotation, first line) |
| KinematicBody / KinematicBody2D | CharacterBody3D / CharacterBody2D |
| Spatial | Node3D |
| Area / RigidBody / StaticBody | Area3D / RigidBody3D / StaticBody3D |
| Sprite | Sprite2D |
| scene.instance() | scene.instantiate() |
| move_and_slide(velocity, UP) (positional) | set velocity property, then move_and_slide() — no args |
| connect("hit", self, "_on_hit") | node.hit.connect(_on_hit) (Callable) |
| File.new() / Directory.new() | FileAccess.open(...) / DirAccess.open(...) |
| standalone Tween node + interpolate_property | create_tween() → tween.tween_property(...) |
| OS.get_ticks_msec for gameplay timing | Time.get_ticks_msec() (OS timing moved to Time) |
| PoolByteArray / PoolVector2Array | PackedByteArray / PackedVector2Array |
Lifecycle overrides must chain the parent with super() (Godot 3 called it implicitly; Godot 4
does not). If you override _ready, _process, _init, etc. in a script that extends another
script defining them, call super() / super._ready() or the base logic silently never runs.
Silent-breakers (compile fine, behave wrong — the dangerous class):
Array.slice(begin, end) — end is now exclusive (was inclusive in Godot 3). [1,2,3,4].slice(1,3) → [2,3].Camera2D.zoom is inverted vs Godot 3: a larger zoom now means zoomed in (magnified). Vector2(2,2) = 2× magnification, not half.TileMap is deprecated → use one TileMapLayer node per layer (since 4.3)._process(delta) delta is a float (GDScript) / double (C#) in seconds.Full table with every rename → references/godot3-to-4-traps.md.
.tscn) is the reusable unit — a self-contained tree you instance many times(a Player, a Bullet, a HUD). Prefer composition: small scenes/nodes assembled, not one
60-node monolith. If a subtree has its own behavior, make it its own scene.
class_name Foo registers a global type usable in the inspector and as Foo.new(). Use it forreusable scripts and custom Resources; skip it for one-off scene scripts.
snake_case.gd / snake_case.tscn for scenes and scripts; PascalCase for nodenames in the tree and for class_name. Group by feature (player/, enemy/, ui/), not by type.
| You need | Use |
| --- | --- |
| A thing in the tree that renders / moves / collides / processes | a Node (typed subclass) |
| A reusable, instanceable bundle of nodes | a scene (.tscn) |
| Behavior attached to a node | a script (.gd / .cs) |
| Pure data (stats, items, dialogue, level config) with no place in the tree | a custom Resource (.tres) |
Custom Resources are Godot's typed, savable, inspector-editable data objects — reach for them
instead of loose Dictionaries or JSON for game data. See references/nodes-scenes-resources.md.
class_name EnemyStats extends Resource
@export var max_health: int = 30
@export var speed: float = 120.0
@export var loot_table: Array[ItemDrop] = []
Register a script or scene as an autoload (Project → Project Settings → Globals/Autoload) to get
one always-present instance reachable by name from anywhere. Use it for cross-cutting state (save
game, audio, run config) — not as a dumping ground.
The EventBus pattern decouples unrelated systems: an autoload that owns only signals. Emitters
and listeners never reference each other, just the bus.
# event_bus.gd (autoload named "Events")
extends Node
signal enemy_died(position: Vector2, xp: int)
signal score_changed(new_score: int)
# emitter # listener (anywhere)
Events.enemy_died.emit(global_position, 10)
Events.enemy_died.connect(_on_enemy_died)
Keep gameplay logic in nodes; let the bus carry the *notification*, not the behavior.
_init() runs at construction (no tree, no @onready yet). _ready() runs once the node andall children are in the tree — do node wiring here.
@onready var x = $Path defers the assignment to _ready, so the child exists. Never grabchildren in _init.
%) and access %HealthBarinstead of the fragile, refactor-breaking get_node("../../UI/HealthBar"). $Foo is fine for a
direct child — get_node/$ on a missing path returns null and errors.
@onready vars; don't call get_node every frame.queue_free() (safe, end of frame), not free() mid-signal. Guard reused refswith is_instance_valid(node).
await get_tree().create_timer(1.0).timeout or await a signal.extends CharacterBody2D
@onready var sprite: Sprite2D = $Sprite2D
@onready var health_bar: ProgressBar = %HealthBar # unique name, position-independent
func _ready() -> void:
super() # chain the parent's _ready if the base defines one
health_bar.value = 100
Signals are Godot's decoupling primitive. In Godot 4 you connect a Callable, not strings.
(health_depleted, item_collected), present-tense imperative only for requests.
node.signal_name.connect(_on_thing) — a direct method reference, checked at parsetime. Add CONNECT_ONE_SHOT for auto-disconnect after one fire.
connections you make to *long-lived* objects (autoloads, the bus) from a short-lived node must be
disconnected in _exit_tree(), or use CONNECT_ONE_SHOT, to avoid calls into freed instances.
signal health_depleted
signal health_changed(current: int, max: int)
func take_damage(amount: int) -> void:
_health -= amount
health_changed.emit(_health, _max_health)
if _health <= 0:
health_depleted.emit()
@export exposes a variable in the Inspector so designers tune it without touching code. Use ranges,
groups, and typed exports so the inspector gives real widgets and validation.
@export var title: String = "Level 1"
@export_range(0.0, 1.0, 0.05) var volume := 0.8
@export_group("Movement")
@export var speed: float = 300.0
@export var jump_velocity: float = -400.0
@export var projectile: PackedScene # drag a .tscn in the inspector
@export var stats: EnemyStats # a custom Resource slot
@tool at the top of a script runs it in the editor too — for gizmos, procedural previews, or
validating exported data. Guard runtime-only code with if Engine.is_editor_hint(): return.
Type everything. Typed GDScript is faster (the VM skips dynamic dispatch) and catches errors at parse
time. Use := when the type is inferable, : Type when it isn't, and avoid Variant/untyped.
var speed: float = 300.0 # explicit
var dir := Vector2.ZERO # inferred
var enemies: Array[Enemy] = [] # typed array
func distance_to(target: Node2D) -> float:
return global_position.distance_to(target.global_position)
func _on_body_entered(body: Node) -> void:
var enemy := body as Enemy # safe cast → null if wrong type, no crash
if enemy:
enemy.take_damage(10)
Naming: snake_case vars/funcs/signals, PascalCase types/class_name/nodes, CONSTANT_CASE
consts, tabs for indent, lines < 100 cols. Cheat-sheet → references/gdscript-style.md.
_process vs _physics_process_physics_process(delta) — fixed tick (default 60 Hz), the same every step. All movement,move_and_slide(), forces, and collision-dependent logic go here.
_process(delta) — runs once per rendered frame (variable rate). Use for visuals, UI, andnon-physics polish.
delta so behavior is framerate-independent. move_and_slide()and move_and_collide() already fold in delta internally — do not multiply the velocity you
hand them by delta again.
.tres / .tscn are strict text formats.tscn and .tres are line-oriented text with a strict header/section grammar. **Do not hand-edit
them past trivial value tweaks, and never launch on a file you hand-authored without validating** —
one bad ext_resource id, [node] line, or load_steps count corrupts the whole scene and Godot
refuses to open it. Prefer editing through the editor or building Resources in code and ResourceSaver.save().
preload("res://x.tscn") resolves at parse/compile time — the dependency is baked in; usefor assets you always need.
load("res://x.tscn") resolves at runtime — use for dynamic/optional paths (and to avoidcircular preloads). Both return a PackedScene; call .instantiate() to get a node.
The move-anything-controllable body is CharacterBody2D / CharacterBody3D. The Godot 4 flow is:
write the velocity property, then call move_and_slide() with no arguments.
extends CharacterBody2D
@export var speed: float = 300.0
@export var jump_velocity: float = -400.0
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity += get_gravity() * delta # get_gravity(): project-configured vector
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_velocity
var dir := Input.get_axis("move_left", "move_right")
velocity.x = dir * speed
move_and_slide() # NO args in Godot 4 — reads the velocity property
3D is identical with CharacterBody3D, Vector3, and an X/Z input plane; body/area suffixes are
3D. Deeper body/physics tuning → gamedev-physics.
Same engine, same nodes; C# uses PascalCase members, partial classes, and attributes. Signals
become C# events (generated by source-gen). C# support requires the .NET (Mono) build of Godot.
# GDScript
extends Node
signal health_depleted
@export var speed: float = 300.0
func _ready() -> void:
health_depleted.connect(_on_depleted)
health_depleted.emit()
func _on_depleted() -> void:
print("dead")
// C# — same node, .NET build
using Godot;
public partial class Player : Node
{
[Signal] public delegate void HealthDepletedEventHandler();
[Export] public float Speed { get; set; } = 300.0f;
public override void _Ready()
{
base._Ready(); // chain the parent (== super())
HealthDepleted += OnDepleted; // connect via the generated event
EmitSignal(SignalName.HealthDepleted);
}
private void OnDepleted() => GD.Print("dead");
}
move_and_slide()→MoveAndSlide(), $Node→GetNode<T>("Node"), %Node→GetNode<T>("%Node"),
preload→GD.Load<T>(...). Full parity table → references/export-and-testing.md.
GDExtension / C++: for hot native code, build a godot-cpp GDExtension (.gdextension file,
GDREGISTER_CLASS, _bind_methods()) rather than a Godot module — no engine recompile, and it loads
like any other library. That is native-tooling territory → pair with cpp.
extends GutTest) or gdUnit4 (GDScript + C#). Put testsunder test/ or res://tests/; assert behavior, not private state.
godot --headless -s addons/gut/gut_cmdln.gd -gdir=res://test -gexit(GUT). Export via templates: godot --headless --export-release "Linux/X11" build/game.x86_64.
.tscn/.tres),then export-check per platform. Details → references/export-and-testing.md.
Mechanics, loops, and feel *before* you script them → game-design.
.gdshader / visual shaders → gamedev-shaders. Joints, RigidBody
tuning, deep collision layers → gamedev-physics. NavigationAgent,
A*, steering → gamedev-pathing. MultiplayerSynchronizer/RPC/netcode
→ gamedev-multiplayer. Store builds, signing, platform export at
scale → gamedev-shipping. The GDExtension / godot-cpp native side
and its CMake/build tooling → cpp.
Take ericrisco/godot from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.