## Breaking Change
IIO API redesigned from manual pull+if-forest to callback dispatch.
All modules must update their subscribe() calls to pass handlers.
### Before (OLD API)
```cpp
io->subscribe("input:mouse");
void process(...) {
while (io->hasMessages()) {
auto msg = io->pullMessage();
if (msg.topic == "input:mouse") {
handleMouse(msg);
} else if (msg.topic == "input:keyboard") {
handleKeyboard(msg);
}
}
}
```
### After (NEW API)
```cpp
io->subscribe("input:mouse", [this](const Message& msg) {
handleMouse(msg);
});
void process(...) {
while (io->hasMessages()) {
io->pullAndDispatch(); // Callbacks invoked automatically
}
}
```
## Changes
**Core API (include/grove/IIO.h)**
- Added: `using MessageHandler = std::function<void(const Message&)>`
- Changed: `subscribe()` now requires `MessageHandler` callback parameter
- Changed: `subscribeLowFreq()` now requires `MessageHandler` callback
- Removed: `pullMessage()`
- Added: `pullAndDispatch()` - pulls and auto-dispatches to handlers
**Implementation (src/IntraIO.cpp)**
- Store callbacks in `Subscription.handler`
- `pullAndDispatch()` matches topic against ALL subscriptions (not just first)
- Fixed: Regex pattern compilation supports both wildcards (*) and regex (.*)
- Performance: ~1000 msg/s throughput (unchanged from before)
**Files Updated**
- 31 test/module files migrated to callback API (via parallel agents)
- 8 documentation files updated (DEVELOPER_GUIDE, USER_GUIDE, module READMEs)
## Bugs Fixed During Migration
1. **pullAndDispatch() early return bug**: Was only calling FIRST matching handler
- Fix: Loop through ALL subscriptions, invoke all matching handlers
2. **Regex pattern compilation bug**: Pattern "player:.*" failed to match
- Fix: Detect ".*" in pattern → use as regex, otherwise escape and convert wildcards
## Testing
✅ test_11_io_system: PASSED (IIO pub/sub, pattern matching, batching)
✅ test_threaded_module_system: 6/6 PASSED
✅ test_threaded_stress: 5/5 PASSED (50 modules, 100x reload, concurrent ops)
✅ test_12_datanode: PASSED
✅ 10 TopicTree scenarios: 10/10 PASSED
✅ benchmark_e2e: ~1000 msg/s throughput
Total: 23+ tests passing
## Performance Impact
No performance regression from callback dispatch:
- IIO throughput: ~1000 msg/s (same as before)
- ThreadedModuleSystem: Speedup ~1.0x (barrier pattern expected)
## Migration Guide
For all modules using IIO:
1. Update subscribe() calls to include handler lambda
2. Replace pullMessage() loops with pullAndDispatch()
3. Move topic-specific logic from if-forest into callbacks
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1074 lines
31 KiB
Markdown
1074 lines
31 KiB
Markdown
# 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](#current-limitations) below.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Current Limitations](#current-limitations)
|
|
2. [Getting Started](#getting-started)
|
|
3. [Core System](#core-system)
|
|
4. [Available Modules](#available-modules)
|
|
- [BgfxRenderer - 2D Rendering](#bgfxrenderer---2d-rendering)
|
|
- [UIModule - User Interface](#uimodule---user-interface)
|
|
- [InputModule - Input Handling](#inputmodule---input-handling)
|
|
5. [IIO Topics Reference](#iio-topics-reference)
|
|
6. [Complete Application Example](#complete-application-example)
|
|
7. [Building Your First Game](#building-your-first-game)
|
|
8. [Advanced Topics](#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
|
|
|
|
```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** | Pull-based pub/sub with callback dispatch | [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) |
|
|
|
|
#### IIO Callback Dispatch Pattern
|
|
|
|
GroveEngine uses a **pull-based callback dispatch** pattern for message processing:
|
|
|
|
```cpp
|
|
// OLD API (deprecated):
|
|
// io->subscribe("topic:pattern");
|
|
// while (io->hasMessages()) {
|
|
// auto msg = io->pullMessage();
|
|
// if (msg.topic == "topic:pattern") { /* handle */ }
|
|
// }
|
|
|
|
// NEW API (callback-based):
|
|
io->subscribe("topic:pattern", [this](const Message& msg) {
|
|
// Handle message - no if-forest needed
|
|
});
|
|
|
|
while (io->hasMessages()) {
|
|
io->pullAndDispatch(); // Callbacks invoked automatically
|
|
}
|
|
```
|
|
|
|
**Key advantages:**
|
|
- **No if-forest dispatch**: Register handlers at subscription, not in process loop
|
|
- **Module controls WHEN**: Pull-based processing for deterministic ordering
|
|
- **Callbacks handle HOW**: Clean separation of concerns
|
|
- **Thread-safe**: Callbacks invoked in module's thread context
|
|
|
|
---
|
|
|
|
## 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
|
|
|
|
```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:** ✅ 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
|
|
|
|
```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 with callbacks (in setConfiguration)
|
|
gameIO->subscribe("ui:action", [this](const grove::Message& msg) {
|
|
std::string action = msg.data->getString("action", "");
|
|
std::string widgetId = msg.data->getString("widgetId", "");
|
|
|
|
if (action == "start_game" && widgetId == "play_button") {
|
|
startGame();
|
|
}
|
|
});
|
|
|
|
// In process() - pull and dispatch to callbacks
|
|
while (gameIO->hasMessages() > 0) {
|
|
gameIO->pullAndDispatch(); // Callback invoked automatically
|
|
}
|
|
```
|
|
|
|
#### 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:** ✅ 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
|
|
|
|
```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 with Callbacks
|
|
|
|
```cpp
|
|
// Subscribe to input topics with callback handlers (in setConfiguration)
|
|
gameIO->subscribe("input:mouse:button", [this](const grove::Message& msg) {
|
|
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);
|
|
}
|
|
});
|
|
|
|
gameIO->subscribe("input:keyboard:key", [this](const grove::Message& msg) {
|
|
int scancode = msg.data->getInt("scancode", 0); // SDL_SCANCODE_*
|
|
bool pressed = msg.data->getBool("pressed", false);
|
|
|
|
if (scancode == SDL_SCANCODE_SPACE && pressed) {
|
|
playerJump();
|
|
}
|
|
});
|
|
|
|
// In process() - pull and auto-dispatch to callbacks
|
|
while (gameIO->hasMessages() > 0) {
|
|
gameIO->pullAndDispatch(); // Callbacks invoked automatically
|
|
}
|
|
```
|
|
|
|
**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 with callback handlers
|
|
m_io->subscribe("ui:action", [this](const grove::Message& msg) {
|
|
std::string action = msg.data->getString("action", "");
|
|
if (action == "start_game") {
|
|
startGame();
|
|
}
|
|
});
|
|
|
|
m_io->subscribe("ui:click", [this](const grove::Message& msg) {
|
|
std::string widgetId = msg.data->getString("widgetId", "");
|
|
double x = msg.data->getDouble("x", 0.0);
|
|
double y = msg.data->getDouble("y", 0.0);
|
|
handleClick(widgetId, x, y);
|
|
});
|
|
}
|
|
|
|
void process(const grove::IDataNode& input) override {
|
|
double deltaTime = input.getDouble("deltaTime", 0.016);
|
|
|
|
// Process UI events - pull and auto-dispatch to callbacks
|
|
while (m_io->hasMessages() > 0) {
|
|
m_io->pullAndDispatch(); // Callbacks invoked automatically
|
|
}
|
|
|
|
// 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: Subscribe to response first (in setConfiguration)
|
|
moduleA_io->subscribe("pathfinding:response", [this](const grove::Message& msg) {
|
|
std::string requestId = msg.data->getString("requestId", "");
|
|
// ... apply path result ...
|
|
});
|
|
|
|
// Module A: Request pathfinding (in process)
|
|
auto request = std::make_unique<JsonDataNode>("request");
|
|
request->setString("requestId", "path_123");
|
|
request->setDouble("startX", 10.0);
|
|
request->setDouble("startY", 20.0);
|
|
moduleA_io->publish("pathfinding:request", std::move(request));
|
|
|
|
// Module B: Subscribe to request (in setConfiguration)
|
|
moduleB_io->subscribe("pathfinding:request", [this](const grove::Message& msg) {
|
|
std::string requestId = msg.data->getString("requestId", "");
|
|
// ... compute path ...
|
|
|
|
auto response = std::make_unique<JsonDataNode>("response");
|
|
response->setString("requestId", requestId);
|
|
// ... add path data ...
|
|
m_io->publish("pathfinding:response", std::move(response));
|
|
});
|
|
|
|
// Module A/B: In process() - pull and dispatch
|
|
while (io->hasMessages() > 0) {
|
|
io->pullAndDispatch(); // Callbacks invoked automatically
|
|
}
|
|
```
|
|
|
|
#### 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 (in setConfiguration)
|
|
analyticsIO->subscribe("combat:*", [this](const grove::Message& msg) {
|
|
aggregateCombatEvent(msg);
|
|
});
|
|
|
|
// In process()
|
|
while (analyticsIO->hasMessages() > 0) {
|
|
analyticsIO->pullAndDispatch(); // Callback invoked for each event
|
|
}
|
|
```
|
|
|
|
### 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 with callback BEFORE publishing (in setConfiguration)
|
|
io->subscribe("render:sprite", [this](const grove::Message& msg) {
|
|
handleSprite(msg);
|
|
});
|
|
|
|
// Check topic patterns
|
|
io->subscribe("render:*", [this](const grove::Message& msg) {
|
|
// Matches render:sprite, render:text, etc.
|
|
});
|
|
|
|
io->subscribe("render:sprite:*", [this](const grove::Message& msg) {
|
|
// Only matches render:sprite:batch, render:sprite:add, etc.
|
|
});
|
|
|
|
// Remember to pullAndDispatch in process()
|
|
while (io->hasMessages() > 0) {
|
|
io->pullAndDispatch();
|
|
}
|
|
```
|
|
|
|
#### 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** 🌳
|