L2.1 — Classes, extends, class_name, _init & Casting¶
What you'll learn
- Treat each script as a class, extend a base, and call the base with
super. - Register a script as a global type with
class_name, and why that unlocks clean typing and casts. - Distinguish
_init(object created in memory) from_ready(node entered the tree). - Test and narrow types with
isandas, including the null-guard the safe cast requires.
How it applies
- An unnamed class forces stringly-typed checks. Without
class_name, other scripts cannot refer to your type by name, so code falls back to comparing string names or duck-typing — fragile checks that pass the wrong object and fail far from the cause. A named class lets the compiler do the checking. _initruns before children exist. Code that reaches for a child node in_initfinds nothing there — the node tree is not assembled yet — and crashes on a null. Knowing which constructor-like hook runs when is the difference between a clean startup and a launch-time null error.- An unchecked cast crashes the same as no cast.
node as Enemyreturnsnullwhennodeis not anEnemy; using the result without checking fornulljust moves the crash one line down. The safe pattern is cast-then-check. - Skipping
superskips the base's setup. Overriding a method that the base relies on, without callingsuper, silently drops whatever the base did — a defect that looks like "the parent behavior randomly stopped working."
Concepts¶
Every script is a class; extends picks the base¶
You met this in L1.1: a .gd file is a class, and extends names its base. Now the consequences
matter. Members you declare (variables, functions) are members of your class; members you inherit
come from the base and its ancestors. When you override an inherited method, you can still reach the
base version with super:
extends Node
func _ready() -> void:
super() # run Node's _ready first (if relevant), then ours
print("my setup")
func describe() -> void:
super.describe() # call the base class's describe(), then extend it
print("...and more")
super() calls the same method on the base; super.name() calls a specific base method. Override
without super and the base's version simply does not run — fine when you mean to replace it, a bug
when the base needed to do setup.
class_name — a globally known type¶
By default a script is anonymous: other scripts can load it by path, but cannot name its type.
class_name registers the script as a global type:
(The extends and class_name lines may appear in either order.) Now any script can write
var h: Health, check thing is Health, and create Health.new() — no preload of the file
needed. The type also appears in the editor's Create Node / Create Resource dialog. class_name
is what turns a script from "a file at a path" into "a type the whole project knows," and the game
books use it for every component and data class (Health, StatBlock, ItemData).
Example
Naming a class is what makes type-safe checks possible elsewhere:
# in health.gd
class_name Health
extends Node
# in any other script — no preload required:
if node is Health:
node.take_damage(5)
Without class_name, that other script could not say is Health at all; it would have to load the
script by path and compare, or guess by checking for a method. The name is the seam that lets the
compiler verify the check.
_init versus _ready¶
Two hooks look like constructors; they fire at different moments.
_init()runs when the object is created in memory (Health.new(), or when the engine instantiates the node) — before it is part of the scene tree. Child nodes do not exist yet. Use it for pure in-memory setup that depends only on the object itself._ready()runs once after the node and all its children have entered the tree (L1.1). This is where child references and signal connections are safe.
func _init() -> void:
# safe: set up own data
current = max_health
func _ready() -> void:
# safe: children exist now
$Bar.value = current
Reaching for $Bar in _init would fail — the child is not there yet. The full lifecycle ordering is
G2.1; the rule to carry now is _init = in memory, _ready = in the tree. A custom Resource
or plain data class (no nodes) typically uses _init; a node that touches its children uses _ready.
Inner classes¶
A script can declare classes inside it for small local helpers:
Inner classes are scoped to the file. They are useful for tiny structured records; for anything
shared across files, a class_name'd script or a custom Resource (G1.6) is the better home.
is and as — testing and narrowing types¶
is tests whether a value is of a type and returns a bool. as casts: it returns the value typed
as that class, or null if it is not that type (a safe cast — it never crashes):
if node is Enemy:
# inside here, you know node is an Enemy
...
var enemy := node as Enemy
if enemy: # null if node was not an Enemy
enemy.alert()
as is safe precisely because the wrong type yields null instead of an error — but that means you
must check for null before using the result. Using enemy without the if enemy: guard
re-introduces the crash you were avoiding, just one line later. The game books use this exact pattern
(M3.1 reads an overlapping area as a hitbox, guards the null, then uses it).
Walkthrough¶
Create a new script for this chapter; you can attach it to a fresh Node scene and run with F6.
- Write a small
class_name'd class — e.g. a scriptclass_name Counter/extends Nodewith avar count := 0and afunc tick() -> void: count += 1. Save it. - In a second script (your run scene's node), declare
var c: Counter = Counter.new(), callc.tick()twice, and printc.count. You used the type by name with nopreload— that isclass_nameworking. - Add
_initand_readytoCounter, each printing which one ran. AttachCounterto a node in a scene, run, and observe the order (_initthen_ready). - Type-narrow: make
var thing: Node = c, thenif thing is Counter: print("yes"), thenvar maybe := thing as Counterand guardif maybe: maybe.tick(). Changethingto a plainNode.new()and confirm theasyieldsnulland the guarded block is skipped (no crash).
Optional sanity check
In Counter, try to access a child node inside _init (e.g. $SomeChild) on a scene where that
child exists. It fails — the child is not in the tree yet at _init time. Move the access to
_ready and it works. That contrast is the _init-versus-_ready rule, demonstrated.
Self-check quiz¶
Q1 — What does adding class_name Health to a script enable that an anonymous script cannot do?
A. It makes the script run automatically at startup.
B. It registers a global type, so other scripts can write var h: Health, thing is Health, and Health.new() without preloading the file.
C. It improves performance of the script.
D. It makes the script a singleton.
Reveal answer
B. class_name registers the script as a globally known type usable by name across the
project (and in the editor's create dialog). A describes an autoload (G1.5), a different
mechanism. C is unrelated — naming a class is about visibility, not speed. D confuses
class_name with the singleton/autoload pattern.
Q2 — Why does accessing $Bar in _init fail, while it works in _ready?
A. $ is only valid inside _ready syntactically.
B. _init runs when the object is created in memory, before its children are in the tree; _ready runs after all children have entered the tree.
C. _init cannot access any variables.
D. _ready creates the children, so they don't exist until it runs.
Reveal answer
B. _init fires at object creation, before the node is assembled into the tree with its
children; _ready fires once the whole subtree is in the tree, so child references are valid.
A is false ($ is valid anywhere syntactically; it just resolves to nothing in _init). C is
false — _init can use members. D is wrong about who creates children (the scene/engine does,
not _ready).
Q3 — var e := node as Enemy. What must you do before calling e.alert()?
A. Nothing — as guarantees e is an Enemy.
B. Check if e: (or e != null), because as yields null when node is not an Enemy.
C. Wrap it in a match.
D. Re-cast with is.
Reveal answer
B. as is a safe cast: it returns null rather than crashing when the type does not
match, so you must guard against null before using the result. A misstates the guarantee —
as does not assure success. C and D are unnecessary machinery; the idiomatic guard is the
null check after the cast.
Integration question¶
Q4 — open
A component script has class_name Health / extends Node, a func _ready() that does setup, and
a subclass class_name ArmoredHealth / extends Health that overrides _ready to also read an
armor value — but the armored version's bars never initialize. Separately, another script does
node.take_damage(5) directly on a Node that might be a Health, and crashes when it is not.
Diagnose both, naming the chapter idea each violates.
Reveal expected answer
Bug 1 — missing super. ArmoredHealth._ready overrides Health._ready without calling
super(), so the base class's setup (initializing the bars) never runs; only the armor read
does. The fix is to call super() at the top of the override so the base setup still happens,
then add the armored behavior. This violates the "skipping super skips the base's setup"
point. Bug 2 — unchecked type use. Calling node.take_damage(5) on a Node that may not be
a Health is an unguarded assumption about type; when node is some other node, the method
does not exist and it crashes. The fix is to narrow first — var h := node as Health; if h:
h.take_damage(5) — or guard with if node is Health:. This violates the "an unchecked cast (or
unchecked assumption) crashes the same as no cast" point. Both bugs come from treating types
casually; class_name, super, and is/as are the tools that make type handling explicit
and checked.