module
Lapis::Docs::C_GAMEPLAY_AND_DECLARATIVE_DSL::G_ERGONOMIC_NODE_ACCESS
Overview
Ergonomic Node & Path Access
Comprehensive guide to ergonomic node retrieval in Lapis for Crystal, providing statically type-safe, zero-cost alternatives to GDScript's $Node and %UniqueNode syntax.
Executive Summary & Key Topics
| Topic | Method / Anchor | Description |
|---|---|---|
| Syntax Ergonomics Comparison Matrix | .topic_00_syntax_matrix |
Side-by-side comparison of GDScript syntax vs Lapis ergonomic Crystal alternatives |
| Type-Inferred Subscript Indexers | .topic_01_typed_indexers |
Using self[Type] and self["path", Type] for compile-time type-safe node lookups |
| Path Traversal and Scene Unique Operators | .topic_02_path_and_unique_operators |
Intuitive path navigation with / and scene unique node retrieval with % |
| Bare Retrieval Macros in Methods | .topic_03_bare_macros |
Calling n! and u! directly inside methods without prefixing self. |
| Path Prefixes ($, %, $%) and NodePath Support | .topic_04_prefixes_and_nodepath |
Native support for GDScript $ and % prefixes, self-referencing $, and NodePath objects |
| Type Safety, KeyError, and Safe Lookups | .topic_05_safe_lookups_and_error_handling |
Guaranteed compile-time and runtime type safety with NodeNotFoundError and TypeCastError |
| Unary Tilde Operator and NodeContext Scoping | .topic_06_tilde_and_node_context |
Using ~("$Path").as(Type), ~("%Path").as(Type), and ~Type with zero allocation overhead |
Related Guides & Source References
- Object & Node Core:
src/libgodot/object.cr - Node Extensions:
src/libgodot/extensions/node.cr - Macro DSL:
src/libgodot/macros.cr - Live Specs:
spec/suites/test_node_hierarchy.cr - Ergonomics Specs:
spec/ergonomics_spec.cr
Defined in:
libgodot/docs/c_gameplay_and_declarative_dsl/g_ergonomic_node_access.crClass Method Summary
-
.topic_00_syntax_matrix : Nil
Syntax Ergonomics Comparison Matrix: Side-by-side comparison of GDScript syntax vs Lapis ergonomic Crystal alternatives.
-
.topic_01_typed_indexers : Nil
Type-Inferred Subscript Indexers: Using self[Type] and self["path", Type] for compile-time type-safe node lookups.
-
.topic_02_path_and_unique_operators : Nil
Path Traversal and Scene Unique Operators: Intuitive path navigation with / and scene unique node retrieval with %.
-
.topic_03_bare_macros : Nil
Bare Retrieval Macros in Methods: Calling n! and u! directly inside methods without prefixing self.
-
.topic_04_prefixes_and_nodepath : Nil
Path Prefixes ($, %, $%) and NodePath Support: Native support for GDScript $ and % prefixes, self-referencing $, and NodePath objects.
-
.topic_05_safe_lookups_and_error_handling : Nil
Type Safety, KeyError, and Safe Lookups: Guaranteed compile-time and runtime type safety with NodeNotFoundError and TypeCastError.
-
.topic_06_tilde_and_node_context : Nil
Unary Tilde Operator and NodeContext Scoping: Using ~("$Path").as(Type), ~("%Path").as(Type), and ~Type with zero allocation overhead.
Class Method Detail
Syntax Ergonomics Comparison Matrix: Side-by-side comparison of GDScript syntax vs Lapis ergonomic Crystal alternatives.
| Intent | GDScript Syntax | Verbose Method | Lapis Ergonomic Syntax |
|---|---|---|---|
| Child Node by Class | $Sprite2D (untyped) |
get_node_as("Sprite2D", Sprite2D) |
self[Sprite2D] or n!(Sprite2D) (typed!) |
| Child Node by Path (Typed) | $Visuals/Sprite2D as Sprite2D |
get_node_as("Visuals/Sprite2D", Sprite2D) |
self["Visuals/Sprite2D", Sprite2D] or n!("Visuals/Sprite2D", Sprite2D) |
| Child Node by Path (Untyped) | $"Visuals/Sprite2D" |
self["Visuals/Sprite2D"] |
self["Visuals/Sprite2D"] or self / "Visuals/Sprite2D" |
| Path Prefix Handling ($, %, $%) | $"Entities/Player", %"HealthBar" |
get_node("Entities/Player") |
self["$Entities/Player", Player] or self["%HealthBar", ProgressBar] |
| Scene Unique Node (Typed) | %HealthBar as ProgressBar |
get_node_as("%HealthBar", ProgressBar) |
self["%HealthBar", ProgressBar], self % ProgressBar, unique_as("HealthBar", ProgressBar), or u!("HealthBar", ProgressBar) |
| Scene Unique Node (Untyped) | %HealthBar |
get_node("%HealthBar") |
self["%HealthBar"], self % "HealthBar", or u!("HealthBar") |
| Safe / Optional Node (Typed) | get_node_or_null("Path") as Sprite2D |
get_node_as?("Path", Sprite2D) |
self[Sprite2D]?, self["path", Sprite2D]?, unique_as?("HealthBar", ProgressBar), or n?(Sprite2D) |
| Self-Reference Operator | $ or self |
self |
self["$"] or self["$."] (returns self directly) |
| Class Property Declaration | @onready var cam = $Camera3D |
onready cam, Camera3D |
onready cam, Camera3D (lazy-cached) |
| Unary Tilde (Typed Cast) | $Visuals/Player as Player |
get_node_as("Visuals/Player", Player) |
(~"$Visuals/Player").as(Player) or (~"%Player").as(Player) |
| Unary Tilde (Direct Node) | $"Visuals/Player" |
get_node("Visuals/Player") |
~"$Visuals/Player" or ~"%Player" |
| Unary Tilde on Class | $Player as Player |
get_node_as("Player", Player) |
~Player (typed!) |
| Multi-Segment Child Traversal | $Items/MyItem/Item3 |
get_node("Items/MyItem/Item3") |
self / "Items/MyItem/Item3" |
| Scene-Unique Subpath Navigation | %Items/Item22/Mesh |
get_node("%Items").get_node("Item22/Mesh") |
self % "Items/Item22/Mesh" |
Type-Inferred Subscript Indexers: Using self[Type] and self["path", Type] for compile-time type-safe node lookups.
# 1. Type-inferred lookup (matches class name):
sprite = self[Godot::Sprite2D]
# Equivalent to: get_node_as("Sprite2D", Godot::Sprite2D)
# 2. Path + Type lookup:
weapon = self["Visuals/WeaponMount", Godot::Marker2D]
# 3. Path prefix lookups with $ and %:
player = self["$Entities/Player", Player]
bar = self["%HealthBar", Godot::ProgressBar]
# 4. Safe optional lookup (returns nil instead of raising):
if maybe_camera = self[Godot::Camera3D]?
maybe_camera.make_current
end
if maybe_marker = self["Visuals/WeaponMount", Godot::Marker2D]?
maybe_marker.position = Godot::Vector2.new(10.0_f32, 0.0_f32)
end
Path Traversal and Scene Unique Operators: Intuitive path navigation with / and scene unique node retrieval with %.
# Path traversal with / (left-associative or multi-segment string):
player_marker = self / "Entities" / "Player" / "Marker2D"
deep_item = self / "Items/MyItem/Item3"
dollar_item = self / "$Items/MyItem/Item3" # leading $ safely ignored
# Typed path traversal:
hud_label = (self / "HUD")[Godot::Label]
# Scene Unique Node with % (mirrors GDScript %):
health_bar = self % "HealthBar"
typed_bar = self % Godot::ProgressBar
# Scene-Unique subpath navigation (%UniqueRoot/child/subchild):
mesh = self % "Items/Item22/Mesh" # resolves %Items first, then "Item22/Mesh"
Bare Retrieval Macros in Methods: Calling n! and u! directly inside methods without prefixing self.
node PlayerController < CharacterBody2D do
def _ready : Void
# Bare typed node retrieval (GDScript $ analog):
sprite = n!(Godot::Sprite2D)
gun = n!("Visuals/GunMount", Godot::Marker2D)
# Bare scene unique node retrieval (GDScript % analog):
health = u!(Godot::ProgressBar)
hud = u!("PlayerHUD")
typed_hud = u!("PlayerHUD", CanvasLayer)
# Safe variants returning nil if missing:
optional_cam = n?(Godot::Camera2D)
optional_gun = n?("Visuals/GunMount", Godot::Marker2D)
end
end
Path Prefixes ($, %, $%) and NodePath Support: Native support for GDScript $ and % prefixes, self-referencing $, and NodePath objects.
# Standard GDScript prefix notation:
player = self["$Player"] # leading $ safely stripped
player_typed = self["$Player", Player] # leading $ with typed cast
hp = self["%HealthBar"] # scene unique node %
hp_typed = self["%HealthBar", ProgressBar]
# Self-referencing notation ($ or $.):
same_node = self["$"] # returns self directly
same_node_safe = self["$"]? # returns self directly
same_node_dot = self["$."] # returns self directly
# Full NodePath struct support:
np = node_path!("Visuals/Sprite2D")
child = self[np]
child_typed = self[np, Godot::Sprite2D]
Type Safety, KeyError, and Safe Lookups: Guaranteed compile-time and runtime type safety with NodeNotFoundError and TypeCastError.
# 1. Raising on missing nodes:
# self["Missing"] raises Godot::NodeNotFoundError (inherits from KeyError)
begin
cam = self["MissingCamera"]
rescue ex : Godot::NodeNotFoundError
Godot.print("Camera node missing from scene: #{ex.message}")
end
# 2. Safe lookups returning nil instead of raising:
if maybe_cam = self["Camera3D"]?
# Found
end
# 3. Type-safe casting with TypeCastError:
# self["Sprite2D", Godot::Camera3D] raises TypeCastError if node is not a Camera3D
# self["Sprite2D", Godot::Camera3D]? returns nil on type mismatch
if sprite = self["Visuals/PlayerSprite", Godot::Sprite2D]?
sprite.visible = true
end
# 4. Scene unique type-safe helpers:
hp = unique_as("HealthBar", Godot::ProgressBar)
maybe_hp = unique_as?("NonExistent", Godot::ProgressBar) # returns nil
Unary Tilde Operator and NodeContext Scoping: Using ~("$Path").as(Type), ~("%Path").as(Type), and ~Type with zero allocation overhead.
# Enter active NodeContext scope:
self.with_context do
# 1. Direct Node retrieval via ~("$Path"):
camera = ~"$Camera3D"
# 2. Typed cast via (~"$Path").as(Type):
player = (~"$Entities/Player").as(Player)
# 3. Scene-unique typed cast via (~"%Name").as(Type):
health_bar = (~"%HealthBar").as(ProgressBar)
# 4. Two-tier resolution via ~Type (name first, then child search; raises Godot::NodeNotFoundError if missing):
hero = ~Player
# 5. Nilable two-tier resolution via ~Type? (returns nil if missing, get_node_or_null parity):
if optional_sprite = ~Godot::Sprite2D?
optional_sprite.visible = true
end
end