module
Lapis::Docs::C_GAMEPLAY_AND_DECLARATIVE_DSL::H_MODULES_AND_MIXINS
Overview
Modular Node Mixins (gmodule)
Comprehensive guide to authoring modular gameplay components and mixins in Lapis for Crystal using the gmodule macro.
Learn how to compose reusable traits (damageable, interactable, inventory, movement) with full Godot engine parity:
automatic ClassDB registration, Inspector export properties, typed signals, cooperative lifecycle hooks, and abstract interface contracts.
Executive Summary & Key Topics
| Topic | Method / Anchor | Description |
|---|---|---|
| Composition & Mixin Comparison Matrix | .topic_00_overview_matrix |
Architectural comparison of traditional class inheritance versus modular mixins (gmodule) and child node composition in Lapis. |
| Declaring Modular Mixins (gmodule) | .topic_01_declaring_gmodule |
Syntax for declaring reusable gameplay trait modules with properties, exported variables, and custom methods. |
| Including Mixins in Custom Nodes | .topic_02_node_inclusion |
Mixing modules into custom nodes using standard Crystal include syntax with automatic Inspector exposure. |
| Composed Module Inheritance | .topic_03_composed_inheritance |
Composing modules by having one gmodule inherit from another gmodule. |
| Typed Signals in Modules | .topic_04_signals_and_events |
Declaring, emitting, and connecting to type-safe signals defined within gmodule mixins. |
| Cooperative Lifecycle Hooks | .topic_05_lifecycle_hooks |
Chaining engine lifecycle methods (_ready, _process, _physics_process) across multiple mixins using super. |
| Inspector Tool Buttons in Mixins | .topic_06_tool_buttons_and_inspector |
Exporting interactive clickable buttons in the Godot Editor Inspector from within gmodule mixins. |
| Abstract Interface Contracts | .topic_07_abstract_contracts |
Enforcing compile-time interface method implementations on nodes that include a trait module. |
Related Guides & Source References
- Macro Core:
src/libgodot/macros.cr - Object Hierarchy:
src/libgodot/object.cr - Modules Spec:
spec/modules_spec.cr - Node Hierarchy Spec:
spec/suites/test_node_hierarchy.cr
Defined in:
libgodot/docs/c_gameplay_and_declarative_dsl/h_modules_and_mixins.crClass Method Summary
-
.topic_00_overview_matrix : Nil
Composition & Mixin Comparison Matrix: Architectural comparison of traditional class inheritance versus modular mixins (gmodule) and child node composition in Lapis.
-
.topic_01_declaring_gmodule : Nil
Declaring Modular Mixins (gmodule): Syntax for declaring reusable gameplay trait modules with properties, exported variables, and custom methods.
-
.topic_02_node_inclusion : Nil
Including Mixins in Custom Nodes: Mixing modules into custom nodes using standard Crystal include syntax with automatic Inspector exposure.
-
.topic_03_composed_inheritance : Nil
Composed Module Inheritance: Composing modules by having one gmodule inherit from another gmodule.
-
.topic_04_signals_and_events : Nil
Typed Signals in Modules: Declaring, emitting, and connecting to type-safe signals defined within gmodule mixins.
-
.topic_05_lifecycle_hooks : Nil
Cooperative Lifecycle Hooks: Chaining engine lifecycle methods (_ready, _process, _physics_process) across multiple mixins using super.
-
.topic_06_tool_buttons_and_inspector : Nil
Inspector Tool Buttons in Mixins: Exporting interactive clickable buttons in the Godot Editor Inspector from within gmodule mixins.
-
.topic_07_abstract_contracts : Nil
Abstract Interface Contracts: Enforcing compile-time interface method implementations on nodes that include a trait module.
Class Method Detail
Composition & Mixin Comparison Matrix: Architectural comparison of traditional class inheritance versus modular mixins (gmodule) and child node composition in Lapis.
| Paradigm | Primary Mechanism | ClassDB / Inspector Exposure | Performance Overhead | Typical Use Cases |
|---|---|---|---|---|
| Class Inheritance | node Enemy < CharacterBody2D |
Inherited from base engine node and custom parent class | Zero overhead; direct vtable dispatch | Core physics bodies, specialized base classes (Vehicle, Character) |
| Modular Mixins | gmodule Damageable + include Damageable |
Flattened into node ClassDB entry; fully editable in Inspector | Zero overhead; compiled inline with static dispatch | Cross-cutting gameplay behaviors: Health, Interactable, StatusEffects |
| Child Node Composition | add_child(HealthComponent.new) |
Separate node instance in SceneTree with independent Inspector | SceneTree traversal overhead; separate memory allocations | Complex sub-systems with visual/spatial representation (Hitboxes, Hurtboxes) |
Declaring Modular Mixins (gmodule): Syntax for declaring reusable gameplay trait modules with properties, exported variables, and custom methods.
Declare a modular mixin using the gmodule macro. Inside the module block, you can declare exported properties,
internal state variables, typed signals, and gameplay helper methods:
require "libgodot"
# Reusable damage and health trait
gmodule Damageable do
# Exported properties are exposed to Godot's Inspector on any node that includes this module
@[Export(range: 0..500)]
property health : Int32 = 100
@[Export]
property max_health : Int32 = 100
@[Export]
property defense : Float32 = 5.0_f32
@[Export]
property is_invulnerable : Bool = false
def take_damage(amount : Int32) : Void
return if self.is_invulnerable
effective = Math.max(0, amount - self.defense.to_i32)
self.health = Math.max(0, self.health - effective)
emit(health_changed, self.health, self.max_health)
emit(died) if self.health == 0
end
def heal(amount : Int32) : Void
self.health = Math.min(self.max_health, self.health + amount)
emit(health_changed, self.health, self.max_health)
end
end
When compiled, gmodule synthesizes:
- Standard Crystal
module Damageable. - Module reflection metadata (
_godot_module_properties,_godot_module_signals,_godot_module_constants). - Property getter and setter dispatch routines (
_godot_set_property,_godot_get_property) chaining tosuper.
Including Mixins in Custom Nodes: Mixing modules into custom nodes using standard Crystal include syntax with automatic Inspector exposure.
Any node declared with the node macro can mix in one or more gmodule definitions using standard include:
node HeroCharacter < CharacterBody2D do
include Damageable
@[Export]
property hero_name : String = "Hero"
def _ready : Void
Godot.print("#{hero_name} ready with #{health}/#{max_health} HP")
end
end
Automatic ClassDB Registration:
At compile time, macro node inspects all included modules in its ancestor chain via macro finished.
All exported properties declared inside included modules are automatically registered into the node's ClassRegistry.Entry.
In the Godot Editor Inspector, the node displays both its own exported properties and all properties contributed by included mixins.
Composed Module Inheritance: Composing modules by having one gmodule inherit from another gmodule.
Modules can inherit from and extend other modules using the standard inheritance syntax gmodule ChildModule < ParentModule:
# Composed module extending Damageable
gmodule Combatant < Damageable do
signal attack_landed(target : String, damage : Int32)
@[Export]
property attack_power : Int32 = 25
def perform_attack(target_name : String) : Int32
dmg = self.attack_power
emit(attack_landed, target_name, dmg)
dmg
end
end
node BossMonster < CharacterBody3D do
include Combatant
@[Export]
property phase : Int32 = 1
end
BossMonster automatically inherits:
- All properties from
Combatant(attack_power) - All properties from
Damageable(health,max_health,defense,is_invulnerable) - All signals from both modules (
attack_landed,health_changed,died) - Its own properties (
phase)
Typed Signals in Modules: Declaring, emitting, and connecting to type-safe signals defined within gmodule mixins.
Declare signals inside a gmodule using the standard signal macro:
gmodule Interactable do
signal interacted(actor_name : String)
signal interaction_failed(reason : String)
@[Export]
property prompt : String = "Press E to interact"
def trigger_interaction(actor : String) : Void
emit(interacted, actor)
end
end
When included in a node, the signal accessor method (interacted) is available with compile-time type safety:
node Chest < Area2D do
include Interactable
def _ready : Void
self.interacted.connect do |actor|
Godot.print("Chest opened by #{actor}")
end
end
end
Cooperative Lifecycle Hooks: Chaining engine lifecycle methods (_ready, _process, _physics_process) across multiple mixins using super.
Modules can define engine virtual callbacks (_enter_tree, _exit_tree, _ready, _process, _physics_process).
Because Godot::Object and Godot::Node provide default no-op implementations, modules and classes should always call super
to ensure every ancestor in the Crystal method resolution order (MRO) executes cooperatively:
gmodule AutoHealthRegen do
include Damageable
@[Export]
property regen_rate : Float32 = 2.0_f32 # HP per second
property regen_accumulator : Float64 = 0.0
def _process(delta : Float64) : Void
super # Invoke other modules and base node _process
self.regen_accumulator += delta
if self.regen_accumulator >= 1.0
self.heal(self.regen_rate.to_i32)
self.regen_accumulator = 0.0
end
end
end
node RegeneratingHero < CharacterBody2D do
include AutoHealthRegen
def _process(delta : Float64) : Void
super # Runs AutoHealthRegen._process, which in turn calls super
# Custom hero frame logic here
end
end
Inspector Tool Buttons in Mixins: Exporting interactive clickable buttons in the Godot Editor Inspector from within gmodule mixins.
Use the @[ExportToolButton] annotation inside a gmodule to expose one-click actions in the Godot Inspector:
gmodule Damageable do
@[Export]
property health : Int32 = 100
@[Export]
property max_health : Int32 = 100
@[ExportToolButton("Reset Health & Stats")]
def reset_stats : Void
self.health = self.max_health
self.is_invulnerable = false
Godot.print("Stats reset to maximum.")
end
end
When an included node is selected in the Godot Editor, the Reset Health & Stats button appears in the Inspector.
Clicking the button dispatches _godot_call_tool_button through the module chain to invoke the method.
Abstract Interface Contracts: Enforcing compile-time interface method implementations on nodes that include a trait module.
Use Crystal's native abstract def inside a gmodule to define an interface contract that including nodes MUST implement:
gmodule Interactable do
signal interacted(actor_name : String)
@[Export]
property prompt : String = "Press E to interact"
# Including nodes MUST implement this method or compilation fails
abstract def on_interact(actor : String) : Void
end
node NPC < CharacterBody2D do
include Interactable
# Satisfies the Interactable contract
def on_interact(actor : String) : Void
Godot.print("NPC greets #{actor}!")
emit(interacted, actor)
end
end
If a node includes Interactable without defining on_interact, Crystal's compiler issues a clear compile error,
guaranteeing architectural correctness at compile time.