# Godot

> 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`).

- Skill: `ericrisco/godot` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds@latest add ericrisco/godot`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ericrisco/godot/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: ericrisco (https://skillmd.com/u/ericrisco)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ericrisco/godot

---


# Godot 4.x (GDScript + C#)

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.

## Version contract — read first

**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).
- Angles are radians; `_process(delta)` delta is a `float` (GDScript) / `double` (C#) in **seconds**.

Full table with every rename → `references/godot3-to-4-traps.md`.

## Project & scene organization

- The **scene** (`.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.
- One responsibility per script. Attach behavior to the scene's **root**; child nodes are parts.
- `class_name Foo` registers a global type usable in the inspector and as `Foo.new()`. Use it for
  reusable scripts and custom Resources; skip it for one-off scene scripts.
- **Files**: `snake_case.gd` / `snake_case.tscn` for scenes and scripts; `PascalCase` for node
  names in the tree and for `class_name`. Group by feature (`player/`, `enemy/`, `ui/`), not by type.

## Nodes vs scenes vs scripts vs custom Resources

| 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 `Resource`s 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`.

```gdscript
class_name EnemyStats extends Resource
@export var max_health: int = 30
@export var speed: float = 120.0
@export var loot_table: Array[ItemDrop] = []
```

## Autoloads / singletons + EventBus

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.

```gdscript
# event_bus.gd  (autoload named "Events")
extends Node
signal enemy_died(position: Vector2, xp: int)
signal score_changed(new_score: int)
```
```gdscript
# 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.

## Node access & lifecycle

- `_init()` runs at construction (no tree, no `@onready` yet). `_ready()` runs once the node and
  all children are in the tree — do node wiring here.
- `@onready var x = $Path` defers the assignment to `_ready`, so the child exists. Never grab
  children in `_init`.
- **Prefer unique names**: mark a node **Unique Name in Owner** (`%`) and access `%HealthBar`
  instead 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.
- Cache node lookups in `@onready` vars; don't call `get_node` every frame.
- **Freeing**: call `queue_free()` (safe, end of frame), not `free()` mid-signal. Guard reused refs
  with `is_instance_valid(node)`.
- Never busy-wait; `await get_tree().create_timer(1.0).timeout` or `await` a signal.

```gdscript
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

Signals are Godot's decoupling primitive. In Godot 4 you connect a **Callable**, not strings.

- **Declare** with typed params; **name in the past tense** for facts that happened
  (`health_depleted`, `item_collected`), present-tense imperative only for requests.
- **Connect**: `node.signal_name.connect(_on_thing)` — a direct method reference, checked at parse
  time. Add `CONNECT_ONE_SHOT` for auto-disconnect after one fire.
- **Disconnect discipline**: a connection to a node that gets freed is cleaned up automatically, but
  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.

```gdscript
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 / @tool (inspector config)

`@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.

```gdscript
@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`.

## Static (typed) GDScript

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.

```gdscript
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, and
  non-physics polish.
- **Always scale rate-based change by `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.

## Resources & data — `.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; use
  for assets you always need.
- **`load("res://x.tscn")`** resolves at **runtime** — use for dynamic/optional paths (and to avoid
  circular preloads). Both return a `PackedScene`; call `.instantiate()` to get a node.

## 2D / 3D bodies quickstart

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.**

```gdscript
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`.

## Language parity — GDScript ↔ C#

Same engine, same nodes; C# uses PascalCase members, `partial` classes, and attributes. Signals
become C# `event`s (generated by source-gen). C# support requires the **.NET (Mono) build** of Godot.

```gdscript
# 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")
```
```csharp
// 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`.

## Export & testing

- **Test with GUT 9.x** (GDScript, `extends GutTest`) or **gdUnit4** (GDScript + C#). Put tests
  under `test/` or `res://tests/`; assert behavior, not private state.
- **Run headless in CI**: `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`.
- Validate a project before shipping: open in the editor once (catches broken `.tscn`/`.tres`),
  then export-check per platform. Details → `references/export-and-testing.md`.

## Hand off to

Mechanics, loops, and feel *before* you script them → [`game-design`](../game-design/SKILL.md).
`.gdshader` / visual shaders → [`gamedev-shaders`](../gamedev-shaders/SKILL.md). Joints, RigidBody
tuning, deep collision layers → [`gamedev-physics`](../gamedev-physics/SKILL.md). NavigationAgent,
A*, steering → [`gamedev-pathing`](../gamedev-pathing/SKILL.md). `MultiplayerSynchronizer`/RPC/netcode
→ [`gamedev-multiplayer`](../gamedev-multiplayer/SKILL.md). Store builds, signing, platform export at
scale → [`gamedev-shipping`](../gamedev-shipping/SKILL.md). The GDExtension / `godot-cpp` native side
and its CMake/build tooling → [`cpp`](../cpp/SKILL.md).

