From 8507a3a501a8cfe1b8d6e1bb9a47b0173df9666e Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Mon, 27 Apr 2026 18:45:01 +0300 Subject: [PATCH] Event system --- src/features/editScene/CMakeLists.txt | 8 + src/features/editScene/EditorApp.cpp | 14 ++ src/features/editScene/EditorApp.hpp | 6 + .../editScene/components/BehaviorTree.hpp | 3 +- .../editScene/components/EventHandler.hpp | 21 ++ .../components/EventHandlerModule.cpp | 21 ++ .../editScene/components/GoapBlackboard.cpp | 23 ++ .../editScene/components/GoapBlackboard.hpp | 35 ++- .../editScene/systems/BehaviorTreeSystem.cpp | 94 ++++++++ src/features/editScene/systems/EventBus.cpp | 78 ++++++ src/features/editScene/systems/EventBus.hpp | 66 +++++ .../editScene/systems/EventHandlerSystem.cpp | 226 ++++++++++++++++++ .../editScene/systems/EventHandlerSystem.hpp | 64 +++++ .../editScene/systems/SceneSerializer.cpp | 31 +++ .../editScene/systems/SceneSerializer.hpp | 3 + .../editScene/ui/EventHandlerEditor.cpp | 64 +++++ .../editScene/ui/EventHandlerEditor.hpp | 20 ++ 17 files changed, 775 insertions(+), 2 deletions(-) create mode 100644 src/features/editScene/components/EventHandler.hpp create mode 100644 src/features/editScene/components/EventHandlerModule.cpp create mode 100644 src/features/editScene/systems/EventBus.cpp create mode 100644 src/features/editScene/systems/EventBus.hpp create mode 100644 src/features/editScene/systems/EventHandlerSystem.cpp create mode 100644 src/features/editScene/systems/EventHandlerSystem.hpp create mode 100644 src/features/editScene/ui/EventHandlerEditor.cpp create mode 100644 src/features/editScene/ui/EventHandlerEditor.hpp diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 3cd6426..adf7638 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -58,6 +58,10 @@ set(EDITSCENE_SOURCES systems/ActuatorSystem.cpp ui/ActuatorEditor.cpp components/ActuatorModule.cpp + systems/EventBus.cpp + systems/EventHandlerSystem.cpp + ui/EventHandlerEditor.cpp + components/EventHandlerModule.cpp systems/PrefabSystem.cpp ui/PrefabInstanceEditor.cpp @@ -197,6 +201,10 @@ set(EDITSCENE_HEADERS systems/GoapPlannerSystem.hpp components/Actuator.hpp ui/ActuatorEditor.hpp + systems/EventBus.hpp + components/EventHandler.hpp + systems/EventHandlerSystem.hpp + ui/EventHandlerEditor.hpp components/PrefabInstance.hpp ui/PrefabInstanceEditor.hpp diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 14ca33d..a9e05b3 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -74,6 +74,8 @@ #include "components/GoapRunner.hpp" #include "components/PathFollowing.hpp" #include "systems/ActuatorSystem.hpp" +#include "systems/EventHandlerSystem.hpp" +#include "components/EventHandler.hpp" #include #include @@ -356,6 +358,10 @@ void EditorApp::setup() m_actuatorSystem = std::make_unique( m_world, m_sceneMgr, this, m_behaviorTreeSystem.get()); + // Setup Event Handler system + m_eventHandlerSystem = std::make_unique( + m_world, m_behaviorTreeSystem.get()); + // Setup GOAP Runner system m_goapRunnerSystem = std::make_unique( m_world, m_sceneMgr, m_smartObjectSystem.get(), @@ -618,6 +624,9 @@ void EditorApp::setupECS() // Register Actuator component m_world.component(); + // Register Event Handler component + m_world.component(); + // Register GOAP Planner component m_world.component(); @@ -830,6 +839,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) m_actuatorSystem->update(evt.timeSinceLastFrame); } + /* --- Event Handler system (event-driven BTs) --- */ + if (m_eventHandlerSystem) { + m_eventHandlerSystem->update(evt.timeSinceLastFrame); + } + /* --- Dynamic physics (characters after static world) --- */ if (m_characterSystem) { diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index c775399..d587b01 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -40,6 +40,7 @@ class GoapRunnerSystem; class PathFollowingSystem; class GoapPlannerSystem; class ActuatorSystem; +class EventHandlerSystem; class EditorApp; /** @@ -190,6 +191,10 @@ public: { return m_actuatorSystem.get(); } + EventHandlerSystem *getEventHandlerSystem() const + { + return m_eventHandlerSystem.get(); + } Ogre::ImGuiOverlay *getImGuiOverlay() const { return m_imguiOverlay; @@ -234,6 +239,7 @@ private: std::unique_ptr m_pathFollowingSystem; std::unique_ptr m_goapPlannerSystem; std::unique_ptr m_actuatorSystem; + std::unique_ptr m_eventHandlerSystem; // Game systems diff --git a/src/features/editScene/components/BehaviorTree.hpp b/src/features/editScene/components/BehaviorTree.hpp index 4f2b1fd..81251d7 100644 --- a/src/features/editScene/components/BehaviorTree.hpp +++ b/src/features/editScene/components/BehaviorTree.hpp @@ -73,7 +73,8 @@ struct BehaviorTreeNode { type == "checkBit" || type == "setValue" || type == "checkValue" || type == "blackboardDump" || type == "delay" || type == "teleportToChild" || - type == "disablePhysics" || type == "enablePhysics"; + type == "disablePhysics" || type == "enablePhysics" || + type == "sendEvent"; } }; diff --git a/src/features/editScene/components/EventHandler.hpp b/src/features/editScene/components/EventHandler.hpp new file mode 100644 index 0000000..89e2df6 --- /dev/null +++ b/src/features/editScene/components/EventHandler.hpp @@ -0,0 +1,21 @@ +#ifndef EDITSCENE_EVENT_HANDLER_HPP +#define EDITSCENE_EVENT_HANDLER_HPP +#pragma once + +#include + +/** + * Event-driven behavior tree handler component. + * + * When the specified event is received, the referenced GoapAction's + * behavior tree is executed for this entity. Event parameters are + * merged into the entity's GoapBlackboard before the tree runs and + * cleaned up when the tree completes. + */ +struct EventHandlerComponent { + Ogre::String eventName; + Ogre::String actionName; + bool enabled = true; +}; + +#endif // EDITSCENE_EVENT_HANDLER_HPP diff --git a/src/features/editScene/components/EventHandlerModule.cpp b/src/features/editScene/components/EventHandlerModule.cpp new file mode 100644 index 0000000..4bf6de4 --- /dev/null +++ b/src/features/editScene/components/EventHandlerModule.cpp @@ -0,0 +1,21 @@ +#include "EventHandler.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/EventHandlerEditor.hpp" + +REGISTER_COMPONENT_GROUP("Event Handler", "Game", EventHandlerComponent, + EventHandlerEditor) +{ + registry.registerComponent( + "Event Handler", "Game", + std::make_unique(), + // Adder + [](flecs::entity e) { + if (!e.has()) + e.set({}); + }, + // Remover + [](flecs::entity e) { + if (e.has()) + e.remove(); + }); +} diff --git a/src/features/editScene/components/GoapBlackboard.cpp b/src/features/editScene/components/GoapBlackboard.cpp index 4a5253b..6d247e4 100644 --- a/src/features/editScene/components/GoapBlackboard.cpp +++ b/src/features/editScene/components/GoapBlackboard.cpp @@ -188,9 +188,32 @@ Ogre::String GoapBlackboard::dump() const ")\n"; } + if (!stringValues.empty()) { + result += " String values:\n"; + for (const auto &pair : stringValues) + result += " " + pair.first + " = " + pair.second + "\n"; + } + return result; } +void GoapBlackboard::merge(const GoapBlackboard &other) +{ + // Merge bits + bits = (bits & ~other.mask) | (other.bits & other.mask); + mask |= other.mask; + + // Merge values + for (const auto &pair : other.values) + values[pair.first] = pair.second; + for (const auto &pair : other.floatValues) + floatValues[pair.first] = pair.second; + for (const auto &pair : other.vec3Values) + vec3Values[pair.first] = pair.second; + for (const auto &pair : other.stringValues) + stringValues[pair.first] = pair.second; +} + std::vector GoapBlackboard::getSetBits() const { std::vector result; diff --git a/src/features/editScene/components/GoapBlackboard.hpp b/src/features/editScene/components/GoapBlackboard.hpp index fdd32e6..d7a4984 100644 --- a/src/features/editScene/components/GoapBlackboard.hpp +++ b/src/features/editScene/components/GoapBlackboard.hpp @@ -35,6 +35,9 @@ struct GoapBlackboard { // Named Vector3 values — runtime character state std::unordered_map vec3Values; + // Named string values — event params, tags, etc. + std::unordered_map stringValues; + GoapBlackboard() = default; /* --- Bit accessors --- */ @@ -148,6 +151,34 @@ struct GoapBlackboard { vec3Values.erase(key); } + /* --- String value accessors --- */ + void setStringValue(const std::string &key, const Ogre::String &value) + { + stringValues[key] = value; + } + + Ogre::String getStringValue(const std::string &key, + const Ogre::String &defaultValue = "") const + { + auto it = stringValues.find(key); + if (it != stringValues.end()) + return it->second; + return defaultValue; + } + + bool hasStringValue(const std::string &key) const + { + return stringValues.find(key) != stringValues.end(); + } + + void removeStringValue(const std::string &key) + { + stringValues.erase(key); + } + + /* --- Merge another blackboard into this one --- */ + void merge(const GoapBlackboard &other); + /* --- Generic scalar lookup (tries int then float) --- */ bool getScalarValue(const std::string &key, float &out) const; @@ -171,6 +202,7 @@ struct GoapBlackboard { values.clear(); floatValues.clear(); vec3Values.clear(); + stringValues.clear(); } Ogre::String dump() const; @@ -191,7 +223,8 @@ struct GoapBlackboard { bitmask == other.bitmask && values == other.values && floatValues == other.floatValues && - vec3Values == other.vec3Values; + vec3Values == other.vec3Values && + stringValues == other.stringValues; } bool operator!=(const GoapBlackboard &other) const diff --git a/src/features/editScene/systems/BehaviorTreeSystem.cpp b/src/features/editScene/systems/BehaviorTreeSystem.cpp index e1c6145..7918db7 100644 --- a/src/features/editScene/systems/BehaviorTreeSystem.cpp +++ b/src/features/editScene/systems/BehaviorTreeSystem.cpp @@ -2,6 +2,7 @@ #include "AnimationTreeSystem.hpp" #include "CharacterSystem.hpp" #include "SmartObjectSystem.hpp" +#include "EventBus.hpp" #include "../components/BehaviorTree.hpp" #include "../components/ActionDatabase.hpp" #include "../components/GoapBlackboard.hpp" @@ -16,6 +17,90 @@ static float g_epsilon = 0.0001f; +static bool parseValueString(const Ogre::String &str, int &outInt, + float &outFloat, Ogre::Vector3 &outVec3, int &type); + +/** Parse "key=val,key2=val2" params into a GoapBlackboard. + * Values are auto-detected: int, float, vec3 (x,y,z), or quoted string. */ +static void parseEventParams(const Ogre::String &str, GoapBlackboard &out) +{ + if (str.empty()) + return; + + const char *s = str.c_str(); + while (*s) { + // Skip whitespace + while (*s == ' ' || *s == '\t') + s++; + if (!*s) + break; + + // Find key + const char *keyStart = s; + while (*s && *s != '=' && *s != ',') + s++; + Ogre::String key(keyStart, static_cast(s - keyStart)); + // Trim trailing spaces from key + while (!key.empty() && + (key.back() == ' ' || key.back() == '\t')) + key.pop_back(); + + if (*s != '=') { + while (*s && *s != ',') + s++; + if (*s == ',') + s++; + continue; + } + s++; // skip '=' + + // Skip whitespace before value + while (*s == ' ' || *s == '\t') + s++; + + // Find value end (next comma or end) + const char *valStart = s; + bool inQuotes = false; + while (*s && (*s != ',' || inQuotes)) { + if (*s == '"') + inQuotes = !inQuotes; + s++; + } + Ogre::String val(valStart, static_cast(s - valStart)); + // Trim trailing spaces from value + while (!val.empty() && + (val.back() == ' ' || val.back() == '\t')) + val.pop_back(); + + // Strip quotes if present + if (val.size() >= 2 && val.front() == '"' && + val.back() == '"') { + val = val.substr(1, val.size() - 2); + out.setStringValue(key, val); + } else { + // Try int/float/vec3 + int iVal; + float fVal; + Ogre::Vector3 vVal; + int vType; + if (parseValueString(val, iVal, fVal, vVal, vType)) { + if (vType == 0) + out.setValue(key, iVal); + else if (vType == 1) + out.setFloatValue(key, fVal); + else + out.setVec3Value(key, vVal); + } else { + // Fallback to string + out.setStringValue(key, val); + } + } + + if (*s == ',') + s++; + } +} + static bool parseBitIndex(const Ogre::String &str, int &out) { // Try numeric first @@ -233,6 +318,15 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e, return Status::success; } + if (node.type == "sendEvent") { + if (isNewlyActive(state, &node)) { + GoapBlackboard params; + parseEventParams(node.params, params); + EventBus::getInstance().send(node.name, params); + } + return Status::success; + } + if (node.type == "delay") { char key[32]; snprintf(key, sizeof(key), "%p", (void *)&node); diff --git a/src/features/editScene/systems/EventBus.cpp b/src/features/editScene/systems/EventBus.cpp new file mode 100644 index 0000000..5505012 --- /dev/null +++ b/src/features/editScene/systems/EventBus.cpp @@ -0,0 +1,78 @@ +#include "EventBus.hpp" + +EventBus &EventBus::getInstance() +{ + static EventBus instance; + return instance; +} + +EventBus::ListenerId EventBus::subscribe(const Ogre::String &eventName, + Callback cb) +{ + ListenerId id = m_nextId++; + m_listeners[eventName].push_back({ id, std::move(cb) }); + m_idToEvent[id] = eventName; + return id; +} + +void EventBus::unsubscribe(ListenerId id) +{ + auto it = m_idToEvent.find(id); + if (it == m_idToEvent.end()) + return; + + const Ogre::String &eventName = it->second; + auto lit = m_listeners.find(eventName); + if (lit != m_listeners.end()) { + auto &vec = lit->second; + for (auto vit = vec.begin(); vit != vec.end(); ++vit) { + if (vit->id == id) { + vec.erase(vit); + break; + } + } + if (vec.empty()) + m_listeners.erase(lit); + } + m_idToEvent.erase(it); +} + +void EventBus::send(const Ogre::String &eventName, + const GoapBlackboard ¶ms) +{ + auto lit = m_listeners.find(eventName); + if (lit == m_listeners.end()) + return; + + // Copy the listener list in case a callback mutates subscriptions + auto listeners = lit->second; + for (const auto &listener : listeners) { + if (listener.callback) + listener.callback(eventName, params); + } +} + +void EventBus::send(const Ogre::String &eventName, + const Ogre::String ¶mName, int value) +{ + GoapBlackboard params; + params.setValue(paramName, value); + send(eventName, params); +} + +void EventBus::send(const Ogre::String &eventName, + const Ogre::String ¶mName, float value) +{ + GoapBlackboard params; + params.setFloatValue(paramName, value); + send(eventName, params); +} + +void EventBus::send(const Ogre::String &eventName, + const Ogre::String ¶mName, + const Ogre::Vector3 &value) +{ + GoapBlackboard params; + params.setVec3Value(paramName, value); + send(eventName, params); +} diff --git a/src/features/editScene/systems/EventBus.hpp b/src/features/editScene/systems/EventBus.hpp new file mode 100644 index 0000000..eaafe5b --- /dev/null +++ b/src/features/editScene/systems/EventBus.hpp @@ -0,0 +1,66 @@ +#ifndef EDITSCENE_EVENT_BUS_HPP +#define EDITSCENE_EVENT_BUS_HPP +#pragma once + +#include "../components/GoapBlackboard.hpp" +#include +#include +#include +#include +#include + +/** + * Global synchronous event bus. + * + * Subscribers register callbacks by event name. When an event is sent, + * all matching callbacks are invoked immediately in the send() call. + * + * Payload is a GoapBlackboard — reuses the existing int/float/vec3/bit/string + * storage with zero RTTI overhead. + */ +class EventBus { +public: + using ListenerId = uint64_t; + using Callback = + std::function; + + static EventBus &getInstance(); + + /** Subscribe to an event. Returns a handle for unsubscribe(). */ + ListenerId subscribe(const Ogre::String &eventName, Callback cb); + + /** Unsubscribe by handle. Safe to call with invalid id. */ + void unsubscribe(ListenerId id); + + /** Send an event with a GoapBlackboard payload. */ + void send(const Ogre::String &eventName, + const GoapBlackboard ¶ms = {}); + + /** Convenience: send event with a single int param. */ + void send(const Ogre::String &eventName, + const Ogre::String ¶mName, int value); + + /** Convenience: send event with a single float param. */ + void send(const Ogre::String &eventName, + const Ogre::String ¶mName, float value); + + /** Convenience: send event with a single Vec3 param. */ + void send(const Ogre::String &eventName, + const Ogre::String ¶mName, + const Ogre::Vector3 &value); + +private: + EventBus() = default; + + struct Listener { + ListenerId id; + Callback callback; + }; + + ListenerId m_nextId = 1; + std::unordered_map> m_listeners; + std::unordered_map m_idToEvent; +}; + +#endif // EDITSCENE_EVENT_BUS_HPP diff --git a/src/features/editScene/systems/EventHandlerSystem.cpp b/src/features/editScene/systems/EventHandlerSystem.cpp new file mode 100644 index 0000000..c09767a --- /dev/null +++ b/src/features/editScene/systems/EventHandlerSystem.cpp @@ -0,0 +1,226 @@ +#include "EventHandlerSystem.hpp" +#include "BehaviorTreeSystem.hpp" +#include "../components/EventHandler.hpp" +#include "../components/ActionDatabase.hpp" +#include "../components/GoapBlackboard.hpp" +#include + +EventHandlerSystem::EventHandlerSystem(flecs::world &world, + BehaviorTreeSystem *btSystem) + : m_world(world) + , m_btSystem(btSystem) +{ +} + +EventHandlerSystem::~EventHandlerSystem() +{ + // Unsubscribe all remaining listeners + for (auto &pair : m_subscriptions) { + EventBus::getInstance().unsubscribe(pair.second); + } + m_subscriptions.clear(); +} + +void EventHandlerSystem::subscribeEntity( + flecs::entity e, const EventHandlerComponent &handler) +{ + if (!handler.enabled || handler.eventName.empty()) + return; + + flecs::entity_t id = e.id(); + if (m_subscriptions.find(id) != m_subscriptions.end()) + return; + + EventBus::ListenerId lid = EventBus::getInstance().subscribe( + handler.eventName, + [this, id, handler](const Ogre::String &, + const GoapBlackboard ¶ms) { + this->onEvent(id, handler.eventName, params); + }); + + m_subscriptions[id] = lid; +} + +void EventHandlerSystem::unsubscribeEntity(flecs::entity_t id) +{ + auto it = m_subscriptions.find(id); + if (it == m_subscriptions.end()) + return; + + EventBus::getInstance().unsubscribe(it->second); + m_subscriptions.erase(it); + + // If there is an active handler, abort it and clean up + auto activeIt = m_activeHandlers.find(id); + if (activeIt != m_activeHandlers.end()) { + auto keysIt = m_injectedKeys.find(id); + if (keysIt != m_injectedKeys.end()) { + flecs::entity e = m_world.entity(id); + if (e.is_alive()) + removeParams(e, keysIt->second); + m_injectedKeys.erase(keysIt); + } + m_activeHandlers.erase(activeIt); + } +} + +void EventHandlerSystem::onEvent(flecs::entity_t entityId, + const Ogre::String &eventName, + const GoapBlackboard ¶ms) +{ + (void)eventName; + + flecs::entity e = m_world.entity(entityId); + if (!e.is_alive() || !e.has()) + return; + + auto &handler = e.get(); + if (!handler.enabled || handler.actionName.empty()) + return; + + // If already handling an event, abort the previous one first + auto activeIt = m_activeHandlers.find(entityId); + if (activeIt != m_activeHandlers.end()) { + auto keysIt = m_injectedKeys.find(entityId); + if (keysIt != m_injectedKeys.end()) { + removeParams(e, keysIt->second); + m_injectedKeys.erase(keysIt); + } + m_activeHandlers.erase(activeIt); + } + + // Start new handler + ActiveHandler ah; + ah.entityId = entityId; + ah.actionName = handler.actionName; + ah.eventParams = params; + ah.firstFrame = true; + m_activeHandlers[entityId] = ah; + + // Inject params immediately so the first update() tick sees them + std::unordered_set injected; + injectParams(e, params, injected); + m_injectedKeys[entityId] = std::move(injected); +} + +void EventHandlerSystem::injectParams( + flecs::entity e, const GoapBlackboard ¶ms, + std::unordered_set &outKeys) +{ + if (!e.has()) + e.set({}); + + auto &bb = e.get_mut(); + + for (const auto &pair : params.values) { + bb.setValue(pair.first, pair.second); + outKeys.insert(pair.first); + } + for (const auto &pair : params.floatValues) { + bb.setFloatValue(pair.first, pair.second); + outKeys.insert(pair.first); + } + for (const auto &pair : params.vec3Values) { + bb.setVec3Value(pair.first, pair.second); + outKeys.insert(pair.first); + } + for (const auto &pair : params.stringValues) { + bb.setStringValue(pair.first, pair.second); + outKeys.insert(pair.first); + } + // Bits are not injected individually; they are part of the event + // payload semantics but merging bits globally is risky. + // Instead, event bits are NOT auto-injected. If needed, use + // setBit nodes inside the handler BT. +} + +void EventHandlerSystem::removeParams( + flecs::entity e, + const std::unordered_set &keys) +{ + if (!e.has()) + return; + + auto &bb = e.get_mut(); + for (const auto &key : keys) { + bb.removeValue(key); + bb.removeFloatValue(key); + bb.removeVec3Value(key); + bb.removeStringValue(key); + } +} + +void EventHandlerSystem::update(float deltaTime) +{ + if (!m_btSystem) + return; + + // --- Sync subscriptions with current entities --- + std::unordered_set currentEntities; + m_world.query().each( + [&](flecs::entity e, EventHandlerComponent &handler) { + currentEntities.insert(e.id()); + if (handler.enabled) { + subscribeEntity(e, handler); + } else { + unsubscribeEntity(e.id()); + } + }); + + // Unsubscribe entities that lost their component + std::vector toRemove; + for (auto &pair : m_subscriptions) { + if (currentEntities.find(pair.first) == currentEntities.end()) + toRemove.push_back(pair.first); + } + for (flecs::entity_t id : toRemove) + unsubscribeEntity(id); + + // --- Tick active handlers --- + std::vector completedHandlers; + for (auto &pair : m_activeHandlers) { + flecs::entity_t id = pair.first; + ActiveHandler &ah = pair.second; + + // Look up the action in the database + ActionDatabase *db = nullptr; + m_world.query().each( + [&](flecs::entity, ActionDatabase &database) { + if (!db) + db = &database; + }); + if (!db) { + completedHandlers.push_back(id); + continue; + } + + const GoapAction *action = db->findAction(ah.actionName); + if (!action) { + Ogre::LogManager::getSingleton().logMessage( + "[EventHandlerSystem] Action not found: " + + ah.actionName); + completedHandlers.push_back(id); + continue; + } + + auto status = m_btSystem->evaluatePlayerAction( + id, action->behaviorTree, deltaTime, ah.firstFrame); + ah.firstFrame = false; + + if (status != BehaviorTreeSystem::Status::running) { + completedHandlers.push_back(id); + } + } + + // --- Clean up completed handlers --- + for (flecs::entity_t id : completedHandlers) { + flecs::entity e = m_world.entity(id); + auto keysIt = m_injectedKeys.find(id); + if (keysIt != m_injectedKeys.end()) { + if (e.is_alive()) + removeParams(e, keysIt->second); + m_injectedKeys.erase(keysIt); + } + m_activeHandlers.erase(id); + } +} diff --git a/src/features/editScene/systems/EventHandlerSystem.hpp b/src/features/editScene/systems/EventHandlerSystem.hpp new file mode 100644 index 0000000..2e162cc --- /dev/null +++ b/src/features/editScene/systems/EventHandlerSystem.hpp @@ -0,0 +1,64 @@ +#ifndef EDITSCENE_EVENT_HANDLER_SYSTEM_HPP +#define EDITSCENE_EVENT_HANDLER_SYSTEM_HPP +#pragma once + +#include +#include +#include +#include +#include + +#include "EventBus.hpp" +#include "../components/GoapBlackboard.hpp" + +class BehaviorTreeSystem; +class EditorApp; + +/** + * System that executes behavior trees in response to events. + * + * For each entity with EventHandlerComponent, subscribes to the + * specified event. When the event fires, copies parameters into the + * entity's GoapBlackboard and runs the referenced action's behavior + * tree. Cleans up injected parameters when the tree completes. + */ +class EventHandlerSystem { +public: + EventHandlerSystem(flecs::world &world, BehaviorTreeSystem *btSystem); + ~EventHandlerSystem(); + + void update(float deltaTime); + +private: + struct ActiveHandler { + flecs::entity_t entityId; + Ogre::String actionName; + GoapBlackboard eventParams; + bool firstFrame = true; + }; + + void subscribeEntity(flecs::entity e, + const class EventHandlerComponent &handler); + void unsubscribeEntity(flecs::entity_t id); + void onEvent(flecs::entity_t entityId, + const Ogre::String &eventName, + const GoapBlackboard ¶ms); + void injectParams(flecs::entity e, const GoapBlackboard ¶ms, + std::unordered_set &outKeys); + void removeParams(flecs::entity e, + const std::unordered_set &keys); + + flecs::world &m_world; + BehaviorTreeSystem *m_btSystem; + + // Per-entity event subscription + std::unordered_map m_subscriptions; + + // Per-entity active handler (one at a time per entity) + std::unordered_map m_activeHandlers; + + // Keys injected into blackboard per active handler + std::unordered_map> m_injectedKeys; +}; + +#endif // EDITSCENE_EVENT_HANDLER_SYSTEM_HPP diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 4cb6a8b..ae234b3 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -28,6 +28,7 @@ #include "../components/WaterPlane.hpp" #include "../components/Sun.hpp" #include "../components/Skybox.hpp" +#include "../components/EventHandler.hpp" #include "../components/ActionDatabase.hpp" #include "../components/ActionDebug.hpp" #include "../components/SmartObject.hpp" @@ -307,6 +308,9 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) if (entity.has()) { json["actuator"] = serializeActuator(entity); } + if (entity.has()) { + json["eventHandler"] = serializeEventHandler(entity); + } if (entity.has()) { json["goapPlanner"] = serializeGoapPlanner(entity); } @@ -516,6 +520,9 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json, if (json.contains("actuator")) { deserializeActuator(entity, json["actuator"]); } + if (json.contains("eventHandler")) { + deserializeEventHandler(entity, json["eventHandler"]); + } if (json.contains("goapPlanner")) { deserializeGoapPlanner(entity, json["goapPlanner"]); } @@ -800,6 +807,9 @@ void SceneSerializer::deserializeEntityComponents( } if (json.contains("actuator")) { deserializeActuator(entity, json["actuator"]); + if (json.contains("eventHandler")) { + deserializeEventHandler(entity, json["eventHandler"]); + } } if (json.contains("goapPlanner")) { deserializeGoapPlanner(entity, json["goapPlanner"]); @@ -3700,3 +3710,24 @@ void SceneSerializer::deserializeActuator(flecs::entity entity, } entity.set(actuator); } + + +nlohmann::json SceneSerializer::serializeEventHandler(flecs::entity entity) +{ + const EventHandlerComponent &handler = entity.get(); + nlohmann::json json; + json["eventName"] = handler.eventName; + json["actionName"] = handler.actionName; + json["enabled"] = handler.enabled; + return json; +} + +void SceneSerializer::deserializeEventHandler(flecs::entity entity, + const nlohmann::json &json) +{ + EventHandlerComponent handler; + handler.eventName = json.value("eventName", handler.eventName); + handler.actionName = json.value("actionName", handler.actionName); + handler.enabled = json.value("enabled", handler.enabled); + entity.set(handler); +} diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index bb7974c..ea52a97 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -211,6 +211,7 @@ private: nlohmann::json serializePathFollowing(flecs::entity entity); nlohmann::json serializeSmartObject(flecs::entity entity); nlohmann::json serializeActuator(flecs::entity entity); + nlohmann::json serializeEventHandler(flecs::entity entity); nlohmann::json serializeGoapPlanner(flecs::entity entity); nlohmann::json serializeBehaviorTree(flecs::entity entity); void deserializeActionDatabase(flecs::entity entity, @@ -223,6 +224,8 @@ private: const nlohmann::json &json); void deserializeActuator(flecs::entity entity, const nlohmann::json &json); + void deserializeEventHandler(flecs::entity entity, + const nlohmann::json &json); void deserializeGoapPlanner(flecs::entity entity, const nlohmann::json &json); void deserializeBehaviorTree(flecs::entity entity, diff --git a/src/features/editScene/ui/EventHandlerEditor.cpp b/src/features/editScene/ui/EventHandlerEditor.cpp new file mode 100644 index 0000000..dece51c --- /dev/null +++ b/src/features/editScene/ui/EventHandlerEditor.cpp @@ -0,0 +1,64 @@ +#include "EventHandlerEditor.hpp" +#include "../components/ActionDatabase.hpp" +#include + +static ActionDatabase *findDatabase(flecs::entity entity) +{ + auto world = entity.world(); + ActionDatabase *db = nullptr; + world.query().each( + [&](flecs::entity, ActionDatabase &database) { + if (!db) + db = &database; + }); + return db; +} + +bool EventHandlerEditor::renderComponent(flecs::entity entity, + EventHandlerComponent &handler) +{ + bool modified = false; + ImGui::PushID("EventHandler"); + + char eventBuf[256]; + snprintf(eventBuf, sizeof(eventBuf), "%s", handler.eventName.c_str()); + if (ImGui::InputText("Event Name", eventBuf, sizeof(eventBuf))) { + handler.eventName = eventBuf; + modified = true; + } + + if (ImGui::Checkbox("Enabled", &handler.enabled)) + modified = true; + + // Action selection from database + ActionDatabase *db = findDatabase(entity); + if (db && !db->actions.empty()) { + int selected = -1; + std::vector names; + for (size_t i = 0; i < db->actions.size(); i++) { + names.push_back(db->actions[i].name.c_str()); + if (handler.actionName == db->actions[i].name) + selected = static_cast(i); + } + if (ImGui::Combo("Action", &selected, names.data(), + static_cast(names.size()))) { + if (selected >= 0 && + selected < static_cast(names.size())) + handler.actionName = names[selected]; + modified = true; + } + } else { + ImGui::TextDisabled("No actions in database"); + char actionBuf[256]; + snprintf(actionBuf, sizeof(actionBuf), "%s", + handler.actionName.c_str()); + if (ImGui::InputText("Action Name", actionBuf, + sizeof(actionBuf))) { + handler.actionName = actionBuf; + modified = true; + } + } + + ImGui::PopID(); + return modified; +} diff --git a/src/features/editScene/ui/EventHandlerEditor.hpp b/src/features/editScene/ui/EventHandlerEditor.hpp new file mode 100644 index 0000000..b7bb096 --- /dev/null +++ b/src/features/editScene/ui/EventHandlerEditor.hpp @@ -0,0 +1,20 @@ +#ifndef EDITSCENE_EVENT_HANDLER_EDITOR_HPP +#define EDITSCENE_EVENT_HANDLER_EDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/EventHandler.hpp" + +class EventHandlerEditor : public ComponentEditor { +public: + const char *getName() const override + { + return "Event Handler"; + } + +protected: + bool renderComponent(flecs::entity entity, + EventHandlerComponent &handler) override; +}; + +#endif // EDITSCENE_EVENT_HANDLER_EDITOR_HPP