GroveEngine/docs/DEVELOPER_GUIDE.md
StillHammer aa3c35bd2f docs: Update all documentation to reflect development stage
COMPREHENSIVE DOCUMENTATION CLEANUP:

DEVELOPER_GUIDE.md:
- Add prominent warning at top (development-ready, NOT production-ready)
- New 'Current Limitations' section with detailed warnings:
  * Non-deterministic execution
  * Single-threaded only
  * Not suitable for networked games
  * No error recovery, limited optimizations
- Change all 'Production Ready' → 'Development Ready' status tags
- BgfxRenderer: Phase 8 complete | Experimental
- UIModule: Phase 7 complete | Experimental
- InputModule: Phase 1-3 complete | Gamepad Phase 2 TODO
- Add 'What GroveEngine IS Good For' section (prototyping, learning, AI-assisted dev)
- Add Production Roadmap section

USER_GUIDE.md:
- Add experimental/development warning with link to README

Module READMEs:
- BgfxRenderer/README.md: Add development stage warning
- InputModule/README.md: Clarify Phase 1-3 complete, gamepad TODO
- UIModule/README.md: Add experimental warning, clarify thread-safe design is for future

All documentation now consistently reflects that GroveEngine is:
 Excellent for rapid prototyping and experimentation
⚠️ NOT ready for production games
⚠️ Non-deterministic execution
⚠️ Single-threaded only (for now)

Total changes: 5 files, 62 insertions, 13 deletions
2026-01-15 09:26:23 +07:00

29 KiB

GroveEngine - Developer Guide

Comprehensive guide for building applications with GroveEngine

⚠️ IMPORTANT: GroveEngine is currently in development stage - suitable for prototyping and experimentation, not production games. The engine is non-deterministic and optimized for rapid iteration, not stability. See Current Limitations below.

Table of Contents

  1. Current Limitations
  2. Getting Started
  3. Core System
  4. Available Modules
  5. IIO Topics Reference
  6. Complete Application Example
  7. Building Your First Game
  8. Advanced Topics

Current Limitations

⚠️ GroveEngine is EXPERIMENTAL and NOT production-ready. Understand these limitations before building with it:

Non-Deterministic Execution

  • Module execution order is NOT guaranteed - modules may run in different orders between frames
  • Not suitable for networked games - no deterministic replay or synchronization
  • Race conditions possible - only SequentialModuleSystem is currently implemented (single-threaded)

Development Stage

  • Optimized for rapid iteration, not stability
  • No error recovery - crashes are not handled gracefully
  • Limited performance optimizations - no profiling, memory pooling, or SIMD
  • Single-threaded only - ThreadedModuleSystem and MultithreadedModuleSystem are TODO

Module Limitations

  • InputModule: Mouse and keyboard only (gamepad Phase 2 not implemented)
  • BgfxRenderer: Basic text rendering only (8x8 bitmap font for debug)
  • UIModule: Functional but no advanced layout constraints

What GroveEngine IS Good For

Rapid prototyping - 0.4ms hot-reload for instant iteration Learning modular architecture - clean interface-based design AI-assisted development - 200-300 line modules optimized for Claude Code Experimentation - test game ideas quickly

Production Roadmap

To make GroveEngine production-ready, the following is needed:

  • Deterministic execution guarantees
  • Error recovery and graceful degradation
  • Multi-threaded module systems
  • Performance profiling and optimization
  • Network IO and distributed messaging
  • Complete gamepad support
  • Advanced text rendering

Getting Started

Prerequisites

  • C++17 compiler (GCC, Clang, or MSVC)
  • CMake 3.20+
  • Git for dependency management

Quick Start

# Clone GroveEngine
git clone <grove-engine-repo> GroveEngine
cd GroveEngine

# Build with all modules
cmake -B build -DGROVE_BUILD_BGFX_RENDERER=ON -DGROVE_BUILD_UI_MODULE=ON -DGROVE_BUILD_INPUT_MODULE=ON
cmake --build build -j4

# Run tests
cd build && ctest --output-on-failure

Documentation Structure


Core System

Architecture Overview

GroveEngine uses a module-based architecture with hot-reload support:

┌─────────────────────────────────────────────────────────────┐
│                    Your Application                          │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐   │
│  │  Input   │  │   UI     │  │ Renderer │  │  Game    │   │
│  │  Module  │  │  Module  │  │  Module  │  │  Logic   │   │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘   │
│       └─────────────┼─────────────┼─────────────┘          │
│                     │  IIO Pub/Sub System                   │
└─────────────────────┼───────────────────────────────────────┘
                      │
                IntraIOManager

Key Concepts

Component Purpose Documentation
IModule Module interface USER_GUIDE.md
IIO Pub/Sub messaging USER_GUIDE.md
IDataNode Configuration & data USER_GUIDE.md
ModuleLoader Hot-reload system USER_GUIDE.md

Available Modules

BgfxRenderer - 2D Rendering

Status: Development Ready (Phase 8 complete) | ⚠️ Non-deterministic, experimental

Multi-backend 2D renderer using bgfx (DirectX 11/12, OpenGL, Vulkan, Metal).

Features

  • Sprite rendering with batching
  • Text rendering with bitmap fonts
  • Tilemap support
  • Particle effects
  • Debug shapes (lines, rectangles)
  • Layer-based Z-ordering
  • Multi-texture batching
  • Headless mode for testing

Configuration

JsonDataNode config("config");
config.setInt("windowWidth", 1920);
config.setInt("windowHeight", 1080);
config.setString("backend", "auto");  // auto, opengl, vulkan, dx11, dx12, metal, noop
config.setString("shaderPath", "./shaders");
config.setBool("vsync", true);
config.setInt("maxSpritesPerBatch", 10000);
config.setInt("nativeWindowHandle", (int)(intptr_t)hwnd);  // Platform window handle

renderer->setConfiguration(config, rendererIO.get(), nullptr);

Rendering a Sprite

// Publish sprite to render
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setDouble("x", 100.0);
sprite->setDouble("y", 200.0);
sprite->setDouble("scaleX", 1.0);
sprite->setDouble("scaleY", 1.0);
sprite->setDouble("rotation", 0.0);        // Radians
sprite->setInt("color", 0xFFFFFFFF);       // RGBA
sprite->setInt("textureId", playerTexture);
sprite->setInt("layer", 10);               // Z-order (higher = front)
io->publish("render:sprite", std::move(sprite));

Rendering Text

auto text = std::make_unique<JsonDataNode>("text");
text->setDouble("x", 50.0);
text->setDouble("y", 50.0);
text->setString("text", "Score: 100");
text->setDouble("fontSize", 24.0);
text->setInt("color", 0xFFFFFFFF);
text->setInt("layer", 100);                // Text on top
io->publish("render:text", std::move(text));

Camera Control

auto camera = std::make_unique<JsonDataNode>("camera");
camera->setDouble("x", playerX);           // Center camera on player
camera->setDouble("y", playerY);
camera->setDouble("zoom", 1.0);
camera->setInt("viewportX", 0);
camera->setInt("viewportY", 0);
camera->setInt("viewportW", 1920);
camera->setInt("viewportH", 1080);
io->publish("render:camera", std::move(camera));

Full Topic Reference: See IIO Topics - Rendering


UIModule - User Interface

Status: Development Ready (Phase 7 complete) | ⚠️ Experimental

Complete UI widget system with layout, scrolling, and tooltips.

Available Widgets

Widget Purpose Events
UIButton Clickable button ui:click, ui:action
UILabel Static text -
UIPanel Container -
UICheckbox Toggle checkbox ui:value_changed
UISlider Value slider ui:value_changed
UITextInput Text input field ui:value_changed, ui:text_submitted
UIProgressBar Progress display -
UIImage Image/sprite -
UIScrollPanel Scrollable container ui:scroll
UITooltip Hover tooltip -

Configuration

JsonDataNode uiConfig("config");
uiConfig.setInt("windowWidth", 1920);
uiConfig.setInt("windowHeight", 1080);
uiConfig.setString("layoutFile", "./ui/main_menu.json");  // JSON layout
uiConfig.setInt("baseLayer", 1000);  // UI renders above game (layer 1000+)

uiModule->setConfiguration(uiConfig, uiIO.get(), nullptr);

Creating a Button

// In your layout JSON file (ui/main_menu.json)
{
  "type": "UIButton",
  "id": "play_button",
  "x": 100,
  "y": 200,
  "width": 200,
  "height": 50,
  "text": "Play Game",
  "action": "start_game"
}
// In your game module - subscribe to button events
gameIO->subscribe("ui:click");
gameIO->subscribe("ui:action");

// In process()
while (gameIO->hasMessages() > 0) {
    auto msg = gameIO->pullMessage();

    if (msg.topic == "ui:action") {
        std::string action = msg.data->getString("action", "");
        std::string widgetId = msg.data->getString("widgetId", "");

        if (action == "start_game" && widgetId == "play_button") {
            startGame();
        }
    }
}

Handling Input Events

UIModule automatically consumes input events from InputModule:

// UIModule subscribes to:
// - input:mouse:move
// - input:mouse:button
// - input:mouse:wheel
// - input:keyboard:key
// - input:keyboard:text

// Your game module just subscribes to UI events:
gameIO->subscribe("ui:*");  // All UI events

UI Rendering

UIModule publishes render commands to BgfxRenderer via UIRenderer:

// UIModule uses retained mode rendering (only publishes on change):
// - render:sprite:add/update/remove (for UI rectangles/images)
// - render:text:add/update/remove (for labels/buttons)

// BgfxRenderer consumes these and renders the UI
// Layer management ensures UI renders on top (layer 1000+)

Retained Mode: Widgets cache render state and only publish IIO messages when visual properties change. This reduces message traffic by 85%+ for typical UIs. See UI Rendering Documentation for details.

Full Topic Reference: See IIO Topics - UI Events


InputModule - Input Handling

Status: Development Ready (Phase 1-3 complete) | ⚠️ Gamepad Phase 2 TODO

Cross-platform input handling with SDL2 backend (mouse, keyboard).

Features

  • Mouse (move, button, wheel)
  • Keyboard (key events, text input)
  • Thread-safe event buffering
  • Multiple backend support (SDL2, extensible)
  • Hot-reload support

Configuration

JsonDataNode inputConfig("config");
inputConfig.setString("backend", "sdl");
inputConfig.setBool("enableMouse", true);
inputConfig.setBool("enableKeyboard", true);
inputConfig.setBool("enableGamepad", false);  // Phase 2

inputModule->setConfiguration(inputConfig, inputIO.get(), nullptr);

Feeding Events (SDL2)

// In your main loop
SDL_Event event;
while (SDL_PollEvent(&event)) {
    // Feed to InputModule (thread-safe)
    inputModule->feedEvent(&event);

    // Also handle window events
    if (event.type == SDL_QUIT) {
        running = false;
    }
}

// Process InputModule (converts buffered events → IIO messages)
JsonDataNode input("input");
inputModule->process(input);

Consuming Input Events

// Subscribe to input topics
gameIO->subscribe("input:mouse:button");
gameIO->subscribe("input:keyboard:key");

// In process()
while (gameIO->hasMessages() > 0) {
    auto msg = gameIO->pullMessage();

    if (msg.topic == "input:mouse:button") {
        int button = msg.data->getInt("button", 0);  // 0=left, 1=middle, 2=right
        bool pressed = msg.data->getBool("pressed", false);
        double x = msg.data->getDouble("x", 0.0);
        double y = msg.data->getDouble("y", 0.0);

        if (button == 0 && pressed) {
            // Left mouse button pressed at (x, y)
            handleClick(x, y);
        }
    }

    if (msg.topic == "input:keyboard:key") {
        int scancode = msg.data->getInt("scancode", 0);  // SDL_SCANCODE_*
        bool pressed = msg.data->getBool("pressed", false);

        if (scancode == SDL_SCANCODE_SPACE && pressed) {
            playerJump();
        }
    }
}

Full Topic Reference: See IIO Topics - Input Events


IIO Topics Reference

Input Events

Published by InputModule, consumed by UIModule or game logic.

Mouse

Topic Payload Description
input:mouse:move {x: double, y: double} Mouse position (screen coords)
input:mouse:button {button: int, pressed: bool, x: double, y: double} Mouse click (0=left, 1=middle, 2=right)
input:mouse:wheel {delta: double} Mouse wheel (+up, -down)

Keyboard

Topic Payload Description
input:keyboard:key {scancode: int, pressed: bool, repeat: bool, shift: bool, ctrl: bool, alt: bool} Key event (scancode = SDL_SCANCODE_*)
input:keyboard:text {text: string} Text input (UTF-8, for TextInput widgets)

UI Events

Published by UIModule, consumed by game logic.

Topic Payload Description
ui:click {widgetId: string, x: double, y: double} Widget clicked
ui:action {widgetId: string, action: string} Button action triggered
ui:value_changed {widgetId: string, value: variant} Slider, checkbox, or text input changed
ui:text_submitted {widgetId: string, text: string} Text input submitted (Enter key)
ui:hover {widgetId: string, enter: bool} Mouse entered/left widget
ui:scroll {widgetId: string, scrollX: double, scrollY: double} Scroll panel scrolled

Rendering Topics

Consumed by BgfxRenderer, published by UIModule or game logic.

Sprites

Retained Mode (UIModule current):

Topic Payload Description
render:sprite:add {renderId, x, y, scaleX, scaleY, color, textureId, layer} Register new sprite (retained)
render:sprite:update {renderId, x, y, scaleX, scaleY, color, textureId, layer} Update existing sprite
render:sprite:remove {renderId} Unregister sprite

Immediate Mode (legacy, still supported):

Topic Payload Description
render:sprite {x, y, scaleX, scaleY, rotation, u0, v0, u1, v1, color, textureId, layer} Render single sprite (ephemeral)
render:sprite:batch {sprites: [array]} Render sprite batch (optimized)

Text

Retained Mode (UIModule current):

Topic Payload Description
render:text:add {renderId, x, y, text, fontSize, color, layer} Register new text (retained)
render:text:update {renderId, x, y, text, fontSize, color, layer} Update existing text
render:text:remove {renderId} Unregister text

Immediate Mode (legacy, still supported):

Topic Payload Description
render:text {x, y, text, fontSize, color, layer} Render text (ephemeral)

Note: See UI Rendering Documentation for details on retained mode rendering.

Tilemap

Topic Payload Description
render:tilemap {chunkX, chunkY, tiles: [array], tileSize, textureId, layer} Render tilemap chunk

Particles

Topic Payload Description
render:particle {x, y, velocityX, velocityY, color, lifetime, textureId, layer} Render particle

Camera

Topic Payload Description
render:camera {x, y, zoom, viewportX, viewportY, viewportW, viewportH} Set camera transform

Clear

Topic Payload Description
render:clear {color: int} Set clear color (RGBA)

Debug

Topic Payload Description
render:debug:line {x1, y1, x2, y2, color} Draw debug line
render:debug:rect {x, y, w, h, color, filled} Draw debug rectangle

Complete Application Example

Directory Structure

MyGame/
├── CMakeLists.txt
├── src/
│   ├── main.cpp
│   └── modules/
│       ├── GameLogic.h
│       └── GameLogic.cpp
├── assets/
│   ├── ui/
│   │   └── main_menu.json
│   └── sprites/
│       └── player.png
└── external/
    └── GroveEngine/  # Git submodule

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(MyGame VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# GroveEngine + Modules
add_subdirectory(external/GroveEngine)
set(GROVE_BUILD_BGFX_RENDERER ON CACHE BOOL "" FORCE)
set(GROVE_BUILD_UI_MODULE ON CACHE BOOL "" FORCE)
set(GROVE_BUILD_INPUT_MODULE ON CACHE BOOL "" FORCE)

# Main executable
add_executable(mygame src/main.cpp)
target_link_libraries(mygame PRIVATE
    GroveEngine::impl
    SDL2::SDL2
    spdlog::spdlog
)

# Game logic module
add_library(GameLogic SHARED
    src/modules/GameLogic.cpp
)
target_link_libraries(GameLogic PRIVATE
    GroveEngine::impl
    spdlog::spdlog
)
set_target_properties(GameLogic PROPERTIES
    LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules
)

main.cpp

#include <grove/ModuleLoader.h>
#include <grove/IntraIOManager.h>
#include <grove/JsonDataNode.h>
#include <SDL2/SDL.h>
#include <iostream>

int main(int argc, char* argv[]) {
    // Initialize SDL
    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
    SDL_Window* window = SDL_CreateWindow("MyGame", SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED, 1920, 1080, SDL_WINDOW_SHOWN);

    // Get native window handle
    SDL_SysWMinfo wmInfo;
    SDL_VERSION(&wmInfo.version);
    SDL_GetWindowWMInfo(window, &wmInfo);
    void* nativeHandle = nullptr;
#ifdef _WIN32
    nativeHandle = wmInfo.info.win.window;  // HWND
#elif __linux__
    nativeHandle = (void*)(uintptr_t)wmInfo.info.x11.window;
#endif

    // Create IIO instances
    auto& ioManager = grove::IntraIOManager::getInstance();
    auto rendererIO = ioManager.createInstance("renderer");
    auto uiIO = ioManager.createInstance("ui");
    auto inputIO = ioManager.createInstance("input");
    auto gameIO = ioManager.createInstance("game");

    // Load modules
    grove::ModuleLoader rendererLoader, uiLoader, inputLoader, gameLoader;

    auto renderer = rendererLoader.load("./modules/BgfxRenderer.dll", "renderer");
    auto uiModule = uiLoader.load("./modules/UIModule.dll", "ui");
    auto inputModule = inputLoader.load("./modules/InputModule.dll", "input");
    auto gameModule = gameLoader.load("./modules/GameLogic.dll", "game");

    // Configure BgfxRenderer
    grove::JsonDataNode rendererConfig("config");
    rendererConfig.setInt("windowWidth", 1920);
    rendererConfig.setInt("windowHeight", 1080);
    rendererConfig.setString("backend", "auto");
    rendererConfig.setInt("nativeWindowHandle", (int)(intptr_t)nativeHandle);
    renderer->setConfiguration(rendererConfig, rendererIO.get(), nullptr);

    // Configure UIModule
    grove::JsonDataNode uiConfig("config");
    uiConfig.setInt("windowWidth", 1920);
    uiConfig.setInt("windowHeight", 1080);
    uiConfig.setString("layoutFile", "./assets/ui/main_menu.json");
    uiConfig.setInt("baseLayer", 1000);
    uiModule->setConfiguration(uiConfig, uiIO.get(), nullptr);

    // Configure InputModule
    grove::JsonDataNode inputConfig("config");
    inputConfig.setString("backend", "sdl");
    inputModule->setConfiguration(inputConfig, inputIO.get(), nullptr);

    // Configure GameLogic
    grove::JsonDataNode gameConfig("config");
    gameModule->setConfiguration(gameConfig, gameIO.get(), nullptr);

    // Main loop
    bool running = true;
    Uint64 lastTime = SDL_GetPerformanceCounter();

    while (running) {
        // 1. Handle SDL events
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = false;
            }
            inputModule->feedEvent(&event);  // Feed to InputModule
        }

        // 2. Calculate deltaTime
        Uint64 now = SDL_GetPerformanceCounter();
        double deltaTime = (now - lastTime) / (double)SDL_GetPerformanceFrequency();
        lastTime = now;

        // 3. Process all modules
        grove::JsonDataNode input("input");
        input.setDouble("deltaTime", deltaTime);

        inputModule->process(input);   // Input → IIO messages
        uiModule->process(input);      // UI → IIO messages
        gameModule->process(input);    // Game logic
        renderer->process(input);      // Render frame

        // 4. Optional: Hot-reload check
        // (file watcher code here)
    }

    // Cleanup
    renderer->shutdown();
    uiModule->shutdown();
    inputModule->shutdown();
    gameModule->shutdown();

    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

GameLogic.cpp

#include <grove/IModule.h>
#include <grove/JsonDataNode.h>
#include <grove/IIO.h>
#include <spdlog/spdlog.h>

class GameLogic : public grove::IModule {
public:
    GameLogic() {
        m_logger = spdlog::stdout_color_mt("GameLogic");
    }

    void setConfiguration(const grove::IDataNode& config,
                          grove::IIO* io,
                          grove::ITaskScheduler* scheduler) override {
        m_io = io;

        // Subscribe to UI events
        m_io->subscribe("ui:action");
        m_io->subscribe("ui:click");
    }

    void process(const grove::IDataNode& input) override {
        double deltaTime = input.getDouble("deltaTime", 0.016);

        // Process UI events
        while (m_io->hasMessages() > 0) {
            auto msg = m_io->pullMessage();

            if (msg.topic == "ui:action") {
                std::string action = msg.data->getString("action", "");

                if (action == "start_game") {
                    startGame();
                }
            }
        }

        // Update game logic
        if (m_gameStarted) {
            updatePlayer(deltaTime);
            renderPlayer();
        }
    }

    // ... other IModule methods ...

private:
    void startGame() {
        m_gameStarted = true;
        m_playerX = 960.0;
        m_playerY = 540.0;
        m_logger->info("Game started!");
    }

    void updatePlayer(double deltaTime) {
        // Update player position, etc.
    }

    void renderPlayer() {
        // Publish sprite to renderer
        auto sprite = std::make_unique<grove::JsonDataNode>("sprite");
        sprite->setDouble("x", m_playerX);
        sprite->setDouble("y", m_playerY);
        sprite->setInt("textureId", 0);  // Player texture
        sprite->setInt("layer", 10);
        m_io->publish("render:sprite", std::move(sprite));
    }

    std::shared_ptr<spdlog::logger> m_logger;
    grove::IIO* m_io = nullptr;
    bool m_gameStarted = false;
    double m_playerX = 0.0;
    double m_playerY = 0.0;
};

extern "C" {
    grove::IModule* createModule() { return new GameLogic(); }
    void destroyModule(grove::IModule* m) { delete m; }
}

Interactive Demo - Try It First!

Before reading further, try the full stack interactive demo to see everything in action:

# Windows
run_full_stack_demo.bat

# Linux
./build/tests/test_full_stack_interactive

What it demonstrates:

  • BgfxRenderer rendering sprites and text
  • UIModule with buttons, sliders, panels
  • InputModule capturing mouse and keyboard
  • Complete IIO message flow (input → UI → game → render)
  • Hit testing and click detection (raycasting 2D)
  • Game logic responding to UI events

Interactive controls:

  • Click buttons to spawn/clear sprites
  • Drag slider to change speed
  • Press SPACE to spawn from keyboard
  • Press ESC to exit

See: tests/visual/README_FULL_STACK.md for details.


Building Your First Game

Step-by-Step Tutorial

1. Create Project Structure

mkdir MyGame && cd MyGame
git init
git submodule add <grove-engine-repo> external/GroveEngine
mkdir -p src/modules assets/ui

2. Create CMakeLists.txt

(See Complete Application Example)

3. Create UI Layout

assets/ui/main_menu.json:

{
  "widgets": [
    {
      "type": "UIPanel",
      "id": "main_panel",
      "x": 0,
      "y": 0,
      "width": 1920,
      "height": 1080,
      "color": 2155905279
    },
    {
      "type": "UIButton",
      "id": "play_button",
      "x": 860,
      "y": 500,
      "width": 200,
      "height": 60,
      "text": "Play",
      "action": "start_game"
    },
    {
      "type": "UIButton",
      "id": "quit_button",
      "x": 860,
      "y": 580,
      "width": 200,
      "height": 60,
      "text": "Quit",
      "action": "quit_game"
    }
  ]
}

4. Build and Run

cmake -B build
cmake --build build -j4
./build/mygame

Advanced Topics

Hot-Reload Workflow

# Terminal 1: Run game
./build/mygame

# Terminal 2: Edit and rebuild module
vim src/modules/GameLogic.cpp
cmake --build build --target GameLogic

# Game automatically reloads GameLogic with state preserved!

Performance Optimization

Sprite Batching

// Instead of publishing 100 individual sprites:
for (auto& enemy : enemies) {
    auto sprite = std::make_unique<JsonDataNode>("sprite");
    sprite->setDouble("x", enemy.x);
    // ...
    io->publish("render:sprite", std::move(sprite));  // 100 IIO messages
}

// Publish as batch:
auto batch = std::make_unique<JsonDataNode>("batch");
auto sprites = std::make_unique<JsonDataNode>("sprites");
for (auto& enemy : enemies) {
    auto sprite = std::make_unique<JsonDataNode>("sprite");
    sprite->setDouble("x", enemy.x);
    // ...
    sprites->setChild(enemy.id, std::move(sprite));
}
batch->setChild("sprites", std::move(sprites));
io->publish("render:sprite:batch", std::move(batch));  // 1 IIO message

Low-Frequency Subscriptions

// For non-critical analytics/logging
grove::SubscriptionConfig config;
config.batchInterval = 1000;  // Batch messages for 1 second
io->subscribeLowFreq("analytics:*", config);

Multi-Module Communication Patterns

Request-Response Pattern

// Module A: Request pathfinding
auto request = std::make_unique<JsonDataNode>("request");
request->setString("requestId", "path_123");
request->setDouble("startX", 10.0);
request->setDouble("startY", 20.0);
io->publish("pathfinding:request", std::move(request));

// Module B: Respond with path
moduleB_io->subscribe("pathfinding:request");
// ... compute path ...
auto response = std::make_unique<JsonDataNode>("response");
response->setString("requestId", "path_123");
// ... add path data ...
moduleB_io->publish("pathfinding:response", std::move(response));

// Module A: Receive response
moduleA_io->subscribe("pathfinding:response");

Event Aggregation

// Multiple modules publish events
io->publish("combat:damage", damageData);
io->publish("combat:kill", killData);
io->publish("combat:levelup", levelupData);

// Analytics module aggregates all combat events
analyticsIO->subscribe("combat:*");

Testing Strategies

Headless Testing

// Configure renderer in headless mode
JsonDataNode config("config");
config.setString("backend", "noop");  // No actual rendering
config.setBool("vsync", false);
renderer->setConfiguration(config, io, nullptr);

// Run tests without window
for (int i = 0; i < 1000; i++) {
    // Simulate game logic
    renderer->process(input);
}

Integration Tests

See tests/integration/IT_014_ui_module_integration.cpp for complete example.


Troubleshooting

Common Issues

Module not loading

# Check module exports
nm -D build/modules/GameLogic.so | grep createModule
# Should show: createModule and destroyModule

# Check dependencies
ldd build/modules/GameLogic.so

IIO messages not received

// Verify subscription BEFORE publishing
io->subscribe("render:sprite");  // Must be before publish

// Check topic patterns
io->subscribe("render:*");  // Matches render:sprite, render:text
io->subscribe("render:sprite:*");  // Only matches render:sprite:batch

Hot-reload state loss

// Ensure ALL state is serialized in getState()
std::unique_ptr<IDataNode> MyModule::getState() {
    auto state = std::make_unique<JsonDataNode>("state");

    // DON'T FORGET any member variables!
    state->setInt("score", m_score);
    state->setDouble("playerX", m_playerX);
    state->setDouble("playerY", m_playerY);
    // ...

    return state;
}

Additional Resources


GroveEngine - Build modular, hot-reloadable games with ease 🌳