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

Defined in:

libgodot/docs/c_gameplay_and_declarative_dsl/h_modules_and_mixins.cr

Class Method Summary

Class Method Detail

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

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)

def self.topic_01_declaring_gmodule : Nil #

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:

  1. Standard Crystal module Damageable.
  2. Module reflection metadata (_godot_module_properties, _godot_module_signals, _godot_module_constants).
  3. Property getter and setter dispatch routines (_godot_set_property, _godot_get_property) chaining to super.

def self.topic_02_node_inclusion : Nil #

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.


def self.topic_03_composed_inheritance : Nil #

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)

def self.topic_04_signals_and_events : Nil #

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

def self.topic_05_lifecycle_hooks : Nil #

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

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

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.


def self.topic_07_abstract_contracts : Nil #

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.