Path following
This commit is contained in:
@@ -43,6 +43,10 @@ set(EDITSCENE_SOURCES
|
||||
recast/PartitionedMesh.cpp
|
||||
recast/fastlz.c
|
||||
systems/CharacterSystem.cpp
|
||||
systems/SmartObjectSystem.cpp
|
||||
components/SmartObjectModule.cpp
|
||||
ui/SmartObjectEditor.cpp
|
||||
|
||||
ui/TransformEditor.cpp
|
||||
ui/RenderableEditor.cpp
|
||||
ui/PhysicsColliderEditor.cpp
|
||||
@@ -163,6 +167,10 @@ set(EDITSCENE_HEADERS
|
||||
recast/PartitionedMesh.hpp
|
||||
recast/fastlz.h
|
||||
systems/CharacterSystem.hpp
|
||||
systems/SmartObjectSystem.hpp
|
||||
components/SmartObject.hpp
|
||||
ui/SmartObjectEditor.hpp
|
||||
|
||||
systems/ProceduralTextureSystem.hpp
|
||||
systems/StaticGeometrySystem.hpp
|
||||
systems/SceneSerializer.hpp
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
#include "systems/BehaviorTreeSystem.hpp"
|
||||
#include "systems/NavMeshSystem.hpp"
|
||||
#include "systems/CharacterSystem.hpp"
|
||||
#include "systems/SmartObjectSystem.hpp"
|
||||
#include "systems/CellGridSystem.hpp"
|
||||
|
||||
#include "systems/NormalDebugSystem.hpp"
|
||||
#include "systems/RoomLayoutSystem.hpp"
|
||||
#include "systems/StartupMenuSystem.hpp"
|
||||
@@ -61,6 +63,8 @@
|
||||
#include "components/BehaviorTree.hpp"
|
||||
#include "components/GoapBlackboard.hpp"
|
||||
#include "components/NavMesh.hpp"
|
||||
#include "components/SmartObject.hpp"
|
||||
|
||||
#include <OgreRTShaderSystem.h>
|
||||
#include <imgui.h>
|
||||
|
||||
@@ -312,7 +316,16 @@ void EditorApp::setup()
|
||||
m_navMeshSystem =
|
||||
std::make_unique<NavMeshSystem>(m_world, m_sceneMgr);
|
||||
|
||||
// Setup SmartObject system (requires NavMesh, BehaviorTree, and AnimationTree)
|
||||
m_smartObjectSystem = std::make_unique<SmartObjectSystem>(
|
||||
m_world, m_sceneMgr, m_navMeshSystem.get(),
|
||||
m_behaviorTreeSystem.get());
|
||||
// Wire up AnimationTreeSystem for animation state machine control
|
||||
m_smartObjectSystem->setAnimationTreeSystem(
|
||||
m_animationTreeSystem.get());
|
||||
|
||||
// Setup Character physics system
|
||||
|
||||
m_characterSystem =
|
||||
std::make_unique<CharacterSystem>(m_world, m_sceneMgr);
|
||||
m_characterSystem->initialize();
|
||||
@@ -550,6 +563,9 @@ void EditorApp::setupECS()
|
||||
m_world.component<BehaviorTreeComponent>();
|
||||
m_world.component<GoapBlackboard>();
|
||||
|
||||
// Register Smart Object component
|
||||
m_world.component<SmartObjectComponent>();
|
||||
|
||||
// Register Navigation components
|
||||
m_world.component<NavMeshComponent>();
|
||||
m_world.component<NavMeshGeometrySource>();
|
||||
@@ -727,7 +743,13 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
m_navMeshSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Smart Object system (AI navigation to smart objects) --- */
|
||||
if (m_smartObjectSystem) {
|
||||
m_smartObjectSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Dynamic physics (characters after static world) --- */
|
||||
|
||||
if (m_characterSystem) {
|
||||
m_characterSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ class EditorSunSystem;
|
||||
class EditorSkyboxSystem;
|
||||
class EditorWaterPlaneSystem;
|
||||
class NormalDebugSystem;
|
||||
class SmartObjectSystem;
|
||||
class EditorApp;
|
||||
|
||||
/**
|
||||
@@ -220,8 +221,10 @@ private:
|
||||
std::unique_ptr<CellGridSystem> m_cellGridSystem;
|
||||
std::unique_ptr<NormalDebugSystem> m_normalDebugSystem;
|
||||
std::unique_ptr<RoomLayoutSystem> m_roomLayoutSystem;
|
||||
std::unique_ptr<SmartObjectSystem> m_smartObjectSystem;
|
||||
|
||||
// Game systems
|
||||
|
||||
std::unique_ptr<StartupMenuSystem> m_startupMenuSystem;
|
||||
std::unique_ptr<PlayerControllerSystem> m_playerControllerSystem;
|
||||
|
||||
|
||||
@@ -4,6 +4,34 @@
|
||||
|
||||
#include "GoapBlackboard.hpp"
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Configuration for one animation state machine / state pair.
|
||||
* Used to map logical animation names (e.g. "idle", "walk", "run")
|
||||
* to the actual state machine and state in the animation tree.
|
||||
*/
|
||||
struct AnimationStateConfig {
|
||||
/** Logical name used by code (e.g. "idle", "walk", "run") */
|
||||
Ogre::String name;
|
||||
|
||||
/** Name of the state machine in the animation tree */
|
||||
Ogre::String stateMachine = "Locomotion";
|
||||
|
||||
/** Name of the state within the state machine */
|
||||
Ogre::String stateName = "Idle";
|
||||
|
||||
AnimationStateConfig() = default;
|
||||
|
||||
AnimationStateConfig(const Ogre::String &name_,
|
||||
const Ogre::String &stateMachine_,
|
||||
const Ogre::String &stateName_)
|
||||
: name(name_)
|
||||
, stateMachine(stateMachine_)
|
||||
, stateName(stateName_)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-character action debug component.
|
||||
@@ -29,7 +57,43 @@ struct ActionDebug {
|
||||
// Debug output
|
||||
Ogre::String lastResult;
|
||||
|
||||
// --- Animation state machine configuration ---
|
||||
// List of animation state configs (name/stateMachine/stateName triples)
|
||||
// Default entries: idle, walk, run
|
||||
std::vector<AnimationStateConfig> animStates = {
|
||||
{ "idle", "Locomotion", "Idle" },
|
||||
{ "walk", "Locomotion", "Walk" },
|
||||
{ "run", "Locomotion", "Run" },
|
||||
};
|
||||
|
||||
// Walk speed (m/s) used for root motion scaling
|
||||
float walkSpeed = 2.5f;
|
||||
|
||||
// Run speed (m/s) used for root motion scaling
|
||||
float runSpeed = 5.0f;
|
||||
|
||||
// Whether to use root motion (true) or manual velocity (false)
|
||||
bool useRootMotion = true;
|
||||
|
||||
ActionDebug() = default;
|
||||
|
||||
/**
|
||||
* Get the state machine and state name for a given logical animation name.
|
||||
* Returns true if found, false if not (caller should use defaults).
|
||||
*/
|
||||
bool getAnimState(const Ogre::String &animName,
|
||||
Ogre::String &outStateMachine,
|
||||
Ogre::String &outStateName) const
|
||||
{
|
||||
for (const auto &cfg : animStates) {
|
||||
if (cfg.name == animName) {
|
||||
outStateMachine = cfg.stateMachine;
|
||||
outStateName = cfg.stateName;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTION_DEBUG_HPP
|
||||
|
||||
@@ -17,6 +17,13 @@ struct CharacterSlotsComponent {
|
||||
/* Runtime: master entity with shared skeleton (set by CharacterSlotSystem) */
|
||||
Ogre::Entity *masterEntity = nullptr;
|
||||
|
||||
/**
|
||||
* Front-facing axis for this character model.
|
||||
* Most models face -Z (NEGATIVE_UNIT_Z), but some face +Z.
|
||||
* This is used by path following to rotate the character correctly.
|
||||
*/
|
||||
Ogre::Vector3 frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z;
|
||||
|
||||
CharacterSlotsComponent() = default;
|
||||
};
|
||||
|
||||
|
||||
38
src/features/editScene/components/SmartObject.hpp
Normal file
38
src/features/editScene/components/SmartObject.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef EDITSCENE_SMART_OBJECT_HPP
|
||||
#define EDITSCENE_SMART_OBJECT_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Smart Object component.
|
||||
*
|
||||
* Defines an interactive object in the world that characters can
|
||||
* navigate to and perform GOAP actions on.
|
||||
*
|
||||
* The entity's TransformComponent defines the position/orientation.
|
||||
* Characters will pathfind to within `radius` distance in XZ plane
|
||||
* and `height` difference in Y, then execute the selected action.
|
||||
*/
|
||||
struct SmartObjectComponent {
|
||||
// Interaction radius in XZ plane
|
||||
float radius = 1.0f;
|
||||
|
||||
// Maximum height difference for interaction
|
||||
float height = 1.8f;
|
||||
|
||||
// Names of GOAP actions (from ActionDatabase) that this object provides
|
||||
std::vector<Ogre::String> actionNames;
|
||||
|
||||
SmartObjectComponent() = default;
|
||||
|
||||
explicit SmartObjectComponent(float radius_, float height_)
|
||||
: radius(radius_)
|
||||
, height(height_)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_SMART_OBJECT_HPP
|
||||
20
src/features/editScene/components/SmartObjectModule.cpp
Normal file
20
src/features/editScene/components/SmartObjectModule.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "SmartObject.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/SmartObjectEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Smart Object", "AI", SmartObjectComponent,
|
||||
SmartObjectEditor)
|
||||
{
|
||||
registry.registerComponent<SmartObjectComponent>(
|
||||
"Smart Object", "AI", std::make_unique<SmartObjectEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<SmartObjectComponent>())
|
||||
e.set<SmartObjectComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<SmartObjectComponent>())
|
||||
e.remove<SmartObjectComponent>();
|
||||
});
|
||||
}
|
||||
@@ -33,6 +33,8 @@
|
||||
#include "../components/BehaviorTree.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "../components/NavMesh.hpp"
|
||||
#include "../components/SmartObject.hpp"
|
||||
|
||||
#include "../ui/TransformEditor.hpp"
|
||||
#include "../ui/RenderableEditor.hpp"
|
||||
#include "../ui/PhysicsColliderEditor.hpp"
|
||||
@@ -480,6 +482,8 @@ void EditorUISystem::renderEntityNode(flecs::entity entity, int depth)
|
||||
indicators += " [Nav]";
|
||||
if (entity.has<NavMeshGeometrySource>())
|
||||
indicators += " [NavSrc]";
|
||||
if (entity.has<SmartObjectComponent>())
|
||||
indicators += " [SO]";
|
||||
|
||||
snprintf(label, sizeof(label), "%s%s##%llu", name.c_str(),
|
||||
indicators.c_str(), (unsigned long long)entity.id());
|
||||
@@ -891,7 +895,15 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render SmartObject if present
|
||||
if (entity.has<SmartObjectComponent>()) {
|
||||
auto &so = entity.get_mut<SmartObjectComponent>();
|
||||
m_componentRegistry.render<SmartObjectComponent>(entity, so);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Show message if no components
|
||||
|
||||
if (componentCount == 0) {
|
||||
ImGui::TextDisabled("No components");
|
||||
ImGui::Text("Click 'Add Component' to add components");
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "../components/Skybox.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/ActionDebug.hpp"
|
||||
#include "../components/SmartObject.hpp"
|
||||
#include "../components/BehaviorTree.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "../components/NavMesh.hpp"
|
||||
@@ -292,6 +293,9 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
|
||||
if (entity.has<ActionDebug>()) {
|
||||
json["actionDebug"] = serializeActionDebug(entity);
|
||||
}
|
||||
if (entity.has<SmartObjectComponent>()) {
|
||||
json["smartObject"] = serializeSmartObject(entity);
|
||||
}
|
||||
if (entity.has<BehaviorTreeComponent>()) {
|
||||
json["behaviorTree"] = serializeBehaviorTree(entity);
|
||||
}
|
||||
@@ -485,6 +489,9 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json,
|
||||
if (json.contains("actionDebug")) {
|
||||
deserializeActionDebug(entity, json["actionDebug"]);
|
||||
}
|
||||
if (json.contains("smartObject")) {
|
||||
deserializeSmartObject(entity, json["smartObject"]);
|
||||
}
|
||||
if (json.contains("behaviorTree")) {
|
||||
deserializeBehaviorTree(entity, json["behaviorTree"]);
|
||||
}
|
||||
@@ -748,6 +755,9 @@ void SceneSerializer::deserializeEntityFirstPass(const nlohmann::json &json,
|
||||
if (json.contains("actionDebug")) {
|
||||
deserializeActionDebug(entity, json["actionDebug"]);
|
||||
}
|
||||
if (json.contains("smartObject")) {
|
||||
deserializeSmartObject(entity, json["smartObject"]);
|
||||
}
|
||||
if (json.contains("behaviorTree")) {
|
||||
deserializeBehaviorTree(entity, json["behaviorTree"]);
|
||||
}
|
||||
@@ -3204,6 +3214,20 @@ nlohmann::json SceneSerializer::serializeActionDebug(flecs::entity entity)
|
||||
json["selectedActionName"] = debug.selectedActionName;
|
||||
if (!debug.selectedGoalName.empty())
|
||||
json["selectedGoalName"] = debug.selectedGoalName;
|
||||
|
||||
// Serialize animation state configs
|
||||
json["animStates"] = nlohmann::json::array();
|
||||
for (const auto &cfg : debug.animStates) {
|
||||
nlohmann::json entry;
|
||||
entry["name"] = cfg.name;
|
||||
entry["stateMachine"] = cfg.stateMachine;
|
||||
entry["stateName"] = cfg.stateName;
|
||||
json["animStates"].push_back(entry);
|
||||
}
|
||||
|
||||
json["walkSpeed"] = debug.walkSpeed;
|
||||
json["runSpeed"] = debug.runSpeed;
|
||||
json["useRootMotion"] = debug.useRootMotion;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -3215,9 +3239,63 @@ void SceneSerializer::deserializeActionDebug(flecs::entity entity,
|
||||
deserializeGoapBlackboard(debug.blackboard, json["blackboard"]);
|
||||
debug.selectedActionName = json.value("selectedActionName", "");
|
||||
debug.selectedGoalName = json.value("selectedGoalName", "");
|
||||
|
||||
// Deserialize animation state configs (new format)
|
||||
if (json.contains("animStates") && json["animStates"].is_array()) {
|
||||
debug.animStates.clear();
|
||||
for (const auto &entry : json["animStates"]) {
|
||||
AnimationStateConfig cfg;
|
||||
cfg.name = entry.value("name", "");
|
||||
cfg.stateMachine =
|
||||
entry.value("stateMachine", "Locomotion");
|
||||
cfg.stateName = entry.value("stateName", "Idle");
|
||||
debug.animStates.push_back(cfg);
|
||||
}
|
||||
} else {
|
||||
// Backward compatibility: old format with individual fields
|
||||
debug.animStates.clear();
|
||||
Ogre::String sm =
|
||||
json.value("locomotionStateMachine", "Locomotion");
|
||||
debug.animStates.push_back(
|
||||
{ "idle", sm, json.value("idleStateName", "Idle") });
|
||||
debug.animStates.push_back(
|
||||
{ "walk", sm, json.value("walkStateName", "Walk") });
|
||||
debug.animStates.push_back(
|
||||
{ "run", sm, json.value("runStateName", "Run") });
|
||||
}
|
||||
|
||||
debug.walkSpeed = json.value("walkSpeed", 2.5f);
|
||||
debug.runSpeed = json.value("runSpeed", 5.0f);
|
||||
debug.useRootMotion = json.value("useRootMotion", true);
|
||||
entity.set<ActionDebug>(debug);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeSmartObject(flecs::entity entity)
|
||||
{
|
||||
const SmartObjectComponent &so = entity.get<SmartObjectComponent>();
|
||||
nlohmann::json json;
|
||||
json["radius"] = so.radius;
|
||||
json["height"] = so.height;
|
||||
json["actionNames"] = so.actionNames;
|
||||
return json;
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeSmartObject(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
SmartObjectComponent so;
|
||||
so.radius = json.value("radius", 1.0f);
|
||||
so.height = json.value("height", 1.8f);
|
||||
if (json.contains("actionNames") && json["actionNames"].is_array()) {
|
||||
so.actionNames.clear();
|
||||
for (const auto &name : json["actionNames"]) {
|
||||
if (name.is_string())
|
||||
so.actionNames.push_back(name);
|
||||
}
|
||||
}
|
||||
entity.set<SmartObjectComponent>(so);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeBehaviorTree(flecs::entity entity)
|
||||
{
|
||||
const BehaviorTreeComponent &bt = entity.get<BehaviorTreeComponent>();
|
||||
|
||||
@@ -171,11 +171,14 @@ private:
|
||||
// AI/GOAP serialization
|
||||
nlohmann::json serializeActionDatabase(flecs::entity entity);
|
||||
nlohmann::json serializeActionDebug(flecs::entity entity);
|
||||
nlohmann::json serializeSmartObject(flecs::entity entity);
|
||||
nlohmann::json serializeBehaviorTree(flecs::entity entity);
|
||||
void deserializeActionDatabase(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeActionDebug(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeSmartObject(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeBehaviorTree(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
|
||||
|
||||
725
src/features/editScene/systems/SmartObjectSystem.cpp
Normal file
725
src/features/editScene/systems/SmartObjectSystem.cpp
Normal file
@@ -0,0 +1,725 @@
|
||||
#include "SmartObjectSystem.hpp"
|
||||
#include "../components/SmartObject.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/Character.hpp"
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/ActionDebug.hpp"
|
||||
#include "../components/NavMesh.hpp"
|
||||
#include "../components/AnimationTree.hpp"
|
||||
#include "NavMeshSystem.hpp"
|
||||
#include "BehaviorTreeSystem.hpp"
|
||||
#include "AnimationTreeSystem.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
|
||||
SmartObjectSystem *SmartObjectSystem::s_instance = nullptr;
|
||||
|
||||
SmartObjectSystem::SmartObjectSystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr,
|
||||
NavMeshSystem *navSystem,
|
||||
BehaviorTreeSystem *btSystem)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
, m_navSystem(navSystem)
|
||||
, m_btSystem(btSystem)
|
||||
, m_animTreeSystem(nullptr)
|
||||
{
|
||||
s_instance = this;
|
||||
}
|
||||
|
||||
SmartObjectSystem::~SmartObjectSystem() = default;
|
||||
|
||||
Ogre::Vector3 SmartObjectSystem::getEntityPosition(flecs::entity e)
|
||||
{
|
||||
if (e.has<TransformComponent>()) {
|
||||
auto &trans = e.get<TransformComponent>();
|
||||
if (trans.node)
|
||||
return trans.node->_getDerivedPosition();
|
||||
return trans.position;
|
||||
}
|
||||
return Ogre::Vector3::ZERO;
|
||||
}
|
||||
|
||||
Ogre::Vector3 SmartObjectSystem::getFrontAxis(flecs::entity e) const
|
||||
{
|
||||
if (e.has<CharacterSlotsComponent>()) {
|
||||
auto &slots = e.get<CharacterSlotsComponent>();
|
||||
return slots.frontAxis;
|
||||
}
|
||||
return Ogre::Vector3::NEGATIVE_UNIT_Z;
|
||||
}
|
||||
|
||||
bool SmartObjectSystem::isInRange(const Ogre::Vector3 &charPos,
|
||||
const Ogre::Vector3 &objPos, float radius,
|
||||
float height)
|
||||
{
|
||||
// Check XZ distance
|
||||
Ogre::Vector3 diff = charPos - objPos;
|
||||
float xzDist = std::sqrt(diff.x * diff.x + diff.z * diff.z);
|
||||
if (xzDist > radius)
|
||||
return false;
|
||||
|
||||
// Check Y difference
|
||||
float yDiff = std::abs(diff.y);
|
||||
if (yDiff > height)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SmartObjectSystem::rotateTowards(flecs::entity e,
|
||||
const Ogre::Vector3 &direction,
|
||||
float deltaTime)
|
||||
{
|
||||
if (!e.has<TransformComponent>())
|
||||
return;
|
||||
auto &trans = e.get_mut<TransformComponent>();
|
||||
if (!trans.node)
|
||||
return;
|
||||
|
||||
Ogre::Vector3 flatDir = direction;
|
||||
flatDir.y = 0;
|
||||
if (flatDir.squaredLength() < 0.0001f)
|
||||
return;
|
||||
flatDir.normalise();
|
||||
|
||||
// Get the character's front-facing axis (from CharacterSlots or default -Z)
|
||||
Ogre::Vector3 frontAxis = getFrontAxis(e);
|
||||
|
||||
// Use yaw rotation only (Y plane)
|
||||
Ogre::Quaternion currentRot = trans.node->getOrientation();
|
||||
Ogre::Quaternion targetRot = frontAxis.getRotationTo(flatDir);
|
||||
|
||||
// Slerp for smooth rotation
|
||||
Ogre::Quaternion newRot = Ogre::Quaternion::Slerp(
|
||||
deltaTime * 10.0f, currentRot, targetRot, true);
|
||||
|
||||
// Extract only the Y-axis rotation (yaw) to keep character upright
|
||||
Ogre::Radian yaw = newRot.getYaw();
|
||||
trans.node->setOrientation(
|
||||
Ogre::Quaternion(yaw, Ogre::Vector3::UNIT_Y));
|
||||
}
|
||||
|
||||
void SmartObjectSystem::setLocomotionState(flecs::entity e,
|
||||
const Ogre::String &animName)
|
||||
{
|
||||
if (!m_animTreeSystem)
|
||||
return;
|
||||
if (!e.has<AnimationTreeComponent>())
|
||||
return;
|
||||
|
||||
Ogre::String smName = "Locomotion";
|
||||
Ogre::String stateName = animName;
|
||||
|
||||
// Try to get the state machine/state from ActionDebug's animStates list
|
||||
if (e.has<ActionDebug>()) {
|
||||
auto &debug = e.get<ActionDebug>();
|
||||
Ogre::String foundSM, foundState;
|
||||
if (debug.getAnimState(animName, foundSM, foundState)) {
|
||||
smName = foundSM;
|
||||
stateName = foundState;
|
||||
}
|
||||
}
|
||||
|
||||
m_animTreeSystem->setState(e, smName, stateName, false);
|
||||
}
|
||||
|
||||
bool SmartObjectSystem::testSmartObjectAction(flecs::entity character,
|
||||
flecs::entity smartObject,
|
||||
const Ogre::String &actionName)
|
||||
{
|
||||
if (!character.is_alive() || !smartObject.is_alive())
|
||||
return false;
|
||||
|
||||
// Find the action in the database
|
||||
ActionDatabase *db = nullptr;
|
||||
m_world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
if (!db)
|
||||
return false;
|
||||
|
||||
const GoapAction *action = db->findAction(actionName);
|
||||
if (!action)
|
||||
return false;
|
||||
|
||||
// Set up the character state to navigate to the smart object
|
||||
auto &state = m_states[character.id()];
|
||||
state.state = State::Pathfinding;
|
||||
state.target.smartObject = smartObject;
|
||||
state.target.actionName = actionName;
|
||||
state.target.path.clear();
|
||||
state.target.pathIndex = 0;
|
||||
state.target.pathRecalcTimer = 0.0f;
|
||||
state.target.isExecuting = false;
|
||||
state.target.executionTimer = 0.0f;
|
||||
|
||||
std::cout << "[SmartObjectSystem] Character " << character.id()
|
||||
<< " navigating to smart object " << smartObject.id()
|
||||
<< " for action: " << actionName << std::endl;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SmartObjectSystem::update(float deltaTime)
|
||||
{
|
||||
// Find the navmesh entity
|
||||
flecs::entity navmeshEntity = flecs::entity::null();
|
||||
m_world.query<NavMeshComponent>().each(
|
||||
[&](flecs::entity e, NavMeshComponent &) {
|
||||
if (!navmeshEntity.is_alive())
|
||||
navmeshEntity = e;
|
||||
});
|
||||
|
||||
// Find the action database
|
||||
ActionDatabase *db = nullptr;
|
||||
m_world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
|
||||
// Process each character with a blackboard (AI-driven characters)
|
||||
m_world.query<CharacterComponent, TransformComponent, GoapBlackboard>()
|
||||
.each([&](flecs::entity e, CharacterComponent &cc,
|
||||
TransformComponent &trans, GoapBlackboard &bb) {
|
||||
(void)cc;
|
||||
(void)trans;
|
||||
(void)bb;
|
||||
|
||||
auto &state = m_states[e.id()];
|
||||
state.scanTimer += deltaTime;
|
||||
|
||||
// --- State: Idle - scan for smart objects ---
|
||||
if (state.state == State::Idle) {
|
||||
// Set idle animation
|
||||
setLocomotionState(e, "idle");
|
||||
|
||||
// Only scan periodically
|
||||
if (state.scanTimer < 1.0f)
|
||||
return;
|
||||
state.scanTimer = 0.0f;
|
||||
|
||||
Ogre::Vector3 charPos = getEntityPosition(e);
|
||||
|
||||
// Find the nearest smart object with a
|
||||
// relevant action
|
||||
flecs::entity bestObject =
|
||||
flecs::entity::null();
|
||||
Ogre::String bestActionName;
|
||||
float bestDist =
|
||||
std::numeric_limits<float>::max();
|
||||
|
||||
m_world.query<SmartObjectComponent,
|
||||
TransformComponent>()
|
||||
.each([&](flecs::entity so,
|
||||
SmartObjectComponent &soComp,
|
||||
TransformComponent &soTrans) {
|
||||
(void)soTrans;
|
||||
|
||||
for (const auto &actionName :
|
||||
soComp.actionNames) {
|
||||
if (!db)
|
||||
continue;
|
||||
|
||||
const GoapAction *action =
|
||||
db->findAction(
|
||||
actionName);
|
||||
if (!action)
|
||||
continue;
|
||||
|
||||
if (!action->canRun(bb))
|
||||
continue;
|
||||
|
||||
Ogre::Vector3 objPos =
|
||||
getEntityPosition(
|
||||
so);
|
||||
float dist =
|
||||
charPos.distance(
|
||||
objPos);
|
||||
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
bestObject = so;
|
||||
bestActionName =
|
||||
actionName;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (bestObject.is_alive()) {
|
||||
state.state = State::Pathfinding;
|
||||
state.target.smartObject = bestObject;
|
||||
state.target.actionName =
|
||||
bestActionName;
|
||||
state.target.path.clear();
|
||||
state.target.pathIndex = 0;
|
||||
state.target.pathRecalcTimer = 0.0f;
|
||||
state.target.isExecuting = false;
|
||||
state.target.executionTimer = 0.0f;
|
||||
|
||||
std::cout
|
||||
<< "[SmartObjectSystem] Character "
|
||||
<< e.id()
|
||||
<< " found smart object "
|
||||
<< bestObject.id()
|
||||
<< " for action: "
|
||||
<< bestActionName << std::endl;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- State: Pathfinding - find path to smart object ---
|
||||
if (state.state == State::Pathfinding) {
|
||||
if (!state.target.smartObject.is_alive()) {
|
||||
state.state = State::Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
Ogre::Vector3 charPos = getEntityPosition(e);
|
||||
Ogre::Vector3 objPos = getEntityPosition(
|
||||
state.target.smartObject);
|
||||
|
||||
auto &soComp =
|
||||
state.target.smartObject
|
||||
.get<SmartObjectComponent>();
|
||||
if (isInRange(charPos, objPos, soComp.radius,
|
||||
soComp.height)) {
|
||||
state.state = State::Executing;
|
||||
state.target.isExecuting = true;
|
||||
state.target.executionTimer = 0.0f;
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
if (navmeshEntity.is_alive() && m_navSystem) {
|
||||
state.target.path.clear();
|
||||
state.target.pathIndex = 0;
|
||||
bool found = m_navSystem->findPath(
|
||||
navmeshEntity, charPos, objPos,
|
||||
state.target.path);
|
||||
if (found &&
|
||||
!state.target.path.empty()) {
|
||||
state.state = State::Moving;
|
||||
std::cout
|
||||
<< "[SmartObjectSystem] Path found with "
|
||||
<< state.target.path
|
||||
.size()
|
||||
<< " waypoints"
|
||||
<< std::endl;
|
||||
} else {
|
||||
state.state = State::Idle;
|
||||
cc.linearVelocity =
|
||||
Ogre::Vector3::ZERO;
|
||||
}
|
||||
} else {
|
||||
state.target.path.clear();
|
||||
state.target.path.push_back(objPos);
|
||||
state.target.pathIndex = 0;
|
||||
state.state = State::Moving;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- State: Moving - follow path ---
|
||||
if (state.state == State::Moving) {
|
||||
if (!state.target.smartObject.is_alive() ||
|
||||
state.target.path.empty()) {
|
||||
state.state = State::Idle;
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
Ogre::Vector3 charPos = getEntityPosition(e);
|
||||
Ogre::Vector3 objPos = getEntityPosition(
|
||||
state.target.smartObject);
|
||||
|
||||
auto &soComp =
|
||||
state.target.smartObject
|
||||
.get<SmartObjectComponent>();
|
||||
if (isInRange(charPos, objPos, soComp.radius,
|
||||
soComp.height)) {
|
||||
state.state = State::Executing;
|
||||
state.target.isExecuting = true;
|
||||
state.target.executionTimer = 0.0f;
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
state.target.pathRecalcTimer += deltaTime;
|
||||
if (state.target.pathRecalcTimer > 2.0f) {
|
||||
state.target.pathRecalcTimer = 0.0f;
|
||||
if (navmeshEntity.is_alive() &&
|
||||
m_navSystem) {
|
||||
std::vector<Ogre::Vector3>
|
||||
newPath;
|
||||
bool found =
|
||||
m_navSystem->findPath(
|
||||
navmeshEntity,
|
||||
charPos, objPos,
|
||||
newPath);
|
||||
if (found && !newPath.empty()) {
|
||||
state.target.path =
|
||||
newPath;
|
||||
state.target.pathIndex =
|
||||
0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.target.pathIndex >=
|
||||
(int)state.target.path.size()) {
|
||||
state.target.path.clear();
|
||||
state.target.path.push_back(objPos);
|
||||
state.target.pathIndex = 0;
|
||||
}
|
||||
|
||||
Ogre::Vector3 targetPos =
|
||||
state.target
|
||||
.path[state.target.pathIndex];
|
||||
Ogre::Vector3 toTarget = targetPos - charPos;
|
||||
|
||||
if (toTarget.length() < 0.5f) {
|
||||
state.target.pathIndex++;
|
||||
if (state.target.pathIndex >=
|
||||
(int)state.target.path.size()) {
|
||||
toTarget = objPos - charPos;
|
||||
if (toTarget.length() < 0.5f) {
|
||||
state.state =
|
||||
State::Executing;
|
||||
state.target
|
||||
.isExecuting =
|
||||
true;
|
||||
state.target
|
||||
.executionTimer =
|
||||
0.0f;
|
||||
cc.linearVelocity =
|
||||
Ogre::Vector3::
|
||||
ZERO;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
targetPos =
|
||||
state.target.path
|
||||
[state.target
|
||||
.pathIndex];
|
||||
toTarget = targetPos - charPos;
|
||||
}
|
||||
}
|
||||
|
||||
// Only set velocity direction - AnimationTreeSystem
|
||||
// applies root motion if useRootMotion is enabled.
|
||||
// The character is rotated to face the movement
|
||||
// direction, no other transforms are applied.
|
||||
toTarget.y = 0;
|
||||
if (toTarget.squaredLength() > 0.0001f) {
|
||||
toTarget.normalise();
|
||||
|
||||
// Determine speed based on distance
|
||||
// to target
|
||||
float distToTarget =
|
||||
(charPos - objPos).length();
|
||||
float speed = 2.5f; // walk speed
|
||||
|
||||
// Use run speed if far away
|
||||
if (e.has<ActionDebug>()) {
|
||||
auto &debug =
|
||||
e.get<ActionDebug>();
|
||||
if (distToTarget >
|
||||
debug.walkSpeed * 3.0f) {
|
||||
speed = debug.runSpeed;
|
||||
setLocomotionState(
|
||||
e, "run");
|
||||
} else {
|
||||
speed = debug.walkSpeed;
|
||||
setLocomotionState(
|
||||
e, "walk");
|
||||
}
|
||||
} else {
|
||||
setLocomotionState(e, "walk");
|
||||
}
|
||||
|
||||
cc.linearVelocity = toTarget * speed;
|
||||
|
||||
// Rotate character to face movement
|
||||
// direction (Y plane only)
|
||||
rotateTowards(e, toTarget, deltaTime);
|
||||
} else {
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- State: Executing - run the action ---
|
||||
if (state.state == State::Executing) {
|
||||
if (!state.target.smartObject.is_alive()) {
|
||||
state.state = State::Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep character stopped
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
|
||||
// Set idle animation while executing
|
||||
setLocomotionState(e, "idle");
|
||||
|
||||
if (state.target.isExecuting && db) {
|
||||
const GoapAction *action =
|
||||
db->findAction(
|
||||
state.target.actionName);
|
||||
if (action && m_btSystem) {
|
||||
state.target.executionTimer +=
|
||||
deltaTime;
|
||||
|
||||
if (state.target.executionTimer >
|
||||
3.0f) {
|
||||
bb.apply(
|
||||
action->effects);
|
||||
state.state =
|
||||
State::Idle;
|
||||
state.target
|
||||
.isExecuting =
|
||||
false;
|
||||
std::cout
|
||||
<< "[SmartObjectSystem] Action '"
|
||||
<< state.target
|
||||
.actionName
|
||||
<< "' completed"
|
||||
<< std::endl;
|
||||
}
|
||||
} else {
|
||||
if (action) {
|
||||
bb.apply(
|
||||
action->effects);
|
||||
}
|
||||
state.state = State::Idle;
|
||||
state.target.isExecuting =
|
||||
false;
|
||||
}
|
||||
} else {
|
||||
state.state = State::Idle;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Also process characters with ActionDebug (for editor testing)
|
||||
m_world.query<CharacterComponent, TransformComponent, ActionDebug>()
|
||||
.each([&](flecs::entity e, CharacterComponent &cc,
|
||||
TransformComponent &trans, ActionDebug &debug) {
|
||||
(void)trans;
|
||||
(void)debug;
|
||||
|
||||
auto &state = m_states[e.id()];
|
||||
|
||||
if (state.state == State::Idle) {
|
||||
setLocomotionState(e, "idle");
|
||||
return;
|
||||
}
|
||||
|
||||
GoapBlackboard &bb = debug.blackboard;
|
||||
|
||||
// --- State: Pathfinding ---
|
||||
if (state.state == State::Pathfinding) {
|
||||
if (!state.target.smartObject.is_alive()) {
|
||||
state.state = State::Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
Ogre::Vector3 charPos = getEntityPosition(e);
|
||||
Ogre::Vector3 objPos = getEntityPosition(
|
||||
state.target.smartObject);
|
||||
|
||||
auto &soComp =
|
||||
state.target.smartObject
|
||||
.get<SmartObjectComponent>();
|
||||
if (isInRange(charPos, objPos, soComp.radius,
|
||||
soComp.height)) {
|
||||
state.state = State::Executing;
|
||||
state.target.isExecuting = true;
|
||||
state.target.executionTimer = 0.0f;
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
if (navmeshEntity.is_alive() && m_navSystem) {
|
||||
state.target.path.clear();
|
||||
state.target.pathIndex = 0;
|
||||
bool found = m_navSystem->findPath(
|
||||
navmeshEntity, charPos, objPos,
|
||||
state.target.path);
|
||||
if (found &&
|
||||
!state.target.path.empty()) {
|
||||
state.state = State::Moving;
|
||||
} else {
|
||||
state.state = State::Idle;
|
||||
cc.linearVelocity =
|
||||
Ogre::Vector3::ZERO;
|
||||
}
|
||||
} else {
|
||||
state.target.path.clear();
|
||||
state.target.path.push_back(objPos);
|
||||
state.target.pathIndex = 0;
|
||||
state.state = State::Moving;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- State: Moving ---
|
||||
if (state.state == State::Moving) {
|
||||
if (!state.target.smartObject.is_alive() ||
|
||||
state.target.path.empty()) {
|
||||
state.state = State::Idle;
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
Ogre::Vector3 charPos = getEntityPosition(e);
|
||||
Ogre::Vector3 objPos = getEntityPosition(
|
||||
state.target.smartObject);
|
||||
|
||||
auto &soComp =
|
||||
state.target.smartObject
|
||||
.get<SmartObjectComponent>();
|
||||
if (isInRange(charPos, objPos, soComp.radius,
|
||||
soComp.height)) {
|
||||
state.state = State::Executing;
|
||||
state.target.isExecuting = true;
|
||||
state.target.executionTimer = 0.0f;
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
state.target.pathRecalcTimer += deltaTime;
|
||||
if (state.target.pathRecalcTimer > 2.0f) {
|
||||
state.target.pathRecalcTimer = 0.0f;
|
||||
if (navmeshEntity.is_alive() &&
|
||||
m_navSystem) {
|
||||
std::vector<Ogre::Vector3>
|
||||
newPath;
|
||||
bool found =
|
||||
m_navSystem->findPath(
|
||||
navmeshEntity,
|
||||
charPos, objPos,
|
||||
newPath);
|
||||
if (found && !newPath.empty()) {
|
||||
state.target.path =
|
||||
newPath;
|
||||
state.target.pathIndex =
|
||||
0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.target.pathIndex >=
|
||||
(int)state.target.path.size()) {
|
||||
state.target.path.clear();
|
||||
state.target.path.push_back(objPos);
|
||||
state.target.pathIndex = 0;
|
||||
}
|
||||
|
||||
Ogre::Vector3 targetPos =
|
||||
state.target
|
||||
.path[state.target.pathIndex];
|
||||
Ogre::Vector3 toTarget = targetPos - charPos;
|
||||
|
||||
if (toTarget.length() < 0.5f) {
|
||||
state.target.pathIndex++;
|
||||
if (state.target.pathIndex >=
|
||||
(int)state.target.path.size()) {
|
||||
toTarget = objPos - charPos;
|
||||
if (toTarget.length() < 0.5f) {
|
||||
state.state =
|
||||
State::Executing;
|
||||
state.target
|
||||
.isExecuting =
|
||||
true;
|
||||
state.target
|
||||
.executionTimer =
|
||||
0.0f;
|
||||
cc.linearVelocity =
|
||||
Ogre::Vector3::
|
||||
ZERO;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
targetPos =
|
||||
state.target.path
|
||||
[state.target
|
||||
.pathIndex];
|
||||
toTarget = targetPos - charPos;
|
||||
}
|
||||
}
|
||||
|
||||
toTarget.y = 0;
|
||||
if (toTarget.squaredLength() > 0.0001f) {
|
||||
toTarget.normalise();
|
||||
|
||||
float distToTarget =
|
||||
(charPos - objPos).length();
|
||||
float speed = debug.walkSpeed;
|
||||
|
||||
if (distToTarget >
|
||||
debug.walkSpeed * 3.0f) {
|
||||
speed = debug.runSpeed;
|
||||
setLocomotionState(e, "run");
|
||||
} else {
|
||||
speed = debug.walkSpeed;
|
||||
setLocomotionState(e, "walk");
|
||||
}
|
||||
|
||||
cc.linearVelocity = toTarget * speed;
|
||||
|
||||
// Rotate character to face movement
|
||||
// direction (Y plane only)
|
||||
rotateTowards(e, toTarget, deltaTime);
|
||||
} else {
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- State: Executing ---
|
||||
if (state.state == State::Executing) {
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
|
||||
// Set idle animation while executing
|
||||
setLocomotionState(e, "idle");
|
||||
|
||||
if (state.target.isExecuting && db) {
|
||||
const GoapAction *action =
|
||||
db->findAction(
|
||||
state.target.actionName);
|
||||
if (action) {
|
||||
state.target.executionTimer +=
|
||||
deltaTime;
|
||||
if (state.target.executionTimer >
|
||||
3.0f) {
|
||||
bb.apply(
|
||||
action->effects);
|
||||
state.state =
|
||||
State::Idle;
|
||||
state.target
|
||||
.isExecuting =
|
||||
false;
|
||||
debug.lastResult =
|
||||
"Smart object action '" +
|
||||
state.target
|
||||
.actionName +
|
||||
"' completed";
|
||||
}
|
||||
} else {
|
||||
state.state = State::Idle;
|
||||
state.target.isExecuting =
|
||||
false;
|
||||
}
|
||||
} else {
|
||||
state.state = State::Idle;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
109
src/features/editScene/systems/SmartObjectSystem.hpp
Normal file
109
src/features/editScene/systems/SmartObjectSystem.hpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#ifndef EDITSCENE_SMART_OBJECT_SYSTEM_HPP
|
||||
#define EDITSCENE_SMART_OBJECT_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class NavMeshSystem;
|
||||
class BehaviorTreeSystem;
|
||||
class AnimationTreeSystem;
|
||||
|
||||
/**
|
||||
* System that manages Smart Object interactions.
|
||||
*
|
||||
* For each entity with CharacterComponent and a blackboard:
|
||||
* 1. Scans for nearby SmartObjectComponent entities
|
||||
* 2. If a smart object action is relevant to the character's GOAP state,
|
||||
* pathfinds to the smart object
|
||||
* 3. Follows the path until within radius (XZ) and height (Y) threshold
|
||||
* 4. Executes the smart object's action behavior tree
|
||||
*/
|
||||
class SmartObjectSystem {
|
||||
public:
|
||||
SmartObjectSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
NavMeshSystem *navSystem,
|
||||
BehaviorTreeSystem *btSystem);
|
||||
~SmartObjectSystem();
|
||||
|
||||
static SmartObjectSystem *getInstance()
|
||||
{
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the AnimationTreeSystem for animation state machine control.
|
||||
* Must be called after construction.
|
||||
*/
|
||||
void setAnimationTreeSystem(AnimationTreeSystem *system)
|
||||
{
|
||||
m_animTreeSystem = system;
|
||||
}
|
||||
|
||||
void update(float deltaTime);
|
||||
|
||||
/**
|
||||
* Test-run a smart object action on a character.
|
||||
* Used by ActionDebugEditor.
|
||||
*/
|
||||
bool testSmartObjectAction(flecs::entity character,
|
||||
flecs::entity smartObject,
|
||||
const Ogre::String &actionName);
|
||||
|
||||
private:
|
||||
struct SmartObjectTarget {
|
||||
flecs::entity smartObject;
|
||||
Ogre::String actionName;
|
||||
std::vector<Ogre::Vector3> path;
|
||||
int pathIndex = 0;
|
||||
float pathRecalcTimer = 0.0f;
|
||||
bool isExecuting = false;
|
||||
float executionTimer = 0.0f;
|
||||
};
|
||||
|
||||
enum class State { Idle, Pathfinding, Moving, Executing };
|
||||
|
||||
struct CharacterState {
|
||||
State state = State::Idle;
|
||||
SmartObjectTarget target;
|
||||
float scanTimer = 0.0f;
|
||||
};
|
||||
|
||||
bool isInRange(const Ogre::Vector3 &charPos,
|
||||
const Ogre::Vector3 &objPos, float radius, float height);
|
||||
|
||||
Ogre::Vector3 getEntityPosition(flecs::entity e);
|
||||
|
||||
/**
|
||||
* Rotate character to face a target direction (Y plane only).
|
||||
* Uses the scene node's yaw rotation.
|
||||
*/
|
||||
void rotateTowards(flecs::entity e, const Ogre::Vector3 &direction,
|
||||
float deltaTime);
|
||||
|
||||
/**
|
||||
* Set the locomotion animation state (idle/walk/run) via the
|
||||
* AnimationTreeSystem.
|
||||
*/
|
||||
void setLocomotionState(flecs::entity e, const Ogre::String &stateName);
|
||||
|
||||
/**
|
||||
* Get the front-facing axis for a character entity.
|
||||
* Checks CharacterSlotsComponent first, defaults to -Z.
|
||||
*/
|
||||
Ogre::Vector3 getFrontAxis(flecs::entity e) const;
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
NavMeshSystem *m_navSystem;
|
||||
BehaviorTreeSystem *m_btSystem;
|
||||
AnimationTreeSystem *m_animTreeSystem;
|
||||
|
||||
std::unordered_map<flecs::entity_t, CharacterState> m_states;
|
||||
|
||||
static SmartObjectSystem *s_instance;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_SMART_OBJECT_SYSTEM_HPP
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "ActionDebugEditor.hpp"
|
||||
#include "GoapBlackboardEditor.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../systems/SmartObjectSystem.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
ActionDatabase *ActionDebugEditor::findDatabase(flecs::entity entity)
|
||||
@@ -31,6 +34,14 @@ bool ActionDebugEditor::renderComponent(flecs::entity entity,
|
||||
if (ImGui::CollapsingHeader("Goal Tester"))
|
||||
renderGoalTester(entity, debug);
|
||||
|
||||
if (ImGui::CollapsingHeader("Animation Config",
|
||||
ImGuiTreeNodeFlags_DefaultOpen))
|
||||
renderAnimationConfig(debug);
|
||||
|
||||
if (ImGui::CollapsingHeader("Smart Object Tester",
|
||||
ImGuiTreeNodeFlags_DefaultOpen))
|
||||
renderSmartObjectTester(entity, debug);
|
||||
|
||||
if (!debug.lastResult.empty()) {
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Last result: %s", debug.lastResult.c_str());
|
||||
@@ -50,8 +61,7 @@ void ActionDebugEditor::renderActionTester(flecs::entity entity,
|
||||
{
|
||||
ActionDatabase *db = findDatabase(entity);
|
||||
if (!db) {
|
||||
ImGui::TextDisabled(
|
||||
"No ActionDatabase found in scene.");
|
||||
ImGui::TextDisabled("No ActionDatabase found in scene.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -72,11 +82,10 @@ void ActionDebugEditor::renderActionTester(flecs::entity entity,
|
||||
debug.currentActionName = action.name;
|
||||
// Apply effects to local blackboard for testing
|
||||
debug.blackboard.apply(action.effects);
|
||||
debug.lastResult = "Ran " + action.name +
|
||||
" (cost: " +
|
||||
Ogre::StringConverter::toString(
|
||||
action.cost) +
|
||||
")";
|
||||
debug.lastResult =
|
||||
"Ran " + action.name + " (cost: " +
|
||||
Ogre::StringConverter::toString(action.cost) +
|
||||
")";
|
||||
}
|
||||
|
||||
if (!canRun)
|
||||
@@ -84,8 +93,7 @@ void ActionDebugEditor::renderActionTester(flecs::entity entity,
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%s (cost: %d) %s", action.name.c_str(),
|
||||
action.cost,
|
||||
canRun ? "" : "[blocked]");
|
||||
action.cost, canRun ? "" : "[blocked]");
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -94,8 +102,7 @@ void ActionDebugEditor::renderActionTester(flecs::entity entity,
|
||||
|
||||
if (debug.isRunning) {
|
||||
ImGui::Text("Running: %s (%.1fs)",
|
||||
debug.currentActionName.c_str(),
|
||||
debug.runTimer);
|
||||
debug.currentActionName.c_str(), debug.runTimer);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Stop")) {
|
||||
debug.isRunning = false;
|
||||
@@ -109,8 +116,7 @@ void ActionDebugEditor::renderGoalTester(flecs::entity entity,
|
||||
{
|
||||
ActionDatabase *db = findDatabase(entity);
|
||||
if (!db) {
|
||||
ImGui::TextDisabled(
|
||||
"No ActionDatabase found in scene.");
|
||||
ImGui::TextDisabled("No ActionDatabase found in scene.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -132,13 +138,11 @@ void ActionDebugEditor::renderGoalTester(flecs::entity entity,
|
||||
ImGui::Text("%s (prio: %d) %s", goal.name.c_str(),
|
||||
goal.priority,
|
||||
satisfied ? "[satisfied]" :
|
||||
(valid ? "[valid]" :
|
||||
"[invalid]"));
|
||||
(valid ? "[valid]" : "[invalid]"));
|
||||
|
||||
if (!goal.condition.empty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("cond: %s",
|
||||
goal.condition.c_str());
|
||||
ImGui::TextDisabled("cond: %s", goal.condition.c_str());
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
@@ -146,3 +150,149 @@ void ActionDebugEditor::renderGoalTester(flecs::entity entity,
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
void ActionDebugEditor::renderAnimationConfig(ActionDebug &debug)
|
||||
{
|
||||
ImGui::Text("Animation State Configs:");
|
||||
ImGui::TextDisabled(
|
||||
"Map logical names (idle/walk/run) to state machine/state pairs");
|
||||
ImGui::Separator();
|
||||
|
||||
// Render each animation state config
|
||||
int removeIdx = -1;
|
||||
for (int i = 0; i < (int)debug.animStates.size(); i++) {
|
||||
auto &cfg = debug.animStates[i];
|
||||
ImGui::PushID(i);
|
||||
|
||||
char buf[256];
|
||||
|
||||
ImGui::Text("Entry %d:", i);
|
||||
ImGui::Indent();
|
||||
|
||||
// Logical name (e.g. "idle", "walk", "run")
|
||||
strncpy(buf, cfg.name.c_str(), sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
if (ImGui::InputText("Name", buf, sizeof(buf))) {
|
||||
cfg.name = buf;
|
||||
}
|
||||
|
||||
// State machine name
|
||||
strncpy(buf, cfg.stateMachine.c_str(), sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
if (ImGui::InputText("State Machine", buf, sizeof(buf))) {
|
||||
cfg.stateMachine = buf;
|
||||
}
|
||||
|
||||
// State name within the state machine
|
||||
strncpy(buf, cfg.stateName.c_str(), sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
if (ImGui::InputText("State Name", buf, sizeof(buf))) {
|
||||
cfg.stateName = buf;
|
||||
}
|
||||
|
||||
// Remove button (keep at least 1 entry)
|
||||
if (debug.animStates.size() > 1) {
|
||||
if (ImGui::SmallButton("Remove")) {
|
||||
removeIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
ImGui::Separator();
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (removeIdx >= 0) {
|
||||
debug.animStates.erase(debug.animStates.begin() + removeIdx);
|
||||
}
|
||||
|
||||
// Add new entry button
|
||||
if (ImGui::Button("Add Animation State")) {
|
||||
debug.animStates.push_back(AnimationStateConfig());
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Movement Speeds:");
|
||||
ImGui::Indent();
|
||||
|
||||
ImGui::DragFloat("Walk Speed (m/s)", &debug.walkSpeed, 0.1f, 0.5f,
|
||||
20.0f);
|
||||
ImGui::DragFloat("Run Speed (m/s)", &debug.runSpeed, 0.1f, 0.5f, 20.0f);
|
||||
|
||||
ImGui::Unindent();
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Checkbox("Use Root Motion", &debug.useRootMotion);
|
||||
}
|
||||
|
||||
void ActionDebugEditor::renderSmartObjectTester(flecs::entity entity,
|
||||
ActionDebug &debug)
|
||||
{
|
||||
auto world = entity.world();
|
||||
|
||||
// Find SmartObjectSystem instance
|
||||
SmartObjectSystem *soSystem = SmartObjectSystem::getInstance();
|
||||
if (!soSystem) {
|
||||
ImGui::TextDisabled("SmartObjectSystem not available.");
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::Text("Smart Objects in Scene:");
|
||||
ImGui::Indent();
|
||||
|
||||
// Query all entities with SmartObjectComponent
|
||||
bool foundAny = false;
|
||||
world.query<SmartObjectComponent, TransformComponent>().each(
|
||||
[&](flecs::entity so, SmartObjectComponent &soComp,
|
||||
TransformComponent &soTrans) {
|
||||
(void)soTrans;
|
||||
foundAny = true;
|
||||
|
||||
// Get entity name
|
||||
Ogre::String name =
|
||||
"Entity " +
|
||||
Ogre::StringConverter::toString(so.id());
|
||||
if (so.has<EntityNameComponent>()) {
|
||||
name = so.get<EntityNameComponent>().name;
|
||||
}
|
||||
|
||||
ImGui::PushID(so.id());
|
||||
ImGui::Text("%s", name.c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(r=%.1f, h=%.1f, %zu actions)",
|
||||
soComp.radius, soComp.height,
|
||||
soComp.actionNames.size());
|
||||
|
||||
// Show available actions for this smart object
|
||||
ImGui::Indent();
|
||||
for (const auto &actionName : soComp.actionNames) {
|
||||
ImGui::PushID(actionName.c_str());
|
||||
ImGui::Text("%s", actionName.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Test on Character")) {
|
||||
bool ok =
|
||||
soSystem->testSmartObjectAction(
|
||||
entity, so, actionName);
|
||||
if (ok) {
|
||||
debug.lastResult =
|
||||
"Testing smart object action '" +
|
||||
actionName + "' on " +
|
||||
name;
|
||||
} else {
|
||||
debug.lastResult =
|
||||
"Failed to start smart object action '" +
|
||||
actionName + "'";
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::Unindent();
|
||||
ImGui::PopID();
|
||||
});
|
||||
|
||||
if (!foundAny) {
|
||||
ImGui::TextDisabled("No smart objects in scene.");
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
@@ -5,24 +5,31 @@
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/ActionDebug.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/SmartObject.hpp"
|
||||
|
||||
/**
|
||||
* Editor for ActionDebug component.
|
||||
*
|
||||
* Allows inspecting and test-running GOAP actions on a character.
|
||||
* Also displays a list of smart object entities and allows testing
|
||||
* their actions on the character.
|
||||
*/
|
||||
class ActionDebugEditor : public ComponentEditor<ActionDebug> {
|
||||
public:
|
||||
const char *getName() const override { return "Action Debug"; }
|
||||
const char *getName() const override
|
||||
{
|
||||
return "Action Debug";
|
||||
}
|
||||
|
||||
protected:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
ActionDebug &debug) override;
|
||||
bool renderComponent(flecs::entity entity, ActionDebug &debug) override;
|
||||
|
||||
private:
|
||||
bool renderBlackboard(ActionDebug &debug);
|
||||
void renderActionTester(flecs::entity entity, ActionDebug &debug);
|
||||
void renderGoalTester(flecs::entity entity, ActionDebug &debug);
|
||||
void renderAnimationConfig(ActionDebug &debug);
|
||||
void renderSmartObjectTester(flecs::entity entity, ActionDebug &debug);
|
||||
|
||||
ActionDatabase *findDatabase(flecs::entity entity);
|
||||
};
|
||||
|
||||
115
src/features/editScene/ui/SmartObjectEditor.cpp
Normal file
115
src/features/editScene/ui/SmartObjectEditor.cpp
Normal file
@@ -0,0 +1,115 @@
|
||||
#include "SmartObjectEditor.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
ActionDatabase *SmartObjectEditor::findDatabase(flecs::entity entity)
|
||||
{
|
||||
auto world = entity.world();
|
||||
ActionDatabase *db = nullptr;
|
||||
world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
return db;
|
||||
}
|
||||
|
||||
bool SmartObjectEditor::renderComponent(flecs::entity entity,
|
||||
SmartObjectComponent &so)
|
||||
{
|
||||
bool modified = false;
|
||||
ImGui::PushID("SmartObject");
|
||||
|
||||
ImGui::Text("Smart Object Settings");
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::DragFloat("Radius", &so.radius, 0.1f, 0.1f, 100.0f,
|
||||
"%.1f")) {
|
||||
if (so.radius < 0.1f)
|
||||
so.radius = 0.1f;
|
||||
modified = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(XZ interaction distance)");
|
||||
|
||||
if (ImGui::DragFloat("Height", &so.height, 0.1f, 0.1f, 100.0f,
|
||||
"%.1f")) {
|
||||
if (so.height < 0.1f)
|
||||
so.height = 0.1f;
|
||||
modified = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(Y interaction threshold)");
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Actions:");
|
||||
ImGui::Indent();
|
||||
|
||||
// List currently selected actions
|
||||
for (size_t i = 0; i < so.actionNames.size(); i++) {
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
ImGui::Text("%s", so.actionNames[i].c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X")) {
|
||||
so.actionNames.erase(so.actionNames.begin() + i);
|
||||
modified = true;
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
// Add action from database
|
||||
ActionDatabase *db = findDatabase(entity);
|
||||
if (db && !db->actions.empty()) {
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Add action:");
|
||||
static int selectedAction = -1;
|
||||
|
||||
// Build list of action names not already selected
|
||||
std::vector<const char *> availableNames;
|
||||
std::vector<Ogre::String> availableNamesStorage;
|
||||
for (const auto &action : db->actions) {
|
||||
bool alreadySelected = false;
|
||||
for (const auto &selected : so.actionNames) {
|
||||
if (selected == action.name) {
|
||||
alreadySelected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!alreadySelected) {
|
||||
availableNamesStorage.push_back(action.name);
|
||||
availableNames.push_back(
|
||||
availableNamesStorage.back().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (!availableNames.empty()) {
|
||||
if (selectedAction >= (int)availableNames.size())
|
||||
selectedAction = 0;
|
||||
if (ImGui::Combo("##actionSelect", &selectedAction,
|
||||
availableNames.data(),
|
||||
(int)availableNames.size())) {
|
||||
// Selection changed
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Add")) {
|
||||
if (selectedAction >= 0 &&
|
||||
selectedAction <
|
||||
(int)availableNames.size()) {
|
||||
so.actionNames.push_back(
|
||||
availableNamesStorage
|
||||
[selectedAction]);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("All actions already selected");
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("No actions in database");
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
ImGui::PopID();
|
||||
return modified;
|
||||
}
|
||||
30
src/features/editScene/ui/SmartObjectEditor.hpp
Normal file
30
src/features/editScene/ui/SmartObjectEditor.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef EDITSCENE_SMART_OBJECT_EDITOR_HPP
|
||||
#define EDITSCENE_SMART_OBJECT_EDITOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/SmartObject.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
|
||||
/**
|
||||
* Editor for SmartObjectComponent.
|
||||
*
|
||||
* Allows editing radius, height, and selecting GOAP actions
|
||||
* from the ActionDatabase.
|
||||
*/
|
||||
class SmartObjectEditor : public ComponentEditor<SmartObjectComponent> {
|
||||
public:
|
||||
const char *getName() const override
|
||||
{
|
||||
return "Smart Object";
|
||||
}
|
||||
|
||||
protected:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
SmartObjectComponent &so) override;
|
||||
|
||||
private:
|
||||
ActionDatabase *findDatabase(flecs::entity entity);
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_SMART_OBJECT_EDITOR_HPP
|
||||
Reference in New Issue
Block a user