GroveEngine/docs/DEVELOPER_GUIDE.md
StillHammer a106c78bc8 feat: Retained mode rendering for UIModule
Implement retained mode rendering system to reduce IIO message traffic.
Widgets now register render entries that persist across frames and only
publish updates when visual state changes.

Core changes:
- UIWidget: Add dirty flags and render ID tracking
- UIRenderer: Add retained mode API (registerEntry, updateRect, updateText, updateSprite)
- SceneCollector: Add persistent sprite/text storage with add/update/remove handlers
- IIO protocol: New topics (render:sprite:add/update/remove, render:text:add/update/remove)

Widget migrations:
- UIPanel, UIButton, UILabel, UICheckbox, UISlider
- UIProgressBar, UITextInput, UIImage, UIScrollPanel

Documentation:
- docs/UI_RENDERING.md: Retained mode architecture
- modules/UIModule/README.md: Rendering modes section
- docs/DEVELOPER_GUIDE.md: Updated IIO topics

Performance: Reduces message traffic by 85-97% for static/mostly-static UIs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 14:06:28 +07:00

978 lines
27 KiB
Markdown

# GroveEngine - Developer Guide
**Comprehensive guide for building applications with GroveEngine**
## Table of Contents
1. [Getting Started](#getting-started)
2. [Core System](#core-system)
3. [Available Modules](#available-modules)
- [BgfxRenderer - 2D Rendering](#bgfxrenderer---2d-rendering)
- [UIModule - User Interface](#uimodule---user-interface)
- [InputModule - Input Handling](#inputmodule---input-handling)
4. [IIO Topics Reference](#iio-topics-reference)
5. [Complete Application Example](#complete-application-example)
6. [Building Your First Game](#building-your-first-game)
7. [Advanced Topics](#advanced-topics)
---
## Getting Started
### Prerequisites
- **C++17** compiler (GCC, Clang, or MSVC)
- **CMake** 3.20+
- **Git** for dependency management
### Quick Start
```bash
# 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
- **[USER_GUIDE.md](USER_GUIDE.md)** - Module system basics, hot-reload, IIO communication
- **[BgfxRenderer README](../modules/BgfxRenderer/README.md)** - 2D rendering module details
- **[InputModule README](../modules/InputModule/README.md)** - Input handling details
- **This document** - Complete integration guide and examples
---
## 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](USER_GUIDE.md#imodule) |
| **IIO** | Pub/Sub messaging | [USER_GUIDE.md](USER_GUIDE.md#iio) |
| **IDataNode** | Configuration & data | [USER_GUIDE.md](USER_GUIDE.md#idatanode) |
| **ModuleLoader** | Hot-reload system | [USER_GUIDE.md](USER_GUIDE.md#moduleloader) |
---
## Available Modules
### BgfxRenderer - 2D Rendering
**Status:** ✅ Production Ready (Phase 7-8 complete)
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
```cpp
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
```cpp
// 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
```cpp
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
```cpp
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](#rendering-topics)
---
### UIModule - User Interface
**Status:** ✅ Production Ready (Phase 7 complete)
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
```cpp
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
```cpp
// 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"
}
```
```cpp
// 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:
```cpp
// 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`:
```cpp
// 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](UI_RENDERING.md) for details.
**Full Topic Reference:** See [IIO Topics - UI Events](#ui-events)
---
### InputModule - Input Handling
**Status:** ✅ Production Ready (Phase 1 complete)
Cross-platform input handling with SDL2 backend (mouse, keyboard, gamepad).
#### Features
- Mouse (move, button, wheel)
- Keyboard (key events, text input)
- Thread-safe event buffering
- Multiple backend support (SDL2, extensible)
- Hot-reload support
#### Configuration
```cpp
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)
```cpp
// 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
```cpp
// 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](#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](UI_RENDERING.md) 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
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
```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
```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:
```bash
# 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](../tests/visual/README_FULL_STACK.md) for details.
---
## Building Your First Game
### Step-by-Step Tutorial
#### 1. Create Project Structure
```bash
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](#complete-application-example))
#### 3. Create UI Layout
`assets/ui/main_menu.json`:
```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
```bash
cmake -B build
cmake --build build -j4
./build/mygame
```
---
## Advanced Topics
### Hot-Reload Workflow
```bash
# 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
```cpp
// 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
```cpp
// 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
```cpp
// 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
```cpp
// 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
```cpp
// 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
```bash
# 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
```cpp
// 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
```cpp
// 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
- **[USER_GUIDE.md](USER_GUIDE.md)** - Core module system documentation
- **[BgfxRenderer README](../modules/BgfxRenderer/README.md)** - Renderer details
- **[InputModule README](../modules/InputModule/README.md)** - Input details
- **[CLAUDE.md](../CLAUDE.md)** - Development context for Claude Code
- **Integration Tests** - `tests/integration/IT_014_*.cpp`, `IT_015_*.cpp`
---
**GroveEngine - Build modular, hot-reloadable games with ease** 🌳