module
Lapis::Docs::C_GAMEPLAY_AND_DECLARATIVE_DSL::A_NODE_DSL
Overview
Declarative Node DSL & Lifecycle
Comprehensive guide to authoring gameplay entities using Lapis's declarative Node DSL, ClassDB registration, and engine lifecycle virtual methods.
Executive Summary & Key Topics
| Topic | Method / Anchor | Description |
|---|---|---|
| Core DSL Features | .topic_00_dsl_features |
Declarative class registration, export properties, typed signals, and in-editor tool execution. |
| Node Declaration Syntax | .topic_01_node_declaration |
Declaring custom nodes inheriting from 2D, 3D, UI, or Server nodes. |
| Engine Lifecycle Virtual Methods | .topic_02_lifecycle_methods |
Virtual methods invoked by the Godot scene tree during execution. |
| In-Editor Tool Scripts (@[Tool]) | .topic_03_tool_scripts |
Executing Crystal code live inside the Godot Editor viewport and scene dock. |
| Ergonomic Node Declarations & Groups | .topic_04_node_helpers |
Convenient macros for groups, cached node references, and unique scene access. |
| Context-Aware Node Reopening (Define vs. Monkeypatch Mode) | .topic_05_context_aware_node_reopening |
Modularizing Godot node definitions across multiple files without inheritance conflicts. |
| Traditional GodotClass Annotation Syntax | .topic_06_traditional_godot_class_syntax |
Authoring native Godot nodes using standard Crystal class syntax and the @[GodotClass] annotation. |
| Pure Crystal Domain Models & Stack-Allocated Structs | .topic_07_pure_crystal_domain_models |
Decoupling game logic into zero-allocation stack structs for 30ms headless testing and peak cache efficiency. |
| Autoload Nodes & Engine Singletons (@[Autoload]) | .topic_08_autoload_node_singletons |
Declaring persistent autoload nodes automatically registered as engine singletons and mounted to the SceneTree root. |
Related Guides & Source References
- Macro DSL:
src/libgodot/macros.cr - Live Specs:
spec/suites/test_node_dsl.cr - Showcase:
examples/basic_demo/src/main.cr
Defined in:
libgodot/docs/c_gameplay_and_declarative_dsl/a_node_dsl.crClass Method Summary
-
.topic_00_dsl_features : Nil
Core DSL Features: Declarative class registration, export properties, typed signals, and in-editor tool execution.
-
.topic_01_node_declaration : Nil
Node Declaration Syntax: Declaring custom nodes inheriting from 2D, 3D, UI, or Server nodes.
-
.topic_02_lifecycle_methods : Nil
Engine Lifecycle Virtual Methods: Virtual methods invoked by the Godot scene tree during execution.
-
.topic_03_tool_scripts : Nil
In-Editor Tool Scripts (@[Tool]): Executing Crystal code live inside the Godot Editor viewport and scene dock.
-
.topic_04_node_helpers : Nil
Ergonomic Node Declarations & Groups: Convenient macros for groups, cached node references, and unique scene access.
-
.topic_05_context_aware_node_reopening : Nil
Context-Aware Node Reopening (Define vs Monkeypatch Mode): Modularizing Godot node definitions across multiple files without inheritance conflicts.
-
.topic_06_traditional_godot_class_syntax : Nil
Traditional GodotClass Annotation Syntax: Authoring native Godot nodes using standard Crystal class syntax and the @[GodotClass] annotation.
-
.topic_07_pure_crystal_domain_models : Nil
Pure Crystal Domain Models & Stack-Allocated Structs: Decoupling game logic into zero-allocation stack structs for 30ms headless testing and peak cache efficiency.
-
.topic_08_autoload_node_singletons : Nil
Autoload Nodes & Engine Singletons (@[Autoload]): Declaring persistent autoload nodes automatically registered as engine singletons and mounted to the SceneTree root.
Class Method Detail
Core DSL Features: Declarative class registration, export properties, typed signals, and in-editor tool execution.
Key Topics & Information
- Declarative syntax: node ClassName < ParentNode do ... end
- Context-aware node reopening across files without inheritance conflict
- Traditional class syntax via @[GodotClass] annotation
- Pure Crystal domain models with zero GC stack-allocated structs
- Automatic ClassDB engine registration
- Virtual method overrides: _ready, _process, _physics_process, _enter_tree, _exit_tree
- In-editor live execution via @[Tool] annotation
- Declarative Autoload node singletons via @[Autoload] and typed .instance accessors
Node Declaration Syntax: Declaring custom nodes inheriting from 2D, 3D, UI, or Server nodes.
Declare a custom Godot node using the node macro:
require "libgodot"
node Enemy < CharacterBody2D do
@[Export]
property max_health : Int32 = 100
def _ready : Void
Godot.print("Enemy #{name} ready!")
end
end
Supported Parent Classes:
You can inherit from any registered Godot engine node:
- 2D Nodes:
Node2D,Sprite2D,CharacterBody2D,Area2D,RigidBody2D - 3D Nodes:
Node3D,MeshInstance3D,CharacterBody3D,Camera3D - UI Controls:
Control,Label,Button,Panel,ProgressBar - Servers: Direct RID manipulation with
RenderingServer,PhysicsServer2D
Engine Lifecycle Virtual Methods: Virtual methods invoked by the Godot scene tree during execution.
Godot dispatches lifecycle callbacks to custom nodes:
| Virtual Method | Invocation Timing | Typical Use Case |
|---|---|---|
_enter_tree : Void |
When node enters active scene tree | Subscribing to signals, global lookups |
_ready : Void |
When node and all children enter tree | Initialization, setting initial positions |
_process(delta : Float64) : Void |
Every visual render frame | Smooth visuals, UI updates, animations |
_physics_process(delta : Float64) : Void |
Fixed-rate physics step (60 Hz) | Movement, raycasts, collision calculations |
_exit_tree : Void |
When node leaves active scene tree | Cleanup, unregistering listeners |
In-Editor Tool Scripts (@[Tool]): Executing Crystal code live inside the Godot Editor viewport and scene dock.
Annotate a class with @[Tool] to have it execute inside the Godot Editor:
@[Tool]
node LevelGenerator < Node2D do
@[ExportToolButton("Generate Maze")]
property generate_button : ToolButton do
generate_level!
end
def generate_level! : Void
Godot.print("Procedural generation running in Godot Editor!")
end
end
When selected in the editor, clicking the inspector tool button executes the Crystal code live in the viewport!
Ergonomic Node Declarations & Groups: Convenient macros for groups, cached node references, and unique scene access.
Lapis provides built-in macros for declarative scene setup directly inside the node block:
node Player < CharacterBody2D do
# Declarative group membership
group "players", "damageable"
# Eager onready property initialized during _ready via unary ~:
onready sprite : Sprite2D = ~"Sprite2D"
onready health_bar : ProgressBar = ~"%HealthBar"
onready anim = ~"AnimationPlayer".as(AnimationPlayer)
# Lazy-cached node accessors (resolved on first access with dead-pointer safety):
onready camera, Camera2D, "Pivot/Camera2D"
onready? particle_fx, CPUParticles2D
# Scene Unique Node access (%UniqueNodeName)
unique_node hud, CanvasLayer, "PlayerHUD"
unique_node? quest_tracker, Control
end
Context-Aware Node Reopening (Define vs Monkeypatch Mode): Modularizing Godot node definitions across multiple files without inheritance conflicts.
Lapis features a context-aware node macro that distinguishes between the initial class definition and subsequent class reopenings (monkeypatching).
In standard Crystal, declaring class Player < CharacterBody2D a second time triggers a compile-time superclass mismatch or redefinition error. Lapis inspects class resolution at compile time:
- Initial Definition:
node Player < CharacterBody2Dcreates the class, registers it inClassDB, binds initial exported properties and lifecycle methods. - Reopening (Monkeypatch): Subsequent declarations (
node Player do ... end) automatically open the class in monkeypatch mode without repeating< SuperClass.
All exported properties, signals, RPC definitions, and virtual methods from reopened blocks are dynamically merged into a single unified ClassDB table:
# File: src/entities/player.cr
node Player < CharacterBody2D do
@[Export]
property speed : Float32 = 250.0_f32
def _physics_process(delta : Float64) : Void
# Movement logic
end
end
# File: src/entities/player_combat.cr
node Player do
signal health_changed(current : Int32, max : Int32)
@[Export]
property attack_power : Int32 = 25
def take_damage(amount : Int32) : Void
# Combat resolution
end
end
Dispatch methods (_godot_call_virtual, _godot_set_property, _godot_get_property) seamlessly chain to previous_def, allowing modular code splitting across feature directories without runtime overhead.
Traditional GodotClass Annotation Syntax: Authoring native Godot nodes using standard Crystal class syntax and the @[GodotClass] annotation.
Developers who prefer traditional object-oriented Crystal class syntax over declarative macro blocks can annotate standard classes with @[GodotClass]:
require "libgodot"
@[GodotClass]
class Player < Godot::CharacterBody2D
@[Export]
property speed : Float32 = 300.0_f32
@[Export]
property max_health : Int32 = 100
def _ready : Void
Godot.print("Player #{name} initialized via @[GodotClass]!")
end
def _physics_process(delta : Float64) : Void
# Standard physics movement
end
end
Automated ClassDB Synthesis:
When a class inherits from any Godot::Object subclass and carries @[GodotClass]:
- ClassDB Registration: Automatically generates entry with proper parent class name (
CharacterBody2D). - Property Reflection: Gathers all
@[Export]properties and registers typedPropertyInfoentries into the Godot Inspector. - Virtual Dispatch: Automatically wires
_ready,_process,_physics_process, and input methods to engine ptrcalls. - Zero Macro Boilerplate: Requires no
node do ... endwrapper blocks, matching standard Crystal class design.
Pure Crystal Domain Models & Stack-Allocated Structs: Decoupling game logic into zero-allocation stack structs for 30ms headless testing and peak cache efficiency.
While Godot nodes are excellent for spatial hierarchies and rendering, writing all gameplay logic directly inside engine nodes introduces native wrapper overhead, garbage collection pressure, and slow test feedback loops.
Lapis enables a high-performance Domain-Driven Design (DDD) pattern using pure Crystal stack-allocated structs:
# Zero GC allocation - lives directly in parent memory
struct CombatStats
property health : Int32
property max_health : Int32
property defense : Int32
property attack_power : Int32
def initialize(@max_health : Int32, @defense : Int32, @attack_power : Int32)
@health = @max_health
end
def alive? : Bool
@health > 0
end
# Pure deterministic calculation - zero engine dependencies
def apply_incoming_damage(raw_damage : Int32) : Int32
mitigated = Math.max(1, raw_damage - @defense)
@health = Math.max(0, @health - mitigated)
mitigated
end
end
Clean Composition inside a Godot Node:
The Godot node acts strictly as a visual renderer and spatial bridge:
node Player < CharacterBody2D do
# Inlined directly in memory - no separate heap pointer
@stats = CombatStats.new(max_health: 100, defense: 15, attack_power: 25)
def take_hit(damage : Int32) : Void
mitigated = @stats.apply_incoming_damage(damage)
Godot.print("Player took #{mitigated} damage! Remaining: #{@stats.health}")
queue_free unless @stats.alive?
end
end
High-Velocity Test-Driven Development (TDD):
Because CombatStats and other domain models have no dependency on Godot's SceneTree, developers can write comprehensive headless Crystal specs:
# spec/domain/combat_stats_spec.cr
require "spec"
require "../../src/domain/combat_stats"
describe CombatStats do
it "correctly mitigates damage by defense rating" do
stats = CombatStats.new(max_health: 100, defense: 10, attack_power: 20)
actual_dmg = stats.apply_incoming_damage(35)
actual_dmg.should eq(25)
stats.health.should eq(75)
stats.alive?.should be_true
end
end
Running crystal spec spec/domain/ executes hundreds of unit tests in 30 milliseconds (0.03s), without booting Godot, creating OS windows, or waiting for scene tree initialization!
Autoload Nodes & Engine Singletons (@[Autoload]): Declaring persistent autoload nodes automatically registered as engine singletons and mounted to the SceneTree root.
In Godot, Autoloads are persistent nodes loaded automatically at startup, accessible globally across scenes, and optionally registered as engine singletons in Engine.has_singleton.
Lapis provides first-class declarative support for Autoload nodes via the @[Autoload] annotation or in-body autoload macro directive:
@[Autoload]
node GameManager < Node do
property score : Int32 = 0
property current_level : String = "world_1"
def add_score(points : Int32) : Void
@score += points
end
end
Automatic Lifecycle & SceneTree Mounting:
- Automatic Lifecycle: When the GDExtension library initializes or the SceneTree boots,
AutoloadManagerinstantiates every registered Autoload node. - SceneTree Root Attachment: Autoloads are mounted under the root viewport (
/root/GameManager) so they participate in engine process loops, physics, and input events. - Engine Singleton Registration: The instance is registered in Godot's native
Enginesingleton registry (Engine.get_singleton("GameManager")), making it accessible from both Crystal and GDScript. - Type-Safe Accessors: The macro generates typed class-level accessors:
# Non-nil typed accessor (raises if not initialized) GameManager.instance.add_score(100) # Nilable typed accessor if gm = GameManager.instance? Godot.print("Score: #{gm.score}") end # Generic engine lookup node = Godot.autoload(GameManager)
Customizing Autoload Registration:
You can customize the singleton name, disable engine singleton registration, or disable SceneTree mounting:
# Custom name and tree-only (no Engine.register_singleton)
@[Autoload(name: "AudioService", singleton: false)]
node CustomAudioManager < AudioStreamPlayer do
end
# Or using in-body directive:
node InventoryManager < Node do
autoload name: "Inventory", singleton: true, mount_tree: true
end