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

Defined in:

libgodot/docs/c_gameplay_and_declarative_dsl/g_ergonomic_node_access.cr

Class Method Summary

Class Method Detail

def self.topic_00_syntax_matrix : Nil #

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"

def self.topic_01_typed_indexers : Nil #

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

def self.topic_02_path_and_unique_operators : Nil #

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"

def self.topic_03_bare_macros : Nil #

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

def self.topic_04_prefixes_and_nodepath : Nil #

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]

def self.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.

# 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

def self.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.

# 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