1. Executive Summary
This document outlines the design for EmberMonitor, a real-time behavior tree monitoring application that complements EmberForge (the XML editor). While EmberForge edits behavior tree XML files, EmberMonitor receives tree structure and execution state over TCP from external systems (games, robots, simulations) and visualizes them in real-time.
1.1 Goals
| Goal | Description |
| Live Monitoring | Visualize behavior tree execution in real-time |
| Zero XML Dependency | Tree structure received entirely over TCP |
| Code Reuse | Maximize sharing with EmberForge via EmberCore |
| Low Latency | Minimal delay between tick and visualization update |
| Multi-Tree Support | Monitor multiple trees from multiple sources |
| Easy Integration | FlatBuffers schema + reference client for any language |
1.2 Non-Goals
- EmberMonitor will NOT edit trees (read-only visualization)
- EmberMonitor will NOT save/load XML files
- EmberMonitor will NOT execute behavior trees (only visualize)
2. System Architecture
2.1 High-Level Overview
┌──────────────────────────────────────────────────────────────────────────────┐
│ EmberCore (Shared Library) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Core │ │ Parsers │ │ Adapters │ │ Interfaces │ │
│ │ Node, Tree, │ │ XML (Forge) │ │ DirectTree │ │ ITreeNode │ │
│ │ Blackboard │ │ │ │ NodeAdapter │ │ ITreeStruct │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └───────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Network Module │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌────────────┐ │ │
│ │ │ FlatBuffers │ │ FlatBuffer │ │ TreeBuilder │ │TickStateMgr│ │ │
│ │ │ Schema │ │ Codec │ │ (Construct) │ │ (State) │ │ │
│ │ └─────────────┘ └──────────────┘ └─────────────┘ └────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ EmberUI (Shared UI Library) │
│ ┌────────────────┐ ┌──────────────────────┐ ┌─────────────────────────┐ │
│ │ Components │ │ Tabs │ │ Visualization │ │
│ │ Panel │ │ NavigatorTab │ │ TreeCanvas │ │
│ │ SidePanel │ │ PropertiesTabBase │ │ StatusOverlay │ │
│ └────────────────┘ │ TreeHierarchyTab │ └─────────────────────────┘ │
│ ┌────────────────┐ └──────────────────────┘ │
│ │ Interfaces │ │
│ │ ITab, IPanel │ │
│ │ IScene │ │
│ └────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────┐ ┌─────────────────────────────────────┐
│ EmberForge │ │ EmberMonitor │
│ (Edit Mode) │ │ (Monitor Mode) │
├─────────────────────────────────────┤ ├─────────────────────────────────────┤
│ • XML Load/Save │ │ • TCP Server (accept connections) │
│ • Full Editing UI │ │ • Real-time State Visualization │
│ • DirectTreeAdapter │ │ • TCPTreeAdapter │
│ • Node Creation/Deletion │ │ • Read-Only Visualization │
│ • Property Editing │ │ • MonitorTreeCanvas │
│ • Blackboard Editing │ │ • Navigator + Properties Tabs │
└─────────────────────────────────────┘ └─────────────────────────────────────┘
2.2 Component Responsibilities
| Component | Location | Responsibility |
| EmberCore | Shared | Core data structures, interfaces, adapters |
| Network Module | EmberCore (new) | FlatBuffers schema, codec, tree building |
| EmberForge | Standalone App | XML editing, full tree manipulation |
| EmberMonitor | Standalone App | TCP reception, read-only visualization |
| Client SDK | Distributed | Schema file + C++ reference client for user integration |
2.3 Shared vs. Separate Code
Shared (EmberCore):
├── Core/
│ ├── Node.h/.cpp # Reused as-is
│ ├── BehaviorTree.h/.cpp # Reused as-is
│ ├── Blackboard.h/.cpp # Reused as-is
│ └── BehaviorTreeValidator.h/.cpp # Reused as-is
├── Interfaces/
│ ├── ITreeNode.h # Reused as-is
│ ├── ITreeStructure.h # Reused as-is
│ └── ITreeFactory.h # Reused as-is
├── Adapters/
│ ├── NodeAdapter.h/.cpp # Reused as-is
│ └── DirectTreeAdapter.h/.cpp # Reused as-is
├── Types/ # Reused as-is
├── Utils/ # Reused as-is
└── Network/ (NEW)
├── Schema/
│ ├── ember_protocol.fbs # FlatBuffers schema (source of truth)
│ └── generated/ # Auto-generated from schema
├── Codec/
└── Builder/
EmberUI (Shared UI Library):
├── Interfaces/
│ ├── ITab.h # Tab interface
│ ├── IPanel.h # Panel interface
│ └── IScene.h # Scene interface
├── Components/
│ ├── Panel.h/.cpp # Base panel class
│ ├── SidePanel.h/.cpp # Tabbed side panel
│ └── ScalableDialog.h/.cpp # DPI-aware dialog
├── Tabs/
│ ├── NavigatorTab.h/.cpp # Tree list + hierarchy + search
│ ├── PropertiesTabBase.h/.cpp # Shared property editor base
│ └── TreeHierarchyTab.h/.cpp # Tree hierarchy view
├── Visualization/
│ ├── TreeCanvas.h/.cpp # Shared tree rendering canvas
│ └── StatusOverlay.h/.cpp # Status colors and IStatusProvider
└── Utils/
├── DPI.h # DPI scaling utilities
├── LayoutConstants.h # Layout constants
└── ProportionalLayout.h/.cpp # Proportional AUI layout
EmberForge-Only:
├── Parsers/
│ ├── LibXMLBehaviorTreeParser # XML-specific
│ └── LibXMLBehaviorTreeSerializer # XML-specific
└── [All current UI code]
EmberMonitor-Only (NEW):
├── App/
│ ├── MonitorApp.h/.cpp
│ ├── MonitorFrame.h/.cpp
│ └── MonitorConstants.h
├── Network/
│ └── TCPServer.h/.cpp # TCP server (accept connections)
├── Panels/
│ ├── ConnectionPanel.h/.cpp
│ ├── MainPanel.h/.cpp
│ └── RightSidePanel.h/.cpp
├── Tabs/
│ ├── MonitorNavigatorTab.h/.cpp # Navigator (extends NavigatorTab)
│ └── PropertiesTab.h/.cpp # Properties (extends PropertiesTabBase)
└── Visualization/
└── MonitorTreeCanvas.h/.cpp # Monitor tree canvas (extends TreeCanvas)
Client SDK (Distributed):
├── README.md # Integration guide
├── ember_protocol.fbs # Schema file (source of truth)
└── cpp/
├── EmberClient.h # Header-only reference client
├── flatbuffers/ # FlatBuffers runtime headers
└── example/
├── CMakeLists.txt
└── main.cpp # Minimal usage example
3. Protocol Specification (FlatBuffers)
3.1 Why FlatBuffers
| Requirement | FlatBuffers Advantage |
| Performance | Zero-copy deserialization, minimal CPU overhead |
| Low Latency | No parsing step - direct memory access |
| Multi-Language | Auto-generates C++, C#, Python, Rust, Go, Java, etc. |
| No Runtime Deps | Header-only for C++, no .dll/.so required |
| Easy Integration | Users just include generated files |
3.2 Protocol Overview
The protocol uses FlatBuffers binary serialization over TCP with a simple framing layer.
┌──────────────────────────────────────────────────────────────────┐
│ Message Frame │
├──────────────┬──────────────┬────────────────────────────────────┤
│ Length (4B) │ Type (1B) │ FlatBuffer Payload │
│ Little-End │ MessageType │ (Binary) │
└──────────────┴──────────────┴────────────────────────────────────┘
Frame Header (5 bytes):
- Bytes 0-3: Payload length (little-endian uint32)
- Byte 4: Message type enum
3.3 FlatBuffers Schema
The complete protocol is defined in a single schema file:
ember_protocol.fbs
// EmberMonitor Protocol Schema
// Version: 1.0.0
namespace Ember.Protocol;
// ============================================================================
// Enumerations
// ============================================================================
enum MessageType : ubyte {
Handshake = 1,
HandshakeAck = 2,
TreeInit = 16, // 0x10
TreeInitAck = 17, // 0x11
TickUpdate = 32, // 0x20
TickUpdateBatch = 33, // 0x21
BlackboardUpdate = 48,// 0x30
TreeReset = 64, // 0x40
Disconnect = 240, // 0xF0
Error = 255 // 0xFF
}
enum NodeStatus : ubyte {
Idle = 0,
Running = 1,
Success = 2,
Failure = 3,
Halted = 4
}
enum NodeType : ubyte {
Action = 0,
Control = 1,
Condition = 2,
Decorator = 3,
SubTree = 4
}
enum ErrorCode : ushort {
None = 0,
ProtocolMismatch = 1,
InvalidMessage = 2,
UnknownTree = 3,
UnknownNode = 4,
InternalError = 500
}
// ============================================================================
// Common Structures
// ============================================================================
table KeyValue {
key: string (required);
value: string;
}
// ============================================================================
// Handshake Messages
// ============================================================================
table Handshake {
version: string (required); // Protocol version "1.0"
client_id: string (required); // Unique client identifier
client_name: string; // Human-readable name
capabilities: [string]; // Supported features
}
table HandshakeAck {
version: string (required); // Monitor's protocol version
session_id: string (required); // Assigned session ID
accepted: bool = true;
error: string; // Error message if not accepted
}
// ============================================================================
// Tree Structure Messages
// ============================================================================
table NodeDefinition {
id: int64; // Unique node ID within tree
node_type: NodeType; // Action, Control, etc.
subtype: string (required); // "Sequence", "Attack", etc.
name: string (required); // Node instance name
description: string; // Human-readable description
attributes: [KeyValue]; // Custom attributes
children: [NodeDefinition]; // Child nodes (recursive)
}
table BlackboardEntry {
key: string (required);
value_type: string; // Type for display ("int", "float", etc.)
value: string; // Serialized value
description: string;
}
table BlackboardDefinition {
id: string (required); // Blackboard identifier
name: string; // Human-readable name
entries: [BlackboardEntry];
}
table TreeInit {
tree_id: string (required); // Unique tree identifier
tree_name: string; // Human-readable name
root: NodeDefinition (required); // Root node (contains full tree)
blackboards: [BlackboardDefinition];
metadata: [KeyValue]; // Additional metadata
}
table TreeInitAck {
tree_id: string (required);
success: bool = true;
node_count: int32; // Number of nodes constructed
error: string;
}
// ============================================================================
// Tick Update Messages
// ============================================================================
table NodeState {
id: int64; // Node ID
status: NodeStatus; // Current status
last_result: NodeStatus; // Previous tick result
tick_count: int64; // Times this node was ticked
message: string; // Debug message from node
}
table TickUpdate {
tree_id: string (required);
tick_number: int64;
tick_timestamp_ms: int64; // Unix timestamp in milliseconds
delta_time_ms: float; // Time since last tick
is_delta: bool = false; // True = only changed states included
states: [NodeState] (required);
execution_path: [int64]; // Ordered list of executed node IDs
}
table TickUpdateBatch {
tree_id: string (required);
ticks: [TickUpdate] (required); // Multiple ticks for replay/recording
}
// ============================================================================
// Blackboard Update Messages
// ============================================================================
table BlackboardUpdateEntry {
key: string (required);
value: string;
previous_value: string;
}
table BlackboardUpdate {
tree_id: string (required);
blackboard_id: string (required);
tick_number: int64;
updates: [BlackboardUpdateEntry];
}
// ============================================================================
// Control Messages
// ============================================================================
table TreeReset {
tree_id: string (required);
tick_number: int64; // Reset tick counter to this value
reason: string; // "level_restart", "manual", etc.
}
table Disconnect {
reason: string;
message: string;
}
table Error {
code: ErrorCode;
message: string;
fatal: bool = false; // If true, connection will be closed
}
// ============================================================================
// Root Types (for verification)
// ============================================================================
root_type TickUpdate; // Most common message, used for default
3.4 Message Types Summary
| Type ID | Message | Direction | Purpose |
| 0x01 | Handshake | Client → Monitor | Initial connection |
| 0x02 | HandshakeAck | Monitor → Client | Acknowledge connection |
| 0x10 | TreeInit | Client → Monitor | Send full tree structure |
| 0x11 | TreeInitAck | Monitor → Client | Acknowledge tree received |
| 0x20 | TickUpdate | Client → Monitor | Node state changes |
| 0x21 | TickUpdateBatch | Client → Monitor | Multiple ticks (replay) |
| 0x30 | BlackboardUpdate | Client → Monitor | Blackboard changes |
| 0x40 | TreeReset | Client → Monitor | Tree reset notification |
| 0xF0 | Disconnect | Either | Graceful disconnect |
| 0xFF | Error | Either | Error notification |
3.5 Protocol State Machine
┌─────────────┐
│ DISCONNECTED│
└──────┬──────┘
│ TCP Connect
▼
┌─────────────┐ Handshake ┌─────────────────┐
│ CONNECTED │──────────────▶│ AWAITING_ACK │
└─────────────┘ └────────┬────────┘
│ HandshakeAck
▼
┌─────────────────┐
│ AUTHENTICATED │
└────────┬────────┘
│ TreeInit
▼
┌─────────────────┐
│ AWAITING_TREE │
└────────┬────────┘
│ TreeInitAck
▼
┌─────────────────┐
│ ACTIVE │◀──┐
└────────┬────────┘ │
│ │
┌──────────────────┼────────────┤
│ │ │
▼ ▼ │
TickUpdate BlackboardUpdate │
│ │ │
└──────────────────┴────────────┘
│
│ Disconnect / Error (fatal)
▼
┌─────────────────┐
│ DISCONNECTED │
└─────────────────┘
3.6 Binary Wire Format Examples
3.6.1 TickUpdate Message (Most Common)
Wire format for a TickUpdate with 3 node states:
Header (5 bytes):
┌────────────────────────────────────────┬─────────┐
│ 00 00 00 4C │ 20 │
│ Length = 76 bytes (little-endian) │ Type=32 │
└────────────────────────────────────────┴─────────┘
Payload (76 bytes):
┌─────────────────────────────────────────────────────────────┐
│ FlatBuffer binary data │
│ - tree_id offset → "player_ai" │
│ - tick_number = 1542 │
│ - states vector → [ │
│ {id:1, status:Running, tick_count:1542}, │
│ {id:2, status:Success, tick_count:45}, │
│ {id:3, status:Idle, tick_count:45} │
│ ] │
│ - execution_path → [1, 2] │
└─────────────────────────────────────────────────────────────┘
Total: 81 bytes (vs ~400 bytes for equivalent JSON)
3.6.2 Size Comparison
| Message Type | JSON Size | FlatBuffer Size | Reduction |
| TickUpdate (10 nodes) | ~800 bytes | ~150 bytes | 81% |
| TickUpdate (100 nodes) | ~8 KB | ~1.2 KB | 85% |
| TreeInit (50 nodes) | ~15 KB | ~3 KB | 80% |
| Handshake | ~200 bytes | ~80 bytes | 60% |
3.7 Bandwidth Optimization
Delta Updates
By default, TickUpdate sends all node states. For high-frequency updates, use delta mode:
TickUpdate update;
update.is_delta = true;
update.states = GetChangedStatesOnly();
Monitor tracks last known state and applies deltas.
Batched Updates
For replay or recording, batch multiple ticks:
TickUpdateBatch batch;
batch.tree_id = "player_ai";
batch.ticks = CollectTicksForTimeRange(start, end);
Note: The code samples below were written during initial design. The actual implementation uses Ember::Network namespace for EmberCore network classes and Ember::Monitor namespace for EmberMonitor classes. Class name TCPConnectionManager was implemented as TCPServer in the Ember::Monitor namespace.
4.1 New Network Module Structure
EmberCore/
├── include/
│ └── Network/
│ ├── Schema/
│ │ ├── ember_protocol.fbs # Schema source file
│ │ └── generated/
│ │ └── ember_protocol_generated.h # Auto-generated C++
│ ├── Codec/
│ │ ├── MessageFrame.h # Frame structure
│ │ └── FlatBufferCodec.h # Encode/decode helpers
│ ├── Builder/
│ │ ├── TreeBuilder.h # Build tree from TreeInit
│ │ └── StateManager.h # Manage node states
│ └── Adapter/
│ └── TCPTreeAdapter.h # ITreeStructure for TCP
├── src/
│ └── Network/
│ ├── Codec/
│ │ └── FlatBufferCodec.cpp
│ ├── Builder/
│ │ ├── TreeBuilder.cpp
│ │ └── StateManager.cpp
│ └── Adapter/
│ └── TCPTreeAdapter.cpp
└── third_party/
└── flatbuffers/
└── flatbuffers.h # Header-only runtime (~50KB)
4.2 Key Classes
#pragma once
#include <cstdint>
#include <vector>
namespace ember::network {
struct MessageFrame {
static constexpr size_t HEADER_SIZE = 5;
uint32_t payload_length;
std::vector<uint8_t> payload;
std::vector<uint8_t> ToBytes() const {
std::vector<uint8_t> result(HEADER_SIZE + payload.size());
result[0] = payload_length & 0xFF;
result[1] = (payload_length >> 8) & 0xFF;
result[2] = (payload_length >> 16) & 0xFF;
result[3] = (payload_length >> 24) & 0xFF;
result[4] = static_cast<uint8_t>(message_type);
std::copy(payload.begin(), payload.end(), result.begin() + HEADER_SIZE);
return result;
}
static bool ParseHeader(const uint8_t* data, size_t size,
uint32_t& out_length,
if (size < HEADER_SIZE) return false;
out_length = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
return true;
}
};
}
#pragma once
#include "flatbuffers/flatbuffers.h"
#include <memory>
namespace ember::network {
class FlatBufferCodec {
public:
static MessageFrame BuildHandshake(
const std::string& version,
const std::string& client_id,
const std::string& client_name = "",
const std::vector<std::string>& capabilities = {});
static MessageFrame BuildHandshakeAck(
const std::string& version,
const std::string& session_id,
bool accepted,
const std::string& error = "");
static MessageFrame BuildTreeInit(
const std::string& tree_id,
const std::string& tree_name,
flatbuffers::FlatBufferBuilder& builder,
flatbuffers::Offset<Ember::Protocol::NodeDefinition> root);
static MessageFrame BuildTickUpdate(
const std::string& tree_id,
int64_t tick_number,
const std::vector<std::pair<int64_t, Ember::Protocol::NodeStatus>>& states,
const std::vector<int64_t>& execution_path = {},
bool is_delta = false);
static MessageFrame BuildError(
const std::string& message,
bool fatal = false);
template<typename T>
static bool Verify(const std::vector<uint8_t>& payload) {
flatbuffers::Verifier verifier(payload.data(), payload.size());
return verifier.VerifyBuffer<T>(nullptr);
}
template<typename T>
static const T* GetRoot(const std::vector<uint8_t>& payload) {
return flatbuffers::GetRoot<T>(payload.data());
}
template<typename T>
static const T* VerifyAndGetRoot(const std::vector<uint8_t>& payload) {
if (!Verify<T>(payload)) return nullptr;
return GetRoot<T>(payload);
}
};
}
#pragma once
#include <memory>
namespace ember::network {
class TreeBuilder {
public:
struct BuildResult {
bool success = false;
std::shared_ptr<BehaviorTree> tree;
int node_count = 0;
std::string error;
};
BuildResult Build(const Ember::Protocol::TreeInit* tree_init);
private:
std::unique_ptr<Node> BuildNode(
const Ember::Protocol::NodeDefinition* def,
Node* parent);
void BuildBlackboards(
BehaviorTree* tree,
const flatbuffers::Vector<
flatbuffers::Offset<Ember::Protocol::BlackboardDefinition>>* blackboards);
};
}
#pragma once
#include <unordered_map>
#include <functional>
#include <mutex>
#include <vector>
namespace ember::network {
class StateManager {
public:
using StateChangeCallback = std::function<void(int64_t node_id,
NodeStatus old_status,
NodeStatus new_status)>;
using TickCallback = std::function<void(int64_t tick_number)>;
explicit StateManager(std::shared_ptr<BehaviorTree> tree);
void ApplyTickUpdate(const Ember::Protocol::TickUpdate* update);
void ApplyBlackboardUpdate(const Ember::Protocol::BlackboardUpdate* update);
void Reset();
NodeStatus GetNodeStatus(int64_t node_id) const;
int64_t GetCurrentTick() const { return current_tick_; }
const std::vector<int64_t>& GetExecutionPath() const { return execution_path_; }
struct TickSnapshot {
int64_t tick_number;
int64_t timestamp_ms;
std::unordered_map<int64_t, NodeStatus> states;
std::vector<int64_t> execution_path;
};
void EnableHistory(size_t max_ticks = 1000);
void DisableHistory();
const std::vector<TickSnapshot>& GetHistory() const { return history_; }
void SeekToTick(int64_t tick_number);
void AddStateChangeCallback(StateChangeCallback callback);
void AddTickCallback(TickCallback callback);
private:
std::shared_ptr<BehaviorTree> tree_;
std::unordered_map<int64_t, NodeStatus> node_states_;
std::vector<int64_t> execution_path_;
int64_t current_tick_ = 0;
bool history_enabled_ = false;
size_t max_history_ticks_ = 1000;
std::vector<TickSnapshot> history_;
std::vector<StateChangeCallback> state_callbacks_;
std::vector<TickCallback> tick_callbacks_;
mutable std::mutex mutex_;
void NotifyStateChange(int64_t node_id, NodeStatus old_s, NodeStatus new_s);
void NotifyTick(int64_t tick_number);
};
}
#pragma once
#include <memory>
#include <unordered_map>
namespace ember::network {
class TCPTreeAdapter : public ITreeStructure {
public:
TCPTreeAdapter(std::shared_ptr<BehaviorTree> tree,
std::shared_ptr<StateManager> state_manager);
ITreeNode* GetRoot() override;
ITreeNode* FindNode(size_t id) override;
void TraverseDepthFirst(std::function<bool(ITreeNode*)> visitor) override;
void TraverseBreadthFirst(std::function<bool(ITreeNode*)> visitor) override;
bool Validate() const override;
size_t GetNodeCount() const override;
NodeStatus GetNodeStatus(size_t node_id) const;
bool IsNodeInExecutionPath(size_t node_id) const;
int64_t GetCurrentTick() const;
std::shared_ptr<StateManager> GetStateManager() { return state_manager_; }
private:
std::shared_ptr<BehaviorTree> tree_;
std::shared_ptr<StateManager> state_manager_;
std::unordered_map<size_t, std::unique_ptr<NodeAdapter>> adapters_;
NodeAdapter* root_adapter_ = nullptr;
void BuildAdapterCache();
NodeAdapter* GetOrCreateAdapter(Node* node);
};
}
5. Client Integration
5.1 Overview
To send behavior tree data to EmberMonitor, you generate protocol bindings for your language using the FlatBuffers compiler (flatc) and implement a simple TCP client wrapper. This approach:
- Works with any language FlatBuffers supports (C++, C#, Python, Rust, Go, Java, TypeScript, Swift, etc.)
- No external dependencies to maintain - you generate code once and own it
- Full control over integration with your existing codebase
What you need:
1. ember_protocol.fbs # Schema file (from client_sdk/ or repository)
2. flatc compiler # One-time install to generate code
3. ~100 lines of code # TCP connection + message framing
5.2 Quick Start
Step 1: Install FlatBuffers Compiler
# Ubuntu/Debian
sudo apt install flatbuffers-compiler
# macOS
brew install flatbuffers
# Windows - download from https://github.com/google/flatbuffers/releases
# Add flatc.exe to PATH
# Verify
flatc --version
Step 2: Get the Schema
# Copy from client_sdk/ in the Ember repository
cp client_sdk/ember_protocol.fbs .
Step 3: Generate Code for Your Language
# C++
flatc --cpp -o generated/ ember_protocol.fbs
# C#
flatc --csharp -o Generated/ ember_protocol.fbs
# Python
flatc --python -o generated/ ember_protocol.fbs
# Other languages - see Section 5.3
Step 4: Add FlatBuffers Runtime
| Language | Runtime |
| C++ | Header-only (included in client_sdk/cpp/) |
| C# | NuGet: Google.FlatBuffers |
| Python | pip install flatbuffers |
| Rust | cargo add flatbuffers |
| Go | go get github.com/google/flatbuffers/go |
| Java | Maven: com.google.flatbuffers:flatbuffers-java |
| TypeScript | npm install flatbuffers |
Step 5: Implement Client Wrapper
You need to implement:
- TCP socket connection to EmberMonitor
- Message framing (5-byte header: 4-byte length + 1-byte type)
- Handshake → TreeInit → TickUpdate message sequence
See Section 5.4 for a complete C++ reference implementation.
5.3 Supported Languages
FlatBuffers supports code generation for many languages:
| Language | Command | Runtime |
| C++ | flatc --cpp | Header-only |
| C# | flatc --csharp | NuGet: Google.FlatBuffers |
| Python | flatc --python | pip: flatbuffers |
| Rust | flatc --rust | crates.io: flatbuffers |
| Go | flatc --go | github.com/google/flatbuffers/go |
| Java | flatc --java | Maven: com.google.flatbuffers |
| Kotlin | flatc --kotlin | Maven: com.google.flatbuffers |
| TypeScript | flatc --ts | npm: flatbuffers |
| Swift | flatc --swift | SPM: FlatBuffers |
| Dart | flatc --dart | pub.dev: flat_buffers |
| Lua | flatc --lua | Community runtime |
| PHP | flatc --php | Composer |
5.4 C++ Reference Client
A complete, working C++ reference client is provided in client_sdk/cpp/. This demonstrates:
- Header-only implementation (EmberClient.h)
- TCP socket connection (POSIX)
- Message framing
- Full protocol message sequence
Directory Structure:
client_sdk/
├── README.md # Integration guide
├── ember_protocol.fbs # Schema file
└── cpp/
├── EmberClient.h # Header-only reference client
├── flatbuffers/ # FlatBuffers runtime headers
└── example/
├── CMakeLists.txt
└── main.cpp # Minimal usage example
Basic Usage:
#include "EmberClient.h"
int main() {
ember::EmberClient client;
if (!client.Connect("127.0.0.1", 12345)) {
return 1;
}
client.SendTreeInit("main_ai", BuildMyTree());
int64_t tick = 0;
while (running) {
myTree.Tick();
std::vector<ember::NodeStateInfo> states;
for (auto& node : myTree.GetNodes()) {
states.push_back({node.id, node.status});
}
client.SendTickUpdate("main_ai", tick++, states);
}
client.Disconnect();
return 0;
}
5.5 Integration Examples
C# (Unity)
using FlatBuffers;
using System.Net.Sockets;
public class EmberClient {
private TcpClient _client;
private NetworkStream _stream;
public void Connect(string host, int port) {
_client = new TcpClient(host, port);
_stream = _client.GetStream();
SendHandshake();
}
public void SendTickUpdate(string treeId, long tick, List<NodeState> states) {
var builder = new FlatBufferBuilder(1024);
SendFrame(
MessageType.TickUpdate, builder.SizedByteArray());
}
private void SendFrame(MessageType type, byte[] payload) {
var header = new byte[5];
BitConverter.GetBytes((uint)payload.Length).CopyTo(header, 0);
header[4] = (byte)type;
_stream.Write(header, 0, 5);
_stream.Write(payload, 0, payload.Length);
}
}
Python
import socket
import struct
import flatbuffers
from ember.protocol import *
class EmberClient:
def __init__(self):
self.sock = None
self.tick = 0
def connect(self, host, port):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((host, port))
self._send_handshake()
def send_tick_update(self, tree_id, states):
builder = flatbuffers.Builder(1024)
self._send_frame(MessageType.TickUpdate, builder.Output())
self.tick += 1
def _send_frame(self, msg_type, payload):
header = struct.pack('<IB', len(payload), msg_type)
self.sock.sendall(header + bytes(payload))
Rust
use flatbuffers::FlatBufferBuilder;
use std::io::Write;
use std::net::TcpStream;
pub struct EmberClient {
stream: Option<TcpStream>,
}
impl EmberClient {
pub fn connect(&mut self, addr: &str) -> std::io::Result<()> {
self.stream = Some(TcpStream::connect(addr)?);
self.send_handshake()
}
fn send_frame(&mut self, msg_type: u8, payload: &[u8]) -> std::io::Result<()> {
if let Some(ref mut stream) = self.stream {
stream.write_all(&(payload.len() as u32).to_le_bytes())?;
stream.write_all(&[msg_type])?;
stream.write_all(payload)?;
}
Ok(())
}
}
5.6 Message Flow
Client EmberMonitor
│ │
│──────── Handshake ─────────────────►│
│◄─────── HandshakeAck ───────────────│
│ │
│──────── TreeInit ──────────────────►│ (send full tree structure)
│◄─────── TreeInitAck ────────────────│
│ │
│──────── TickUpdate ────────────────►│ ┐
│──────── TickUpdate ────────────────►│ │ repeat every tick
│──────── TickUpdate ────────────────►│ ┘
│ │
│──────── Disconnect ────────────────►│ (optional graceful close)
│ │
5.7 Troubleshooting
| Problem | Solution |
| Connection refused | Ensure EmberMonitor is running and listening on the correct port |
| No tree appears | Verify TreeInit message is sent before TickUpdates |
| Garbled data | Check byte order (little-endian) and frame header format |
| flatc not found | Add flatc to PATH or specify full path |
| Generated code errors | Ensure flatc version matches runtime version |
5.8 Integration Effort
| Task | Time |
| Install flatc | 5 min |
| Generate code | 5 min |
| Add runtime dependency | 5 min |
| Implement client wrapper | 1-2 hours |
| Total | **~2 hours** |
6. EmberMonitor Application
6.1 Application Structure
EmberMonitor/
├── CMakeLists.txt
├── include/
│ ├── App/
│ │ ├── MonitorApp.h
│ │ ├── MonitorFrame.h
│ │ └── MonitorConstants.h
│ ├── Network/
│ │ └── TCPServer.h
│ ├── Panels/
│ │ ├── ConnectionPanel.h
│ │ ├── MainPanel.h
│ │ └── RightSidePanel.h
│ ├── Tabs/
│ │ ├── MonitorNavigatorTab.h
│ │ └── PropertiesTab.h
│ └── Visualization/
│ └── MonitorTreeCanvas.h
└── src/
└── [corresponding .cpp files]
6.2 MonitorFrame Layout
┌──────────────────────────────────────────────────────────────────────────────┐
│ EmberMonitor [─][□][×] │
├──────────────────────────────────────────────────────────────────────────────┤
│ File View Connection Help │
├──────────────────────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌───────────────────────────────────────┐ ┌──────────────┐ │
│ │ Connection │ │ ┌─────────────────┐ │ │ [Navigator] │ │
│ │ Panel │ │ │ Search Bar │ │ │ [Properties] │ │
│ ├──────────────┤ │ └─────────────────┘ │ ├──────────────┤ │
│ │ Port: 12345 │ │ │ │ ▼ Root │ │
│ │ [Start] │ │ │ │ ▼ Sequence │ │
│ │ │ │ Tree Visualization │ │ ● Act1 │ │
│ │ Status: │ │ │ │ ○ Act2 │ │
│ │ ● Connected │ │ (MainPanel with │ │ │ │
│ │ │ │ MonitorTreeCanvas) │ │ -- or -- │ │
│ │ Client: │ │ │ │ │ │
│ │ GameAI │ │ [Status overlays on nodes] │ │ Node: ... │ │
│ │ v1.0 │ │ │ │ Type: ... │ │
│ │ │ │ │ │ Status: │ │
│ │ │ │ │ │ RUNNING │ │
│ └──────────────┘ └───────────────────────────────────────┘ └──────────────┘ │
├──────────────────────────────────────────────────────────────────────────────┤
│ StatusBar: ● Connected | Tick Rate: 60/s | Latency: 2ms │
└──────────────────────────────────────────────────────────────────────────────┘
6.3 Key UI Components
6.3.1 StatusOverlay
Implementation note: The StatusOverlay class (including IStatusProvider interface and StatusColors) was implemented in EmberUI, not EmberMonitor. It lives in EmberUI/include/Visualization/StatusOverlay.h and is shared across applications. The original design below placed it in EmberMonitor, but during implementation it was generalized into the shared UI library.
#pragma once
namespace ember::monitor {
class StatusOverlay {
public:
struct StatusColors {
Color idle = Color(128, 128, 128);
Color running = Color(65, 105, 225);
Color success = Color(50, 205, 50);
Color failure = Color(220, 20, 60);
Color halted = Color(255, 165, 0);
};
};
}
6.3.2 ConnectionManager
Implementation note: The ConnectionManager class shown in the original design was not implemented as a separate class. Instead, MonitorFrame directly owns and manages a single TCPServer instance (in the Ember::Monitor namespace). The single-client connection model made a dedicated manager unnecessary — MonitorFrame handles server start/stop, connection events, and message dispatch directly.
7. Implementation Phases
Phase 1: Protocol Foundation (EmberCore)
Goal: Implement FlatBuffers protocol layer in EmberCore
Tasks:
- Add FlatBuffers header to third_party/
- Create ember_protocol.fbs schema
- Generate C++ code from schema
- Implement MessageFrame and FlatBufferCodec
- Write unit tests for encoding/decoding
Deliverables:
- third_party/flatbuffers/flatbuffers.h
- Network/Schema/ember_protocol.fbs
Build-time dependency: flatc compiler (only needed to regenerate, generated files committed to repo)
Phase 2: Tree Building (EmberCore)
Goal: Build BehaviorTree from FlatBuffer messages
Tasks:
- Implement TreeBuilder to construct trees from TreeInit
- Implement StateManager for tick state tracking
- Implement TCPTreeAdapter as ITreeStructure implementation
- Write unit tests
Deliverables:
Dependencies: Phase 1
Phase 3: Client Integration
Goal: Provide schema and C++ reference client for user integration
Tasks:
- Create client_sdk/ directory with schema and README
- Implement C++ reference client (EmberClient.h)
- Create minimal usage example
- Update design doc Section 5 to focus on self-generation
Deliverables:
- client_sdk/README.md - Integration guide
- client_sdk/ember_protocol.fbs - Schema file
- client_sdk/cpp/EmberClient.h - Header-only reference client
- client_sdk/cpp/example/ - Minimal usage example
- Updated Section 5 documentation
Note: Users generate bindings for their language using flatc. We provide C++ as reference only.
Dependencies: Phase 1
Phase 4: Basic Monitor Application
Goal: Create minimal working EmberMonitor application
Tasks:
- Set up EmberMonitor CMake project
- Create MonitorApp and MonitorFrame
- Implement basic TCPServer
- Create ConnectionPanel with server start/stop
- Display connected clients
Deliverables:
- EmberMonitor/CMakeLists.txt
Dependencies: Phase 2
Phase 5: Tree Visualization
Goal: Visualize received trees with status overlays
Tasks:
- Create MonitorMainPanel with scene container
- Adapt TreeVisualization for read-only mode
- Implement StatusOverlay for node status colors
- Implement ExecutionPathOverlay for path highlighting
- Create MonitorHierarchyTab (read-only)
- Create MonitorPropertiesTab (read-only with status)
Deliverables:
- ExecutionPathOverlay.h/.cpp
- MonitorHierarchyTab.h/.cpp
- MonitorPropertiesTab.h/.cpp
Dependencies: Phase 4
Phase 6: Live Updates
Goal: Real-time tick updates and blackboard monitoring
Tasks:
- Connect TickUpdate handling to UI refresh
- Implement BlackboardTab for live blackboard view
- Add tick counter and rate display to status bar
- Implement latency measurement
- Optimize refresh rate for high-frequency updates
Deliverables:
- Tick update → UI refresh pipeline
- Status bar with tick info
Dependencies: Phase 5
Phase 7: Timeline and History
Goal: Tick history with timeline scrubbing
Tasks:
- Enable history recording in StateManager
- Implement TickTimelineTab with scrubber
- Add play/pause/step controls
- Implement tick replay at variable speeds
- Add visual markers for state changes
Deliverables:
Dependencies: Phase 6
Phase 8: Polish and Integration
Goal: Production-ready application
Tasks:
- Implement MonitorPreferences dialog
- Add keyboard shortcuts
- Implement reconnection logic
- Add export features (tick log to file)
- Performance optimization
- Cross-platform testing
Deliverables:
Dependencies: Phase 7
8. CMake Structure
8.1 Root CMakeLists.txt Updates
# Add to existing BTV/CMakeLists.txt
option(BUILD_EMBER_MONITOR "Build EmberMonitor application" ON)
option(BUILD_CLIENT_SDK "Build client SDK examples" OFF)
# ... existing EmberCore and EmberForge ...
if(BUILD_EMBER_MONITOR)
add_subdirectory(EmberMonitor)
endif()
if(BUILD_CLIENT_SDK)
add_subdirectory(client_sdk/cpp)
endif()
8.2 EmberCore CMakeLists.txt Updates
# Add FlatBuffers schema generation (optional, only if regenerating)
find_program(FLATC flatc)
if(FLATC)
set(FLATBUFFERS_SCHEMA
${CMAKE_CURRENT_SOURCE_DIR}/include/Network/Schema/ember_protocol.fbs)
set(FLATBUFFERS_GENERATED
${CMAKE_CURRENT_SOURCE_DIR}/include/Network/Schema/generated/ember_protocol_generated.h)
add_custom_command(
OUTPUT ${FLATBUFFERS_GENERATED}
COMMAND ${FLATC} --cpp
-o ${CMAKE_CURRENT_SOURCE_DIR}/include/Network/Schema/generated/
${FLATBUFFERS_SCHEMA}
DEPENDS ${FLATBUFFERS_SCHEMA}
COMMENT "Generating FlatBuffers C++ code"
)
add_custom_target(generate_flatbuffers DEPENDS ${FLATBUFFERS_GENERATED})
else()
message(STATUS "flatc not found - using pre-generated FlatBuffers code")
endif()
# Add Network module sources
set(NETWORK_SOURCES
src/Network/Codec/FlatBufferCodec.cpp
src/Network/Builder/TreeBuilder.cpp
src/Network/Builder/StateManager.cpp
src/Network/Adapter/TCPTreeAdapter.cpp
)
set(NETWORK_HEADERS
include/Network/Schema/ember_protocol.fbs
include/Network/Schema/generated/ember_protocol_generated.h
include/Network/Codec/MessageFrame.h
include/Network/Codec/FlatBufferCodec.h
include/Network/Builder/TreeBuilder.h
include/Network/Builder/StateManager.h
include/Network/Adapter/TCPTreeAdapter.h
)
# Add to EmberCore target
target_sources(EmberCore PRIVATE ${NETWORK_SOURCES} ${NETWORK_HEADERS})
# Include FlatBuffers header-only runtime
target_include_directories(EmberCore PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/third_party/flatbuffers
)
8.3 EmberMonitor CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(EmberMonitor VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find wxWidgets
find_package(wxWidgets REQUIRED COMPONENTS core base aui net)
include(${wxWidgets_USE_FILE})
# Source files
set(MONITOR_SOURCES
src/App/MonitorApp.cpp
src/App/MonitorFrame.cpp
src/Network/TCPServer.cpp
src/Network/ConnectionManager.cpp
src/Panels/ConnectionPanel.cpp
src/Panels/MonitorMainPanel.cpp
src/Panels/StatusPanel.cpp
src/Tabs/ConnectionListTab.cpp
src/Tabs/MonitorHierarchyTab.cpp
src/Tabs/MonitorPropertiesTab.cpp
src/Tabs/BlackboardTab.cpp
src/Tabs/TickTimelineTab.cpp
src/Visualization/StatusOverlay.cpp
src/Visualization/ExecutionPathOverlay.cpp
src/Utils/MonitorPreferences.cpp
)
# Create executable
add_executable(EmberMonitor ${MONITOR_SOURCES})
target_include_directories(EmberMonitor
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/EmberCore/include
${CMAKE_SOURCE_DIR}/EmberForge/include
)
target_link_libraries(EmberMonitor
PRIVATE
EmberCore
${wxWidgets_LIBRARIES}
)
# Platform-specific
if(WIN32)
set_target_properties(EmberMonitor PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
if(APPLE)
set_target_properties(EmberMonitor PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/resources/Info.plist
)
endif()
# Install
install(TARGETS EmberMonitor
RUNTIME DESTINATION bin
BUNDLE DESTINATION .
)
9. Dependencies Summary
9.1 Build-Time Dependencies
| Component | Dependency | Required? | Notes |
| EmberCore | flatc | No | Only if regenerating schema |
| EmberMonitor | wxWidgets (net) | Yes | Socket support |
| Client SDK | flatc | No | Pre-generated code committed |
9.2 Runtime Dependencies
| Component | Dependency | Notes |
| EmberCore | None | FlatBuffers is header-only |
| EmberMonitor | None | All compiled in |
| C++ Client | None | Header-only |
| C# Client | None | Self-contained |
| Python Client | flatbuffers pip package | Or bundle locally |
9.3 Third-Party Files to Add
EmberCore/
└── third_party/
└── flatbuffers/
└── flatbuffers.h # ~50KB, header-only runtime
# Download from: github.com/google/flatbuffers
10. Testing Strategy
10.1 Unit Tests
| Component | Test Focus |
| FlatBufferCodec | Encode/decode all message types, verify round-trip |
| TreeBuilder | Build valid trees, handle malformed data |
| StateManager | State transitions, history, delta updates |
| TCPTreeAdapter | ITreeStructure compliance, state queries |
10.2 Integration Tests
| Test | Description |
| Protocol handshake | Full handshake sequence with FlatBuffers |
| Tree construction | TreeInit → visualization |
| Live updates | TickUpdate → UI refresh at 60Hz |
| Reconnection | Handle disconnect/reconnect |
10.3 Mock Client
class MockBTClient {
public:
void Connect(const std::string& host, int port);
void SendMockTree(int node_count);
void SimulateTicks(int count, int interval_ms);
void SimulateRandomStates();
void SimulateDisconnect();
};
10.4 Performance Tests
| Metric | Target | Notes |
| Tick update latency | < 1ms | FlatBuffers zero-copy helps |
| UI refresh rate | 60 FPS | During high-frequency updates |
| Memory (1000 nodes) | < 30 MB | Reduced vs JSON |
| Network bandwidth | < 50 KB/s | At 60Hz, 100 nodes |
11. Future Considerations
11.1 Potential Extensions
| Feature | Description |
| Breakpoints | Pause execution at specific nodes |
| Remote Control | Send commands back to client |
| Recording | Save tick history to file |
| Multiple Trees | Monitor multiple trees per client |
| Filtering | Show/hide specific node types |
| Alerts | Notifications for conditions |
| Statistics | Execution frequency, duration |
11.2 Protocol Versioning
- Version negotiated in Handshake
- FlatBuffers supports schema evolution (add fields, deprecate)
- Maintain backward compatibility for minor versions
11.3 Security Considerations
For production use:
- TLS encryption via wxWidgets SSL sockets
- Authentication tokens in Handshake
- Rate limiting
- Connection whitelisting
12. Glossary
| Term | Definition |
| Tick | Single execution cycle of the behavior tree |
| Node Status | Current execution state (IDLE, RUNNING, SUCCESS, FAILURE, HALTED) |
| Execution Path | Ordered list of nodes executed in current tick |
| Blackboard | Shared key-value storage for tree nodes |
| Delta Update | Tick update containing only changed states |
| Tree Init | Message containing full tree structure |
| FlatBuffers | Binary serialization library (Google) |
| Zero-Copy | Access data directly from buffer without parsing |
13. References