Compare commits
2 Commits
a75db85027
...
a1b74aa2d5
| Author | SHA1 | Date | |
|---|---|---|---|
| a1b74aa2d5 | |||
| c80d9c96e6 |
@@ -46,6 +46,15 @@ set(EDITSCENE_SOURCES
|
||||
systems/SmartObjectSystem.cpp
|
||||
components/SmartObjectModule.cpp
|
||||
ui/SmartObjectEditor.cpp
|
||||
components/GoapPlannerModule.cpp
|
||||
systems/GoapRunnerSystem.cpp
|
||||
systems/PathFollowingSystem.cpp
|
||||
systems/GoapPlannerSystem.cpp
|
||||
components/GoapRunnerModule.cpp
|
||||
components/PathFollowingModule.cpp
|
||||
ui/GoapRunnerEditor.cpp
|
||||
ui/PathFollowingEditor.cpp
|
||||
ui/GoapPlannerEditor.cpp
|
||||
systems/PrefabSystem.cpp
|
||||
ui/PrefabInstanceEditor.cpp
|
||||
|
||||
@@ -173,7 +182,16 @@ set(EDITSCENE_HEADERS
|
||||
systems/SmartObjectSystem.hpp
|
||||
components/SmartObject.hpp
|
||||
ui/SmartObjectEditor.hpp
|
||||
components/GoapPlanner.hpp
|
||||
components/PathFollowing.hpp
|
||||
components/GoapRunner.hpp
|
||||
ui/GoapPlannerEditor.hpp
|
||||
ui/GoapRunnerEditor.hpp
|
||||
ui/PathFollowingEditor.hpp
|
||||
systems/PrefabSystem.hpp
|
||||
systems/GoapRunnerSystem.hpp
|
||||
systems/PathFollowingSystem.hpp
|
||||
systems/GoapPlannerSystem.hpp
|
||||
components/PrefabInstance.hpp
|
||||
ui/PrefabInstanceEditor.hpp
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
#include "systems/NavMeshSystem.hpp"
|
||||
#include "systems/CharacterSystem.hpp"
|
||||
#include "systems/SmartObjectSystem.hpp"
|
||||
#include "systems/GoapRunnerSystem.hpp"
|
||||
#include "systems/PathFollowingSystem.hpp"
|
||||
#include "systems/GoapPlannerSystem.hpp"
|
||||
#include "systems/CellGridSystem.hpp"
|
||||
|
||||
#include "systems/NormalDebugSystem.hpp"
|
||||
@@ -66,6 +69,9 @@
|
||||
#include "systems/PrefabSystem.hpp"
|
||||
#include "components/NavMesh.hpp"
|
||||
#include "components/SmartObject.hpp"
|
||||
#include "components/GoapPlanner.hpp"
|
||||
#include "components/GoapRunner.hpp"
|
||||
#include "components/PathFollowing.hpp"
|
||||
|
||||
#include <OgreRTShaderSystem.h>
|
||||
#include <imgui.h>
|
||||
@@ -237,6 +243,7 @@ void EditorApp::setup()
|
||||
if (m_uiSystem)
|
||||
m_uiSystem->setEditorUIEnabled(m_gameMode ==
|
||||
GameMode::Editor);
|
||||
m_uiSystem->setEditorCamera(m_camera.get());
|
||||
|
||||
// Setup physics system
|
||||
m_physicsSystem = std::make_unique<EditorPhysicsSystem>(
|
||||
@@ -335,6 +342,25 @@ void EditorApp::setup()
|
||||
// Wire up EditorApp for game mode detection
|
||||
m_smartObjectSystem->setEditorApp(this);
|
||||
|
||||
// Setup GOAP Runner system
|
||||
m_goapRunnerSystem = std::make_unique<GoapRunnerSystem>(
|
||||
m_world, m_sceneMgr, m_smartObjectSystem.get(),
|
||||
m_behaviorTreeSystem.get(), m_navMeshSystem.get());
|
||||
m_goapRunnerSystem->setEditorApp(this);
|
||||
m_goapRunnerSystem->setAnimationTreeSystem(
|
||||
m_animationTreeSystem.get());
|
||||
|
||||
// Setup GOAP Planner system
|
||||
m_goapPlannerSystem = std::make_unique<GoapPlannerSystem>(
|
||||
m_world);
|
||||
m_goapPlannerSystem->setEditorApp(this);
|
||||
|
||||
// Setup Path Following system
|
||||
m_pathFollowingSystem = std::make_unique<PathFollowingSystem>(
|
||||
m_world, m_sceneMgr, m_navMeshSystem.get());
|
||||
m_pathFollowingSystem->setAnimationTreeSystem(
|
||||
m_animationTreeSystem.get());
|
||||
|
||||
// Setup CellGrid system
|
||||
m_cellGridSystem =
|
||||
std::make_unique<CellGridSystem>(m_world, m_sceneMgr);
|
||||
@@ -575,6 +601,15 @@ void EditorApp::setupECS()
|
||||
// Register Smart Object component
|
||||
m_world.component<SmartObjectComponent>();
|
||||
|
||||
// Register GOAP Planner component
|
||||
m_world.component<GoapPlannerComponent>();
|
||||
|
||||
// Register GOAP Runner component
|
||||
m_world.component<GoapRunnerComponent>();
|
||||
|
||||
// Register Path Following component
|
||||
m_world.component<PathFollowingComponent>();
|
||||
|
||||
// Register Navigation components
|
||||
m_world.component<NavMeshComponent>();
|
||||
m_world.component<NavMeshGeometrySource>();
|
||||
@@ -733,6 +768,9 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
if (m_behaviorTreeSystem)
|
||||
m_behaviorTreeSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
if (m_pathFollowingSystem) {
|
||||
m_pathFollowingSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
if (m_proceduralMeshSystem) {
|
||||
m_proceduralMeshSystem->update();
|
||||
}
|
||||
@@ -760,6 +798,16 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
m_smartObjectSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- GOAP Planner system (plan generation) --- */
|
||||
if (m_goapPlannerSystem) {
|
||||
m_goapPlannerSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- GOAP Runner system (plan execution) --- */
|
||||
if (m_goapRunnerSystem) {
|
||||
m_goapRunnerSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Dynamic physics (characters after static world) --- */
|
||||
|
||||
if (m_characterSystem) {
|
||||
|
||||
@@ -36,6 +36,9 @@ class EditorSkyboxSystem;
|
||||
class EditorWaterPlaneSystem;
|
||||
class NormalDebugSystem;
|
||||
class SmartObjectSystem;
|
||||
class GoapRunnerSystem;
|
||||
class PathFollowingSystem;
|
||||
class GoapPlannerSystem;
|
||||
class EditorApp;
|
||||
|
||||
/**
|
||||
@@ -222,6 +225,9 @@ private:
|
||||
std::unique_ptr<NormalDebugSystem> m_normalDebugSystem;
|
||||
std::unique_ptr<RoomLayoutSystem> m_roomLayoutSystem;
|
||||
std::unique_ptr<SmartObjectSystem> m_smartObjectSystem;
|
||||
std::unique_ptr<GoapRunnerSystem> m_goapRunnerSystem;
|
||||
std::unique_ptr<PathFollowingSystem> m_pathFollowingSystem;
|
||||
std::unique_ptr<GoapPlannerSystem> m_goapPlannerSystem;
|
||||
|
||||
// Game systems
|
||||
|
||||
|
||||
@@ -7,37 +7,13 @@
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* Configuration for one path following animation state.
|
||||
*
|
||||
* Each state (e.g. "idle", "walk", "run", "swim-idle", "swim", "swim-fast")
|
||||
* consists of unlimited name-value pairs where:
|
||||
* - name = state machine name (e.g. "main", "locomotion")
|
||||
* - value = state name within that machine (e.g. "Idle", "Walking")
|
||||
*
|
||||
* When a path following state is activated, ALL its name-value pairs
|
||||
* are applied via AnimationTreeSystem::setState().
|
||||
*/
|
||||
struct PathFollowingState {
|
||||
/** Logical name (e.g. "idle", "walk", "run", "swim-idle", "swim", "swim-fast") */
|
||||
Ogre::String name;
|
||||
|
||||
/** State machine name -> state name pairs */
|
||||
std::vector<std::pair<Ogre::String, Ogre::String> > stateMachineStates;
|
||||
|
||||
PathFollowingState() = default;
|
||||
|
||||
PathFollowingState(const Ogre::String &name_)
|
||||
: name(name_)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-character action debug component.
|
||||
*
|
||||
* Allows test-running individual actions and inspecting the character's
|
||||
* local blackboard state. Used for debugging AI behavior in the editor.
|
||||
*
|
||||
* Path following animation states have been moved to PathFollowingComponent.
|
||||
*/
|
||||
struct ActionDebug {
|
||||
// Character's local GOAP blackboard
|
||||
@@ -57,39 +33,7 @@ struct ActionDebug {
|
||||
// Debug output
|
||||
Ogre::String lastResult;
|
||||
|
||||
// --- Path following animation states ---
|
||||
// Each state has unlimited state machine name -> state name pairs.
|
||||
// Default entries: idle, walk, run
|
||||
std::vector<PathFollowingState> pathFollowingStates = {
|
||||
{ "idle" },
|
||||
{ "walk" },
|
||||
{ "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/state pairs for a given path following state name.
|
||||
* Returns nullptr if not found.
|
||||
*/
|
||||
const PathFollowingState *
|
||||
findPathState(const Ogre::String &stateName) const
|
||||
{
|
||||
for (const auto &state : pathFollowingStates) {
|
||||
if (state.name == stateName)
|
||||
return &state;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTION_DEBUG_HPP
|
||||
|
||||
@@ -21,6 +21,10 @@ struct GoapAction {
|
||||
GoapBlackboard preconditions;
|
||||
GoapBlackboard effects;
|
||||
|
||||
// Bitmask for precondition checking. Only bits set here are compared.
|
||||
// Defaults to all 1s (check all bits).
|
||||
uint64_t preconditionMask = ~0ULL;
|
||||
|
||||
// Behavior tree to execute when this action is selected
|
||||
BehaviorTreeNode behaviorTree;
|
||||
|
||||
@@ -35,10 +39,35 @@ struct GoapAction {
|
||||
{
|
||||
}
|
||||
|
||||
// Check if the given blackboard satisfies this action's preconditions
|
||||
// Check if the given blackboard satisfies this action's preconditions.
|
||||
// Only bits in preconditionMask are compared.
|
||||
bool canRun(const GoapBlackboard &blackboard) const
|
||||
{
|
||||
return blackboard.satisfies(preconditions);
|
||||
// Fast-path: if mask covers all set bits, use standard check
|
||||
if (preconditionMask == ~0ULL)
|
||||
return blackboard.satisfies(preconditions);
|
||||
|
||||
// Masked check: only compare bits in preconditionMask
|
||||
uint64_t relevantBits = preconditionMask;
|
||||
uint64_t bbBits = blackboard.bits & relevantBits;
|
||||
uint64_t preBits = preconditions.bits & relevantBits;
|
||||
uint64_t bbMask = blackboard.mask & relevantBits;
|
||||
uint64_t preMask = preconditions.mask & relevantBits;
|
||||
|
||||
// All precondition bits must be present in blackboard
|
||||
if ((bbMask & preMask) != preMask)
|
||||
return false;
|
||||
if ((bbBits & preMask) != preBits)
|
||||
return false;
|
||||
|
||||
// Check integer values
|
||||
for (const auto &kv : preconditions.values) {
|
||||
if (!blackboard.hasValue(kv.first))
|
||||
return false;
|
||||
if (blackboard.getValue(kv.first) != kv.second)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -42,12 +42,17 @@ int GoapBlackboard::findBitByName(const std::string &name)
|
||||
|
||||
bool GoapBlackboard::satisfies(const GoapBlackboard &other) const
|
||||
{
|
||||
// Check bits: for every bit set in other's mask, our bit must match
|
||||
uint64_t commonMask = mask & other.mask;
|
||||
// Only compare bits that both sides consider relevant
|
||||
uint64_t relevantMask = bitmask & other.bitmask;
|
||||
|
||||
// Check bits: for every bit set in other's mask (within relevant mask),
|
||||
// our bit must match
|
||||
uint64_t commonMask = mask & other.mask & relevantMask;
|
||||
if ((bits & commonMask) != (other.bits & commonMask))
|
||||
return false;
|
||||
// Also check bits that other has set but we don't
|
||||
uint64_t missingMask = other.mask & ~mask;
|
||||
|
||||
// Also check bits that other has set but we don't (within relevant mask)
|
||||
uint64_t missingMask = other.mask & ~mask & relevantMask;
|
||||
if (missingMask) {
|
||||
// Other requires bits we don't have set -> fail
|
||||
// But only if other has those bits set to 1
|
||||
@@ -97,20 +102,23 @@ bool GoapBlackboard::getScalarValue(const std::string &key,
|
||||
}
|
||||
|
||||
int GoapBlackboard::distanceTo(const GoapBlackboard &target,
|
||||
bool ignoreValues) const
|
||||
bool ignoreValues) const
|
||||
{
|
||||
int distance = 0;
|
||||
|
||||
// Bit differences
|
||||
uint64_t commonMask = mask & target.mask;
|
||||
// Only compare bits that both sides consider relevant
|
||||
uint64_t relevantMask = bitmask & target.bitmask;
|
||||
|
||||
// Bit differences (within relevant mask)
|
||||
uint64_t commonMask = mask & target.mask & relevantMask;
|
||||
distance += __builtin_popcountll((bits ^ target.bits) & commonMask);
|
||||
|
||||
// Bits target cares about but we don't have
|
||||
uint64_t missingInUs = target.mask & ~mask;
|
||||
// Bits target cares about but we don't have (within relevant mask)
|
||||
uint64_t missingInUs = target.mask & ~mask & relevantMask;
|
||||
distance += __builtin_popcountll(target.bits & missingInUs);
|
||||
|
||||
// Bits we care about but target doesn't (we may need to unset them)
|
||||
uint64_t missingInTarget = mask & ~target.mask;
|
||||
// Bits we care about but target doesn't (within relevant mask)
|
||||
uint64_t missingInTarget = mask & ~target.mask & relevantMask;
|
||||
distance += __builtin_popcountll(bits & missingInTarget);
|
||||
|
||||
if (ignoreValues)
|
||||
|
||||
@@ -22,6 +22,10 @@ struct GoapBlackboard {
|
||||
uint64_t bits = 0;
|
||||
uint64_t mask = 0; // which bits are actually set
|
||||
|
||||
// Bitmask for comparison: only bits set here are compared.
|
||||
// Defaults to all 1s (compare all bits).
|
||||
uint64_t bitmask = ~0ULL;
|
||||
|
||||
// Named integer values (health, hunger, etc.) — used by preconditions/effects
|
||||
std::unordered_map<std::string, int> values;
|
||||
|
||||
@@ -184,6 +188,7 @@ struct GoapBlackboard {
|
||||
bool operator==(const GoapBlackboard &other) const
|
||||
{
|
||||
return bits == other.bits && mask == other.mask &&
|
||||
bitmask == other.bitmask &&
|
||||
values == other.values &&
|
||||
floatValues == other.floatValues &&
|
||||
vec3Values == other.vec3Values;
|
||||
|
||||
99
src/features/editScene/components/GoapPlanner.hpp
Normal file
99
src/features/editScene/components/GoapPlanner.hpp
Normal file
@@ -0,0 +1,99 @@
|
||||
#ifndef EDITSCENE_GOAP_PLANNER_HPP
|
||||
#define EDITSCENE_GOAP_PLANNER_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <queue>
|
||||
|
||||
/**
|
||||
* GOAP Planner component.
|
||||
*
|
||||
* Holds a curated list of action and goal names from an ActionDatabase,
|
||||
* plus configuration for smart-object action discovery.
|
||||
* The planner resolves names against an ActionDatabase at runtime.
|
||||
*
|
||||
* The actionNames and goalNames lists act as external references:
|
||||
* prefabs can store them even when the ActionDatabase is not
|
||||
* part of the prefab itself.
|
||||
*/
|
||||
struct GoapPlannerComponent {
|
||||
// Selected action names from ActionDatabase
|
||||
std::vector<Ogre::String> actionNames;
|
||||
|
||||
// Selected goal names from ActionDatabase
|
||||
std::vector<Ogre::String> goalNames;
|
||||
|
||||
// Maximum distance to search for smart objects with matching actions
|
||||
float smartObjectDistance = 50.0f;
|
||||
|
||||
// Whether to include smart object actions in planning
|
||||
bool includeSmartObjects = true;
|
||||
|
||||
// Optional reference to an external ActionDatabase entity by name.
|
||||
Ogre::String actionDatabaseRef;
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Runtime plan queue (not serialized)
|
||||
// -----------------------------------------------------------------
|
||||
struct Plan {
|
||||
std::vector<Ogre::String> actions;
|
||||
int totalCost = 0;
|
||||
Ogre::String goalName;
|
||||
};
|
||||
std::vector<Plan> planQueue;
|
||||
|
||||
// Planner status
|
||||
enum class Status {
|
||||
Idle, // No planning requested
|
||||
Planning, // Currently planning
|
||||
PlansAvailable, // One or more plans in queue
|
||||
NoPlanFound // Planning finished but no valid plan found
|
||||
};
|
||||
Status status = Status::Idle;
|
||||
|
||||
// Goal name that was used for the current plan batch
|
||||
Ogre::String currentGoalName;
|
||||
|
||||
// Planning control
|
||||
bool planDirty = true;
|
||||
int maxPlans = 3; // stop after generating this many plans
|
||||
|
||||
// Planning progress (for status display)
|
||||
int plansGenerated = 0;
|
||||
int nodesExplored = 0;
|
||||
|
||||
GoapPlannerComponent() = default;
|
||||
|
||||
void clearPlans()
|
||||
{
|
||||
planQueue.clear();
|
||||
plansGenerated = 0;
|
||||
status = Status::Idle;
|
||||
}
|
||||
|
||||
// Pop the cheapest plan from the queue
|
||||
Plan popCheapestPlan()
|
||||
{
|
||||
if (planQueue.empty())
|
||||
return Plan();
|
||||
size_t bestIdx = 0;
|
||||
for (size_t i = 1; i < planQueue.size(); i++) {
|
||||
if (planQueue[i].totalCost < planQueue[bestIdx].totalCost)
|
||||
bestIdx = i;
|
||||
}
|
||||
Plan result = std::move(planQueue[bestIdx]);
|
||||
planQueue.erase(planQueue.begin() + bestIdx);
|
||||
if (planQueue.empty() && status == Status::PlansAvailable)
|
||||
status = Status::Idle;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool hasPlans() const
|
||||
{
|
||||
return !planQueue.empty();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GOAP_PLANNER_HPP
|
||||
21
src/features/editScene/components/GoapPlannerModule.cpp
Normal file
21
src/features/editScene/components/GoapPlannerModule.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "GoapPlanner.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/GoapPlannerEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("GOAP Planner", "AI", GoapPlannerComponent,
|
||||
GoapPlannerEditor)
|
||||
{
|
||||
registry.registerComponent<GoapPlannerComponent>(
|
||||
"GOAP Planner", "AI",
|
||||
std::make_unique<GoapPlannerEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<GoapPlannerComponent>())
|
||||
e.set<GoapPlannerComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<GoapPlannerComponent>())
|
||||
e.remove<GoapPlannerComponent>();
|
||||
});
|
||||
}
|
||||
50
src/features/editScene/components/GoapRunner.hpp
Normal file
50
src/features/editScene/components/GoapRunner.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#ifndef EDITSCENE_GOAP_RUNNER_HPP
|
||||
#define EDITSCENE_GOAP_RUNNER_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* GOAP Runner component.
|
||||
*
|
||||
* Executes plans from GoapPlannerComponent.
|
||||
* For normal actions: runs the action's behavior tree.
|
||||
* For smart object actions: pathfinds to the smart object and executes.
|
||||
* After plan completion, marks the planner dirty for replanning.
|
||||
*/
|
||||
struct GoapRunnerComponent {
|
||||
// Current plan execution state
|
||||
enum class State {
|
||||
Idle, // No plan running
|
||||
RunningAction, // Executing a normal action
|
||||
MovingToSmartObject, // Pathfinding to a smart object
|
||||
ExecutingSmartObject, // Executing smart object action
|
||||
PlanComplete // Plan finished, waiting for replan
|
||||
};
|
||||
|
||||
State state = State::Idle;
|
||||
|
||||
// Index of current action in the plan
|
||||
int currentActionIndex = 0;
|
||||
|
||||
// Name of the currently executing action
|
||||
Ogre::String currentActionName;
|
||||
|
||||
// Timer for action execution
|
||||
float actionTimer = 0.0f;
|
||||
|
||||
// Entity ID of target smart object (if applicable)
|
||||
uint64_t targetSmartObjectId = 0;
|
||||
|
||||
// Active plan actions (copied from planner when plan starts)
|
||||
std::vector<Ogre::String> planActions;
|
||||
|
||||
// Whether to auto-replan after completion
|
||||
bool autoReplan = true;
|
||||
|
||||
GoapRunnerComponent() = default;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GOAP_RUNNER_HPP
|
||||
21
src/features/editScene/components/GoapRunnerModule.cpp
Normal file
21
src/features/editScene/components/GoapRunnerModule.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "GoapRunner.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/GoapRunnerEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("GOAP Runner", "AI", GoapRunnerComponent,
|
||||
GoapRunnerEditor)
|
||||
{
|
||||
registry.registerComponent<GoapRunnerComponent>(
|
||||
"GOAP Runner", "AI",
|
||||
std::make_unique<GoapRunnerEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<GoapRunnerComponent>())
|
||||
e.set<GoapRunnerComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<GoapRunnerComponent>())
|
||||
e.remove<GoapRunnerComponent>();
|
||||
});
|
||||
}
|
||||
77
src/features/editScene/components/PathFollowing.hpp
Normal file
77
src/features/editScene/components/PathFollowing.hpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#ifndef EDITSCENE_PATH_FOLLOWING_HPP
|
||||
#define EDITSCENE_PATH_FOLLOWING_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
* Animation state configuration for path following.
|
||||
*
|
||||
* Each state (e.g. "idle", "walk", "run") consists of unlimited
|
||||
* state-machine-name -> state-name pairs applied via AnimationTreeSystem.
|
||||
*/
|
||||
struct PathFollowingState {
|
||||
Ogre::String name;
|
||||
std::vector<std::pair<Ogre::String, Ogre::String> > stateMachineStates;
|
||||
|
||||
PathFollowingState() = default;
|
||||
explicit PathFollowingState(const Ogre::String &name_)
|
||||
: name(name_)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Path Following component.
|
||||
*
|
||||
* Configures animation states for locomotion (idle/walk/run)
|
||||
* and stores the current locomotion state. Used by GoapRunner
|
||||
* to animate characters during plan execution.
|
||||
*/
|
||||
struct PathFollowingComponent {
|
||||
// Animation state configurations
|
||||
std::vector<PathFollowingState> pathFollowingStates = {
|
||||
PathFollowingState("idle"),
|
||||
PathFollowingState("walk"),
|
||||
PathFollowingState("run"),
|
||||
};
|
||||
|
||||
// Current locomotion state name
|
||||
Ogre::String currentLocomotionState = "idle";
|
||||
|
||||
// Walk speed (m/s) for root motion scaling
|
||||
float walkSpeed = 2.5f;
|
||||
|
||||
// Run speed (m/s) for root motion scaling
|
||||
float runSpeed = 5.0f;
|
||||
|
||||
// Whether to use root motion
|
||||
bool useRootMotion = true;
|
||||
|
||||
// Target position for path following (set by GoapRunner)
|
||||
Ogre::Vector3 targetPosition = Ogre::Vector3::ZERO;
|
||||
|
||||
// Whether we have an active target
|
||||
bool hasTarget = false;
|
||||
|
||||
// Path waypoints (set by GoapRunner, followed by PathFollowingSystem)
|
||||
std::vector<Ogre::Vector3> path;
|
||||
int pathIndex = 0;
|
||||
float pathRecalcTimer = 0.0f;
|
||||
|
||||
PathFollowingComponent() = default;
|
||||
|
||||
const PathFollowingState *findState(const Ogre::String &name) const
|
||||
{
|
||||
for (const auto &state : pathFollowingStates) {
|
||||
if (state.name == name)
|
||||
return &state;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_PATH_FOLLOWING_HPP
|
||||
21
src/features/editScene/components/PathFollowingModule.cpp
Normal file
21
src/features/editScene/components/PathFollowingModule.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "PathFollowing.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/PathFollowingEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Path Following", "AI", PathFollowingComponent,
|
||||
PathFollowingEditor)
|
||||
{
|
||||
registry.registerComponent<PathFollowingComponent>(
|
||||
"Path Following", "AI",
|
||||
std::make_unique<PathFollowingEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<PathFollowingComponent>())
|
||||
e.set<PathFollowingComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<PathFollowingComponent>())
|
||||
e.remove<PathFollowingComponent>();
|
||||
});
|
||||
}
|
||||
@@ -2,38 +2,68 @@
|
||||
#include "../components/Transform.hpp"
|
||||
#include <cmath>
|
||||
|
||||
// Colors for cursor - bright cyan/white for visibility
|
||||
static const float COLOR_CYAN[3] = { 0.0f, 1.0f, 1.0f };
|
||||
static const float COLOR_WHITE[3] = { 1.0f, 1.0f, 1.0f };
|
||||
static const float COLOR_RED[3] = { 1.0f, 0.2f, 0.2f };
|
||||
static const float COLOR_GREEN[3] = { 0.2f, 1.0f, 0.2f };
|
||||
static const float COLOR_BLUE[3] = { 0.2f, 0.4f, 1.0f };
|
||||
static const float COLOR_CYAN[3] = { 0.0f, 1.0f, 1.0f };
|
||||
static const float COLOR_YELLOW[3] = { 1.0f, 1.0f, 0.0f };
|
||||
|
||||
static bool projectRayOntoAxis(const Ogre::Ray &ray,
|
||||
const Ogre::Vector3 &axisOrigin,
|
||||
const Ogre::Vector3 &axisDir, float &outT)
|
||||
{
|
||||
Ogre::Vector3 rayOrigin = ray.getOrigin();
|
||||
Ogre::Vector3 rayDir = ray.getDirection();
|
||||
Ogre::Vector3 w0 = rayOrigin - axisOrigin;
|
||||
float a = rayDir.dotProduct(rayDir);
|
||||
float b = rayDir.dotProduct(axisDir);
|
||||
float c = axisDir.dotProduct(axisDir);
|
||||
float d = rayDir.dotProduct(w0);
|
||||
float e = axisDir.dotProduct(w0);
|
||||
float denom = a * c - b * b;
|
||||
if (std::abs(denom) < 0.0001f)
|
||||
return false;
|
||||
outT = (a * e - b * d) / denom;
|
||||
return true;
|
||||
}
|
||||
|
||||
Cursor3D::Cursor3D(Ogre::SceneManager *sceneMgr)
|
||||
: m_sceneMgr(sceneMgr)
|
||||
, m_cursorNode(nullptr)
|
||||
, m_axesObj(nullptr)
|
||||
, m_markerObj(nullptr)
|
||||
, m_axisX(nullptr)
|
||||
, m_axisY(nullptr)
|
||||
, m_axisZ(nullptr)
|
||||
, m_centerMarker(nullptr)
|
||||
, m_position(Ogre::Vector3::ZERO)
|
||||
, m_orientation(Ogre::Quaternion::IDENTITY)
|
||||
, m_size(1.0f)
|
||||
, m_visible(false)
|
||||
, m_mode(Mode::Translate)
|
||||
, m_selectedAxis(Axis::None)
|
||||
, m_hoveredAxis(Axis::None)
|
||||
, m_isDragging(false)
|
||||
, m_dragStartT(0.0f)
|
||||
, m_dragStartAngle(0.0f)
|
||||
{
|
||||
m_cursorNode = m_sceneMgr->getRootSceneNode()->createChildSceneNode(
|
||||
"Cursor3DNode");
|
||||
|
||||
m_axesObj = m_sceneMgr->createManualObject("Cursor3DAxes");
|
||||
m_markerObj = m_sceneMgr->createManualObject("Cursor3DMarker");
|
||||
m_axisX = m_sceneMgr->createManualObject("CursorAxisX");
|
||||
m_axisY = m_sceneMgr->createManualObject("CursorAxisY");
|
||||
m_axisZ = m_sceneMgr->createManualObject("CursorAxisZ");
|
||||
m_centerMarker = m_sceneMgr->createManualObject("CursorCenter");
|
||||
|
||||
m_cursorNode->attachObject(m_axesObj);
|
||||
m_cursorNode->attachObject(m_markerObj);
|
||||
m_cursorNode->attachObject(m_axisX);
|
||||
m_cursorNode->attachObject(m_axisY);
|
||||
m_cursorNode->attachObject(m_axisZ);
|
||||
m_cursorNode->attachObject(m_centerMarker);
|
||||
|
||||
// Draw on top of everything
|
||||
m_axesObj->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
m_markerObj->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
m_axisX->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
m_axisY->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
m_axisZ->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
m_centerMarker->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
|
||||
m_cursorNode->setVisible(false);
|
||||
|
||||
createGeometry();
|
||||
}
|
||||
|
||||
@@ -41,35 +71,34 @@ void Cursor3D::shutdown()
|
||||
{
|
||||
if (!m_sceneMgr)
|
||||
return;
|
||||
|
||||
if (m_cursorNode && m_axesObj) {
|
||||
try {
|
||||
m_cursorNode->detachObject(m_axesObj);
|
||||
} catch (...) {
|
||||
auto detach = [&](Ogre::ManualObject *obj) {
|
||||
if (m_cursorNode && obj) {
|
||||
try {
|
||||
m_cursorNode->detachObject(obj);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_cursorNode && m_markerObj) {
|
||||
try {
|
||||
m_cursorNode->detachObject(m_markerObj);
|
||||
} catch (...) {
|
||||
};
|
||||
auto destroy = [&](Ogre::ManualObject *obj) {
|
||||
if (obj) {
|
||||
try {
|
||||
m_sceneMgr->destroyManualObject(obj);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_axesObj) {
|
||||
try {
|
||||
m_sceneMgr->destroyManualObject(m_axesObj);
|
||||
} catch (...) {
|
||||
}
|
||||
m_axesObj = nullptr;
|
||||
}
|
||||
if (m_markerObj) {
|
||||
try {
|
||||
m_sceneMgr->destroyManualObject(m_markerObj);
|
||||
} catch (...) {
|
||||
}
|
||||
m_markerObj = nullptr;
|
||||
}
|
||||
|
||||
};
|
||||
detach(m_axisX);
|
||||
detach(m_axisY);
|
||||
detach(m_axisZ);
|
||||
detach(m_centerMarker);
|
||||
destroy(m_axisX);
|
||||
m_axisX = nullptr;
|
||||
destroy(m_axisY);
|
||||
m_axisY = nullptr;
|
||||
destroy(m_axisZ);
|
||||
m_axisZ = nullptr;
|
||||
destroy(m_centerMarker);
|
||||
m_centerMarker = nullptr;
|
||||
if (m_cursorNode) {
|
||||
try {
|
||||
m_sceneMgr->destroySceneNode(m_cursorNode);
|
||||
@@ -77,7 +106,6 @@ void Cursor3D::shutdown()
|
||||
}
|
||||
m_cursorNode = nullptr;
|
||||
}
|
||||
|
||||
m_sceneMgr = nullptr;
|
||||
}
|
||||
|
||||
@@ -133,8 +161,7 @@ void Cursor3D::applyToTransform(TransformComponent &transform) const
|
||||
{
|
||||
if (!transform.node)
|
||||
return;
|
||||
|
||||
Ogre::SceneNode *parent = static_cast<Ogre::SceneNode*>(
|
||||
Ogre::SceneNode *parent = static_cast<Ogre::SceneNode *>(
|
||||
transform.node->getParent());
|
||||
if (parent) {
|
||||
transform.position = parent->convertWorldToLocalPosition(
|
||||
@@ -149,22 +176,6 @@ void Cursor3D::applyToTransform(TransformComponent &transform) const
|
||||
transform.markChanged();
|
||||
}
|
||||
|
||||
bool Cursor3D::hitTest(const Ogre::Ray &mouseRay) const
|
||||
{
|
||||
if (!m_cursorNode || !m_visible)
|
||||
return false;
|
||||
|
||||
// Check if ray passes near cursor center
|
||||
Ogre::Vector3 toCursor = m_position - mouseRay.getOrigin();
|
||||
float tca = toCursor.dotProduct(mouseRay.getDirection());
|
||||
if (tca < 0.01f)
|
||||
return false;
|
||||
|
||||
float d2 = toCursor.dotProduct(toCursor) - tca * tca;
|
||||
float threshold = 0.3f * m_size;
|
||||
return d2 <= threshold * threshold;
|
||||
}
|
||||
|
||||
void Cursor3D::updateNodeTransform()
|
||||
{
|
||||
if (m_cursorNode) {
|
||||
@@ -178,36 +189,49 @@ void Cursor3D::createGeometry()
|
||||
float len = 0.5f * m_size;
|
||||
float half = 0.05f * m_size;
|
||||
|
||||
// Axes - short lines in RGB
|
||||
m_axesObj->clear();
|
||||
m_axesObj->begin("Ogre/AxisGizmo",
|
||||
Ogre::RenderOperation::OT_LINE_LIST);
|
||||
// X Axis
|
||||
bool xSel = (m_selectedAxis == Axis::X || m_hoveredAxis == Axis::X);
|
||||
m_axisX->clear();
|
||||
m_axisX->begin("Ogre/AxisGizmo",
|
||||
Ogre::RenderOperation::OT_LINE_LIST);
|
||||
m_axisX->colour(xSel ? COLOR_YELLOW[0] : COLOR_RED[0],
|
||||
xSel ? COLOR_YELLOW[1] : COLOR_RED[1],
|
||||
xSel ? COLOR_YELLOW[2] : COLOR_RED[2]);
|
||||
m_axisX->position(0, 0, 0);
|
||||
m_axisX->position(len, 0, 0);
|
||||
m_axisX->end();
|
||||
|
||||
// X axis - red
|
||||
m_axesObj->colour(COLOR_RED[0], COLOR_RED[1], COLOR_RED[2]);
|
||||
m_axesObj->position(0, 0, 0);
|
||||
m_axesObj->position(len, 0, 0);
|
||||
// Y Axis
|
||||
bool ySel = (m_selectedAxis == Axis::Y || m_hoveredAxis == Axis::Y);
|
||||
m_axisY->clear();
|
||||
m_axisY->begin("Ogre/AxisGizmo",
|
||||
Ogre::RenderOperation::OT_LINE_LIST);
|
||||
m_axisY->colour(ySel ? COLOR_YELLOW[0] : COLOR_GREEN[0],
|
||||
ySel ? COLOR_YELLOW[1] : COLOR_GREEN[1],
|
||||
ySel ? COLOR_YELLOW[2] : COLOR_GREEN[2]);
|
||||
m_axisY->position(0, 0, 0);
|
||||
m_axisY->position(0, len, 0);
|
||||
m_axisY->end();
|
||||
|
||||
// Y axis - green
|
||||
m_axesObj->colour(COLOR_GREEN[0], COLOR_GREEN[1], COLOR_GREEN[2]);
|
||||
m_axesObj->position(0, 0, 0);
|
||||
m_axesObj->position(0, len, 0);
|
||||
// Z Axis
|
||||
bool zSel = (m_selectedAxis == Axis::Z || m_hoveredAxis == Axis::Z);
|
||||
m_axisZ->clear();
|
||||
m_axisZ->begin("Ogre/AxisGizmo",
|
||||
Ogre::RenderOperation::OT_LINE_LIST);
|
||||
m_axisZ->colour(zSel ? COLOR_YELLOW[0] : COLOR_BLUE[0],
|
||||
zSel ? COLOR_YELLOW[1] : COLOR_BLUE[1],
|
||||
zSel ? COLOR_YELLOW[2] : COLOR_BLUE[2]);
|
||||
m_axisZ->position(0, 0, 0);
|
||||
m_axisZ->position(0, 0, len);
|
||||
m_axisZ->end();
|
||||
|
||||
// Z axis - blue
|
||||
m_axesObj->colour(COLOR_BLUE[0], COLOR_BLUE[1], COLOR_BLUE[2]);
|
||||
m_axesObj->position(0, 0, 0);
|
||||
m_axesObj->position(0, 0, len);
|
||||
|
||||
m_axesObj->end();
|
||||
|
||||
// Center marker - small wireframe cube in cyan
|
||||
m_markerObj->clear();
|
||||
m_markerObj->begin("Ogre/AxisGizmo",
|
||||
Ogre::RenderOperation::OT_LINE_LIST);
|
||||
m_markerObj->colour(COLOR_CYAN[0], COLOR_CYAN[1], COLOR_CYAN[2]);
|
||||
|
||||
// Cube corners
|
||||
Ogre::Vector3 corners[8] = {
|
||||
// Center marker
|
||||
m_centerMarker->clear();
|
||||
m_centerMarker->begin("Ogre/AxisGizmo",
|
||||
Ogre::RenderOperation::OT_LINE_LIST);
|
||||
m_centerMarker->colour(COLOR_CYAN[0], COLOR_CYAN[1],
|
||||
COLOR_CYAN[2]);
|
||||
Ogre::Vector3 c[8] = {
|
||||
Ogre::Vector3(-half, -half, -half),
|
||||
Ogre::Vector3(half, -half, -half),
|
||||
Ogre::Vector3(half, half, -half),
|
||||
@@ -217,36 +241,169 @@ void Cursor3D::createGeometry()
|
||||
Ogre::Vector3(half, half, half),
|
||||
Ogre::Vector3(-half, half, half),
|
||||
};
|
||||
|
||||
// Bottom face
|
||||
m_markerObj->position(corners[0]);
|
||||
m_markerObj->position(corners[1]);
|
||||
m_markerObj->position(corners[1]);
|
||||
m_markerObj->position(corners[2]);
|
||||
m_markerObj->position(corners[2]);
|
||||
m_markerObj->position(corners[3]);
|
||||
m_markerObj->position(corners[3]);
|
||||
m_markerObj->position(corners[0]);
|
||||
|
||||
// Top face
|
||||
m_markerObj->position(corners[4]);
|
||||
m_markerObj->position(corners[5]);
|
||||
m_markerObj->position(corners[5]);
|
||||
m_markerObj->position(corners[6]);
|
||||
m_markerObj->position(corners[6]);
|
||||
m_markerObj->position(corners[7]);
|
||||
m_markerObj->position(corners[7]);
|
||||
m_markerObj->position(corners[4]);
|
||||
|
||||
// Vertical edges
|
||||
m_markerObj->position(corners[0]);
|
||||
m_markerObj->position(corners[4]);
|
||||
m_markerObj->position(corners[1]);
|
||||
m_markerObj->position(corners[5]);
|
||||
m_markerObj->position(corners[2]);
|
||||
m_markerObj->position(corners[6]);
|
||||
m_markerObj->position(corners[3]);
|
||||
m_markerObj->position(corners[7]);
|
||||
|
||||
m_markerObj->end();
|
||||
auto line = [&](int a, int b) {
|
||||
m_centerMarker->position(c[a]);
|
||||
m_centerMarker->position(c[b]);
|
||||
};
|
||||
line(0, 1);
|
||||
line(1, 2);
|
||||
line(2, 3);
|
||||
line(3, 0);
|
||||
line(4, 5);
|
||||
line(5, 6);
|
||||
line(6, 7);
|
||||
line(7, 4);
|
||||
line(0, 4);
|
||||
line(1, 5);
|
||||
line(2, 6);
|
||||
line(3, 7);
|
||||
m_centerMarker->end();
|
||||
}
|
||||
|
||||
Cursor3D::Axis Cursor3D::hitTest(const Ogre::Ray &mouseRay)
|
||||
{
|
||||
if (!m_cursorNode || !m_axisX->isVisible())
|
||||
return Axis::None;
|
||||
|
||||
Ogre::Vector3 cursorPos = m_cursorNode->getPosition();
|
||||
float len = 0.5f * m_size;
|
||||
float threshold = 0.15f * m_size;
|
||||
|
||||
float bestDist = 1000000.0f;
|
||||
Axis bestAxis = Axis::None;
|
||||
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
Ogre::Vector3 axisDir;
|
||||
if (i == 0)
|
||||
axisDir = m_cursorNode->getOrientation() *
|
||||
Ogre::Vector3::UNIT_X;
|
||||
else if (i == 1)
|
||||
axisDir = m_cursorNode->getOrientation() *
|
||||
Ogre::Vector3::UNIT_Y;
|
||||
else
|
||||
axisDir = m_cursorNode->getOrientation() *
|
||||
Ogre::Vector3::UNIT_Z;
|
||||
|
||||
for (int j = 0; j <= 8; ++j) {
|
||||
float t = len * j / 8.0f;
|
||||
Ogre::Vector3 pointOnAxis = cursorPos + axisDir * t;
|
||||
Ogre::Vector3 L = pointOnAxis - mouseRay.getOrigin();
|
||||
float tca = L.dotProduct(mouseRay.getDirection());
|
||||
if (tca < 0.01f)
|
||||
continue;
|
||||
float d2 = L.dotProduct(L) - tca * tca;
|
||||
if (d2 <= threshold * threshold) {
|
||||
if (tca < bestDist) {
|
||||
bestDist = tca;
|
||||
bestAxis = static_cast<Axis>(i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestAxis;
|
||||
}
|
||||
|
||||
bool Cursor3D::onMousePressed(const Ogre::Ray &mouseRay,
|
||||
const Ogre::Camera *camera)
|
||||
{
|
||||
(void)camera;
|
||||
if (!m_axisX->isVisible())
|
||||
return false;
|
||||
|
||||
m_selectedAxis = hitTest(mouseRay);
|
||||
|
||||
if (m_selectedAxis != Axis::None) {
|
||||
m_isDragging = true;
|
||||
m_dragStartPosition = m_position;
|
||||
m_dragStartRotation = m_orientation;
|
||||
|
||||
Ogre::Vector3 axisDir;
|
||||
switch (m_selectedAxis) {
|
||||
case Axis::X:
|
||||
axisDir = m_orientation * Ogre::Vector3::UNIT_X;
|
||||
break;
|
||||
case Axis::Y:
|
||||
axisDir = m_orientation * Ogre::Vector3::UNIT_Y;
|
||||
break;
|
||||
case Axis::Z:
|
||||
axisDir = m_orientation * Ogre::Vector3::UNIT_Z;
|
||||
break;
|
||||
default:
|
||||
axisDir = Ogre::Vector3::UNIT_X;
|
||||
break;
|
||||
}
|
||||
m_dragAxisDir = axisDir;
|
||||
|
||||
if (m_mode == Mode::Translate) {
|
||||
projectRayOntoAxis(mouseRay, m_dragStartPosition,
|
||||
m_dragAxisDir, m_dragStartT);
|
||||
} else if (m_mode == Mode::Rotate) {
|
||||
(void)camera;
|
||||
m_dragStartAngle = 0.0f;
|
||||
}
|
||||
|
||||
createGeometry();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Cursor3D::onMouseMoved(const Ogre::Ray &mouseRay,
|
||||
const Ogre::Vector2 &mouseDelta)
|
||||
{
|
||||
if (m_isDragging && m_selectedAxis != Axis::None) {
|
||||
if (m_mode == Mode::Translate) {
|
||||
float currentT;
|
||||
if (!projectRayOntoAxis(mouseRay, m_dragStartPosition,
|
||||
m_dragAxisDir, currentT)) {
|
||||
return true;
|
||||
}
|
||||
float deltaT = currentT - m_dragStartT;
|
||||
Ogre::Vector3 newPos =
|
||||
m_dragStartPosition + m_dragAxisDir * deltaT;
|
||||
setPosition(newPos);
|
||||
return true;
|
||||
} else if (m_mode == Mode::Rotate) {
|
||||
// Simple axis-based rotation from mouse delta
|
||||
float deltaAngle = 0.0f;
|
||||
if (m_selectedAxis == Axis::X) {
|
||||
// Up/down rotates around X
|
||||
deltaAngle = mouseDelta.y * 0.5f;
|
||||
} else if (m_selectedAxis == Axis::Y) {
|
||||
// Left/right rotates around Y
|
||||
deltaAngle = -mouseDelta.x * 0.5f;
|
||||
} else if (m_selectedAxis == Axis::Z) {
|
||||
// Left/right rotates around Z
|
||||
deltaAngle = -mouseDelta.x * 0.5f;
|
||||
}
|
||||
m_dragStartAngle += deltaAngle;
|
||||
|
||||
Ogre::Quaternion rot(
|
||||
Ogre::Degree(m_dragStartAngle),
|
||||
m_dragAxisDir);
|
||||
Ogre::Quaternion newRot = rot * m_dragStartRotation;
|
||||
setOrientation(newRot);
|
||||
return true;
|
||||
}
|
||||
} else if (m_axisX->isVisible()) {
|
||||
Axis prevHover = m_hoveredAxis;
|
||||
m_hoveredAxis = hitTest(mouseRay);
|
||||
if (prevHover != m_hoveredAxis)
|
||||
createGeometry();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Cursor3D::onMouseReleased()
|
||||
{
|
||||
if (m_isDragging) {
|
||||
m_isDragging = false;
|
||||
m_selectedAxis = Axis::None;
|
||||
m_hoveredAxis = Axis::None;
|
||||
createGeometry();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9,80 +9,88 @@
|
||||
struct TransformComponent;
|
||||
|
||||
/**
|
||||
* 3D Cursor - a visual marker for prefab placement and transform reference
|
||||
* Shows a small crosshair with axis indicators in world space
|
||||
* 3D Cursor - a visual marker for prefab placement and transform reference.
|
||||
* Supports axis-based translation and rotation interaction like the gizmo.
|
||||
*/
|
||||
class Cursor3D {
|
||||
public:
|
||||
enum class Mode {
|
||||
Translate,
|
||||
Rotate
|
||||
};
|
||||
|
||||
enum class Axis {
|
||||
None,
|
||||
X,
|
||||
Y,
|
||||
Z
|
||||
};
|
||||
|
||||
Cursor3D(Ogre::SceneManager *sceneMgr);
|
||||
~Cursor3D();
|
||||
|
||||
/**
|
||||
* Shutdown and cleanup - must be called before SceneManager is destroyed
|
||||
*/
|
||||
void shutdown();
|
||||
|
||||
/**
|
||||
* Set world position
|
||||
*/
|
||||
void setPosition(const Ogre::Vector3 &pos);
|
||||
Ogre::Vector3 getPosition() const
|
||||
{
|
||||
return m_position;
|
||||
}
|
||||
Ogre::Vector3 getPosition() const { return m_position; }
|
||||
|
||||
/**
|
||||
* Set world orientation
|
||||
*/
|
||||
void setOrientation(const Ogre::Quaternion &rot);
|
||||
Ogre::Quaternion getOrientation() const
|
||||
{
|
||||
return m_orientation;
|
||||
}
|
||||
Ogre::Quaternion getOrientation() const { return m_orientation; }
|
||||
|
||||
/**
|
||||
* Show/hide cursor
|
||||
*/
|
||||
void setVisible(bool visible);
|
||||
bool isVisible() const;
|
||||
|
||||
/**
|
||||
* Set cursor size/scale
|
||||
*/
|
||||
void setSize(float size);
|
||||
float getSize() const
|
||||
{
|
||||
return m_size;
|
||||
}
|
||||
float getSize() const { return m_size; }
|
||||
|
||||
void setMode(Mode mode) { m_mode = mode; }
|
||||
Mode getMode() const { return m_mode; }
|
||||
|
||||
/**
|
||||
* Copy position and orientation from a TransformComponent (world space)
|
||||
*/
|
||||
void snapToTransform(const TransformComponent &transform);
|
||||
|
||||
/**
|
||||
* Apply cursor position and orientation to a TransformComponent
|
||||
*/
|
||||
void applyToTransform(TransformComponent &transform) const;
|
||||
|
||||
/**
|
||||
* Simple hit test - returns true if mouse ray passes near cursor center
|
||||
* Handle mouse input for cursor interaction.
|
||||
* Returns true if cursor handled the input.
|
||||
*/
|
||||
bool hitTest(const Ogre::Ray &mouseRay) const;
|
||||
bool onMousePressed(const Ogre::Ray &mouseRay,
|
||||
const Ogre::Camera *camera);
|
||||
bool onMouseMoved(const Ogre::Ray &mouseRay,
|
||||
const Ogre::Vector2 &mouseDelta);
|
||||
bool onMouseReleased();
|
||||
|
||||
bool isDragging() const { return m_isDragging; }
|
||||
Axis getSelectedAxis() const { return m_selectedAxis; }
|
||||
|
||||
private:
|
||||
void createGeometry();
|
||||
void updateNodeTransform();
|
||||
Axis hitTest(const Ogre::Ray &mouseRay);
|
||||
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
Ogre::SceneNode *m_cursorNode;
|
||||
Ogre::ManualObject *m_axesObj;
|
||||
Ogre::ManualObject *m_markerObj;
|
||||
Ogre::ManualObject *m_axisX;
|
||||
Ogre::ManualObject *m_axisY;
|
||||
Ogre::ManualObject *m_axisZ;
|
||||
Ogre::ManualObject *m_centerMarker;
|
||||
|
||||
Ogre::Vector3 m_position;
|
||||
Ogre::Quaternion m_orientation;
|
||||
float m_size;
|
||||
bool m_visible;
|
||||
|
||||
Mode m_mode;
|
||||
Axis m_selectedAxis;
|
||||
Axis m_hoveredAxis;
|
||||
bool m_isDragging;
|
||||
|
||||
// Drag state
|
||||
Ogre::Vector3 m_dragStartPosition;
|
||||
Ogre::Quaternion m_dragStartRotation;
|
||||
Ogre::Vector3 m_dragAxisDir;
|
||||
float m_dragStartT;
|
||||
float m_dragStartAngle;
|
||||
Ogre::Vector2 m_dragScreenAxis;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_CURSOR3D_HPP
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "../components/GeneratedPhysicsTag.hpp"
|
||||
#include "EditorUISystem.hpp"
|
||||
#include "PrefabSystem.hpp"
|
||||
#include "../camera/EditorCamera.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/Renderable.hpp"
|
||||
@@ -35,6 +36,9 @@
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "../components/NavMesh.hpp"
|
||||
#include "../components/SmartObject.hpp"
|
||||
#include "../components/GoapPlanner.hpp"
|
||||
#include "../components/GoapRunner.hpp"
|
||||
#include "../components/PathFollowing.hpp"
|
||||
#include "../components/PrefabInstance.hpp"
|
||||
|
||||
#include "../ui/TransformEditor.hpp"
|
||||
@@ -87,11 +91,16 @@ bool EditorUISystem::onMousePressed(const Ogre::Ray &mouseRay)
|
||||
return true;
|
||||
}
|
||||
|
||||
// If cursor placement mode is active, raycast and place cursor
|
||||
// (skip if ImGui wants the mouse for UI interaction)
|
||||
if (m_cursor3D && m_cursorPlaceMode && m_physicsSystem &&
|
||||
m_physicsSystem->isInitialized() &&
|
||||
!ImGui::GetIO().WantCaptureMouse) {
|
||||
// Skip if ImGui wants the mouse
|
||||
if (ImGui::GetIO().WantCaptureMouse)
|
||||
return false;
|
||||
|
||||
if (!m_cursor3D || !m_cursor3D->isVisible())
|
||||
return false;
|
||||
|
||||
// Cursor place mode: raycast and place cursor on surface
|
||||
if (m_cursorMode == CursorInteractionMode::Place && m_physicsSystem &&
|
||||
m_physicsSystem->isInitialized()) {
|
||||
JoltPhysicsWrapper *physics =
|
||||
m_physicsSystem->getPhysicsWrapper();
|
||||
if (physics) {
|
||||
@@ -103,30 +112,62 @@ bool EditorUISystem::onMousePressed(const Ogre::Ray &mouseRay)
|
||||
if (physics->raycastQuery(start, end, hitPos,
|
||||
hitBody)) {
|
||||
m_cursor3D->setPosition(hitPos);
|
||||
m_cursorPlaceMode =
|
||||
false; // disable after placement
|
||||
m_cursorMode = CursorInteractionMode::None;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor translate/rotate: delegate to Cursor3D axis-based interaction
|
||||
if (m_cursorMode == CursorInteractionMode::Translate) {
|
||||
m_cursor3D->setMode(Cursor3D::Mode::Translate);
|
||||
Ogre::Camera *cam =
|
||||
m_editorCamera ? m_editorCamera->getCamera() : nullptr;
|
||||
if (m_cursor3D->onMousePressed(mouseRay, cam))
|
||||
return true;
|
||||
} else if (m_cursorMode == CursorInteractionMode::Rotate) {
|
||||
m_cursor3D->setMode(Cursor3D::Mode::Rotate);
|
||||
Ogre::Camera *cam =
|
||||
m_editorCamera ? m_editorCamera->getCamera() : nullptr;
|
||||
if (m_cursor3D->onMousePressed(mouseRay, cam))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EditorUISystem::onMouseMoved(const Ogre::Ray &mouseRay,
|
||||
const Ogre::Vector2 &mouseDelta)
|
||||
{
|
||||
if (m_gizmo) {
|
||||
return m_gizmo->onMouseMoved(mouseRay, mouseDelta);
|
||||
// Gizmo gets first shot
|
||||
if (m_gizmo && m_gizmo->onMouseMoved(mouseRay, mouseDelta)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cursor translate/rotate drag
|
||||
if (m_cursor3D && (m_cursorMode == CursorInteractionMode::Translate ||
|
||||
m_cursorMode == CursorInteractionMode::Rotate)) {
|
||||
if (m_cursor3D->onMouseMoved(mouseRay, mouseDelta))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EditorUISystem::onMouseReleased()
|
||||
{
|
||||
if (m_gizmo) {
|
||||
return m_gizmo->onMouseReleased();
|
||||
// Gizmo gets first shot
|
||||
if (m_gizmo && m_gizmo->onMouseReleased()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cursor translate/rotate release
|
||||
if (m_cursor3D && (m_cursorMode == CursorInteractionMode::Translate ||
|
||||
m_cursorMode == CursorInteractionMode::Rotate)) {
|
||||
if (m_cursor3D->onMouseReleased())
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -575,6 +616,8 @@ void EditorUISystem::renderEntityNode(flecs::entity entity, int depth)
|
||||
indicators += " [NavSrc]";
|
||||
if (entity.has<SmartObjectComponent>())
|
||||
indicators += " [SO]";
|
||||
if (entity.has<GoapPlannerComponent>())
|
||||
indicators += " [Planner]";
|
||||
|
||||
snprintf(label, sizeof(label), "%s%s##%llu", name.c_str(),
|
||||
indicators.c_str(), (unsigned long long)entity.id());
|
||||
@@ -998,6 +1041,28 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render GoapPlanner if present
|
||||
if (entity.has<GoapPlannerComponent>()) {
|
||||
auto &planner = entity.get_mut<GoapPlannerComponent>();
|
||||
m_componentRegistry.render<GoapPlannerComponent>(entity,
|
||||
planner);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render GoapRunner if present
|
||||
if (entity.has<GoapRunnerComponent>()) {
|
||||
auto &runner = entity.get_mut<GoapRunnerComponent>();
|
||||
m_componentRegistry.render<GoapRunnerComponent>(entity, runner);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render PathFollowing if present
|
||||
if (entity.has<PathFollowingComponent>()) {
|
||||
auto &pf = entity.get_mut<PathFollowingComponent>();
|
||||
m_componentRegistry.render<PathFollowingComponent>(entity, pf);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Show message if no components
|
||||
|
||||
if (componentCount == 0) {
|
||||
@@ -1741,6 +1806,44 @@ void EditorUISystem::renderPrefabBrowser()
|
||||
setSelectedEntity(instance);
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
// Delete prefab button
|
||||
std::string delId = "Del##" + file;
|
||||
if (ImGui::Button(delId.c_str())) {
|
||||
m_prefabToDelete = path;
|
||||
m_showDeletePrefabConfirm = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete prefab confirmation dialog
|
||||
if (m_showDeletePrefabConfirm) {
|
||||
ImGui::OpenPopup("Delete Prefab");
|
||||
}
|
||||
if (ImGui::BeginPopupModal("Delete Prefab",
|
||||
&m_showDeletePrefabConfirm,
|
||||
ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
ImGui::Text(
|
||||
"Are you sure you want to delete this prefab?");
|
||||
ImGui::Text(" %s", m_prefabToDelete.c_str());
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
||||
"This action cannot be undone!");
|
||||
|
||||
if (ImGui::Button("Delete", ImVec2(120, 0))) {
|
||||
PrefabSystem prefabSys(m_world, m_sceneMgr);
|
||||
if (prefabSys.deletePrefab(m_prefabToDelete)) {
|
||||
m_refreshPrefabList = true;
|
||||
}
|
||||
m_showDeletePrefabConfirm = false;
|
||||
m_prefabToDelete.clear();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
||||
m_showDeletePrefabConfirm = false;
|
||||
m_prefabToDelete.clear();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
@@ -1766,15 +1869,48 @@ void EditorUISystem::renderCursorPanel()
|
||||
m_cursor3D->setVisible(visible);
|
||||
}
|
||||
|
||||
// Placement mode
|
||||
ImGui::Separator();
|
||||
|
||||
// Interaction mode radio buttons
|
||||
ImGui::Text("Interaction Mode:");
|
||||
int mode = static_cast<int>(m_cursorMode);
|
||||
if (ImGui::RadioButton("None", &mode, 0)) {
|
||||
m_cursorMode = CursorInteractionMode::None;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox("Place on Click", &m_cursorPlaceMode)) {
|
||||
if (m_cursorPlaceMode && !m_cursor3D->isVisible())
|
||||
if (ImGui::RadioButton("Place", &mode, 1)) {
|
||||
m_cursorMode = CursorInteractionMode::Place;
|
||||
if (!m_cursor3D->isVisible())
|
||||
m_cursor3D->setVisible(true);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Click in the 3D viewport to place cursor on surface");
|
||||
"Click in viewport to raycast-place cursor on surface");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::RadioButton("Move", &mode, 2)) {
|
||||
m_cursorMode = CursorInteractionMode::Translate;
|
||||
if (!m_cursor3D->isVisible())
|
||||
m_cursor3D->setVisible(true);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Click and drag an axis to translate along it");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::RadioButton("Rotate", &mode, 3)) {
|
||||
m_cursorMode = CursorInteractionMode::Rotate;
|
||||
if (!m_cursor3D->isVisible())
|
||||
m_cursor3D->setVisible(true);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Click and drag an axis to rotate around it");
|
||||
}
|
||||
|
||||
if (m_cursor3D->isDragging()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(0, 1, 0, 1), "[Dragging]");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
class EditorPhysicsSystem;
|
||||
class BuoyancySystem;
|
||||
class NormalDebugSystem;
|
||||
class EditorCamera;
|
||||
|
||||
namespace Ogre
|
||||
{
|
||||
@@ -142,6 +143,14 @@ public:
|
||||
m_normalDebugSystem = normalDebug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set editor camera for cursor drag operations
|
||||
*/
|
||||
void setEditorCamera(EditorCamera *camera)
|
||||
{
|
||||
m_editorCamera = camera;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable editor UI rendering
|
||||
*/
|
||||
@@ -267,12 +276,26 @@ private:
|
||||
std::vector<std::string> m_prefabFiles;
|
||||
bool m_refreshPrefabList = true;
|
||||
bool m_prefabInstAtRoot = false;
|
||||
bool m_prefabUseCursor = true; // Use 3D cursor for prefab placement
|
||||
float m_prefabRaycastMargin = 0.0f; // Vertical margin for raycast placement
|
||||
bool m_prefabUseCursor = true; // Use 3D cursor for prefab placement
|
||||
float m_prefabRaycastMargin =
|
||||
0.0f; // Vertical margin for raycast placement
|
||||
|
||||
// Prefab delete confirmation
|
||||
bool m_showDeletePrefabConfirm = false;
|
||||
std::string m_prefabToDelete;
|
||||
|
||||
// 3D Cursor state
|
||||
bool m_showCursorPanel = false;
|
||||
bool m_cursorPlaceMode = false; // Click in viewport to place cursor
|
||||
enum class CursorInteractionMode {
|
||||
None,
|
||||
Place, // Click to raycast-place on surface
|
||||
Translate, // Drag axis to translate cursor
|
||||
Rotate // Drag axis to rotate cursor
|
||||
};
|
||||
CursorInteractionMode m_cursorMode = CursorInteractionMode::None;
|
||||
|
||||
// Camera reference for cursor placement/rotation
|
||||
EditorCamera *m_editorCamera = nullptr;
|
||||
|
||||
// Queries
|
||||
flecs::query<EntityNameComponent> m_nameQuery;
|
||||
|
||||
266
src/features/editScene/systems/GoapPlannerSystem.cpp
Normal file
266
src/features/editScene/systems/GoapPlannerSystem.cpp
Normal file
@@ -0,0 +1,266 @@
|
||||
#include "GoapPlannerSystem.hpp"
|
||||
#include "../components/GoapPlanner.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/ActionDebug.hpp"
|
||||
#include "../components/GoapAction.hpp"
|
||||
#include "../components/GoapGoal.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <queue>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace {
|
||||
|
||||
struct PlannerNode {
|
||||
GoapBlackboard state;
|
||||
std::vector<Ogre::String> actions;
|
||||
int cost = 0;
|
||||
|
||||
int heuristic(const GoapBlackboard &goal) const
|
||||
{
|
||||
(void)goal;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int f(const GoapBlackboard &goal) const
|
||||
{
|
||||
return cost + heuristic(goal);
|
||||
}
|
||||
};
|
||||
|
||||
struct NodeCompare {
|
||||
const GoapBlackboard *goal;
|
||||
bool operator()(const PlannerNode &a, const PlannerNode &b) const
|
||||
{
|
||||
return a.f(*goal) > b.f(*goal);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
GoapPlannerSystem::GoapPlannerSystem(flecs::world &world)
|
||||
: m_world(world)
|
||||
{
|
||||
}
|
||||
|
||||
GoapPlannerSystem::~GoapPlannerSystem() = default;
|
||||
|
||||
|
||||
size_t GoapPlannerSystem::hashState(const GoapBlackboard &bb)
|
||||
{
|
||||
std::hash<uint64_t> hasher;
|
||||
size_t h = hasher(bb.bits);
|
||||
h ^= hasher(bb.mask) + 0x9e3779b9 + (h << 6) + (h >> 2);
|
||||
h ^= hasher(bb.bitmask) + 0x9e3779b9 + (h << 6) + (h >> 2);
|
||||
for (const auto &pair : bb.values) {
|
||||
h ^= std::hash<std::string>{}(pair.first) + 0x9e3779b9 +
|
||||
(h << 6) + (h >> 2);
|
||||
h ^= std::hash<int>{}(pair.second) + 0x9e3779b9 + (h << 6) +
|
||||
(h >> 2);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
bool GoapPlannerSystem::statesEqual(const GoapBlackboard &a,
|
||||
const GoapBlackboard &b)
|
||||
{
|
||||
return a.bits == b.bits && a.mask == b.mask &&
|
||||
a.bitmask == b.bitmask && a.values == b.values;
|
||||
}
|
||||
|
||||
std::vector<const GoapAction *> GoapPlannerSystem::resolveActions(
|
||||
const GoapPlannerComponent &planner, const ActionDatabase *db)
|
||||
{
|
||||
std::vector<const GoapAction *> result;
|
||||
if (!db)
|
||||
return result;
|
||||
|
||||
for (const auto &name : planner.actionNames) {
|
||||
const GoapAction *action = db->findAction(name);
|
||||
if (action)
|
||||
result.push_back(action);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const GoapGoal *GoapPlannerSystem::selectGoal(
|
||||
const GoapPlannerComponent &planner, const ActionDatabase *db,
|
||||
const GoapBlackboard &blackboard)
|
||||
{
|
||||
if (!db)
|
||||
return nullptr;
|
||||
|
||||
const GoapGoal *best = nullptr;
|
||||
int bestPriority = -1;
|
||||
|
||||
for (const auto &name : planner.goalNames) {
|
||||
const GoapGoal *goal = db->findGoal(name);
|
||||
if (!goal)
|
||||
continue;
|
||||
if (!goal->isValid(blackboard))
|
||||
continue;
|
||||
if (goal->priority > bestPriority) {
|
||||
bestPriority = goal->priority;
|
||||
best = goal;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
void GoapPlannerSystem::planForEntity(flecs::entity e,
|
||||
GoapPlannerComponent &planner,
|
||||
const GoapBlackboard &blackboard)
|
||||
{
|
||||
(void)e;
|
||||
|
||||
// Find ActionDatabase
|
||||
const ActionDatabase *db = nullptr;
|
||||
m_world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
|
||||
// Select best valid goal
|
||||
const GoapGoal *goal = selectGoal(planner, db, blackboard);
|
||||
if (!goal) {
|
||||
planner.status = GoapPlannerComponent::Status::NoPlanFound;
|
||||
planner.planDirty = false;
|
||||
planner.currentGoalName.clear();
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[GoapPlanner] No valid goal found.");
|
||||
return;
|
||||
}
|
||||
|
||||
planner.currentGoalName = goal->name;
|
||||
|
||||
// Resolve actions
|
||||
std::vector<const GoapAction *> actions =
|
||||
resolveActions(planner, db);
|
||||
|
||||
if (actions.empty()) {
|
||||
planner.status = GoapPlannerComponent::Status::NoPlanFound;
|
||||
planner.planDirty = false;
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[GoapPlanner] No actions available for planning.");
|
||||
return;
|
||||
}
|
||||
|
||||
// A* search
|
||||
NodeCompare cmp{&goal->target};
|
||||
std::priority_queue<PlannerNode, std::vector<PlannerNode>,
|
||||
NodeCompare>
|
||||
openSet(cmp);
|
||||
|
||||
// Closed set: map from state hash -> best cost seen for that state
|
||||
std::unordered_map<size_t, int> closedSet;
|
||||
|
||||
// Track found plans to avoid duplicates
|
||||
std::unordered_set<std::string> foundPlanSignatures;
|
||||
|
||||
PlannerNode start;
|
||||
start.state = blackboard;
|
||||
start.cost = 0;
|
||||
openSet.push(start);
|
||||
|
||||
planner.nodesExplored = 0;
|
||||
planner.plansGenerated = 0;
|
||||
int maxNodes = 10000; // safety limit
|
||||
|
||||
while (!openSet.empty() && planner.plansGenerated < planner.maxPlans &&
|
||||
planner.nodesExplored < maxNodes) {
|
||||
PlannerNode current = openSet.top();
|
||||
openSet.pop();
|
||||
planner.nodesExplored++;
|
||||
|
||||
// Check if goal is satisfied
|
||||
if (current.state.satisfies(goal->target)) {
|
||||
// Build plan signature for deduplication
|
||||
std::string sig;
|
||||
for (const auto &name : current.actions)
|
||||
sig += name + ",";
|
||||
if (foundPlanSignatures.insert(sig).second) {
|
||||
GoapPlannerComponent::Plan plan;
|
||||
plan.actions = current.actions;
|
||||
plan.totalCost = current.cost;
|
||||
plan.goalName = goal->name;
|
||||
planner.planQueue.push_back(std::move(plan));
|
||||
planner.plansGenerated++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check closed set
|
||||
size_t stateHash = hashState(current.state);
|
||||
auto it = closedSet.find(stateHash);
|
||||
if (it != closedSet.end() && it->second <= current.cost)
|
||||
continue;
|
||||
closedSet[stateHash] = current.cost;
|
||||
|
||||
// Expand: try each action
|
||||
for (const GoapAction *action : actions) {
|
||||
if (!action)
|
||||
continue;
|
||||
if (!action->canRun(current.state))
|
||||
continue;
|
||||
|
||||
PlannerNode next = current;
|
||||
next.state.apply(action->effects);
|
||||
next.actions.push_back(action->name);
|
||||
next.cost += action->cost;
|
||||
|
||||
openSet.push(std::move(next));
|
||||
}
|
||||
}
|
||||
|
||||
if (planner.plansGenerated > 0) {
|
||||
planner.status =
|
||||
GoapPlannerComponent::Status::PlansAvailable;
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[GoapPlanner] Generated " +
|
||||
Ogre::StringConverter::toString(
|
||||
planner.plansGenerated) +
|
||||
" plan(s) for goal: " + goal->name);
|
||||
} else {
|
||||
planner.status =
|
||||
GoapPlannerComponent::Status::NoPlanFound;
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[GoapPlanner] No plan found for goal: " +
|
||||
goal->name);
|
||||
}
|
||||
|
||||
planner.planDirty = false;
|
||||
}
|
||||
|
||||
void GoapPlannerSystem::update(float deltaTime)
|
||||
{
|
||||
(void)deltaTime;
|
||||
|
||||
bool shouldRun = true;
|
||||
if (m_editorApp) {
|
||||
if (m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
|
||||
m_editorApp->getGamePlayState() !=
|
||||
EditorApp::GamePlayState::Playing) {
|
||||
shouldRun = false;
|
||||
}
|
||||
}
|
||||
if (!shouldRun)
|
||||
return;
|
||||
|
||||
m_world
|
||||
.query<GoapPlannerComponent, GoapBlackboard>()
|
||||
.each([&](flecs::entity e, GoapPlannerComponent &planner,
|
||||
GoapBlackboard &blackboard) {
|
||||
(void)e;
|
||||
if (!planner.planDirty)
|
||||
return;
|
||||
|
||||
// Clear old plans before replanning
|
||||
planner.clearPlans();
|
||||
planner.status =
|
||||
GoapPlannerComponent::Status::Planning;
|
||||
planForEntity(e, planner, blackboard);
|
||||
});
|
||||
}
|
||||
68
src/features/editScene/systems/GoapPlannerSystem.hpp
Normal file
68
src/features/editScene/systems/GoapPlannerSystem.hpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#ifndef EDITSCENE_GOAP_PLANNER_SYSTEM_HPP
|
||||
#define EDITSCENE_GOAP_PLANNER_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <queue>
|
||||
#include <unordered_set>
|
||||
|
||||
// Forward declarations
|
||||
class EditorApp;
|
||||
class ActionDatabase;
|
||||
struct GoapPlannerComponent;
|
||||
struct GoapAction;
|
||||
struct GoapGoal;
|
||||
struct GoapBlackboard;
|
||||
|
||||
/**
|
||||
* GOAP Planner system.
|
||||
*
|
||||
* Runs A* planning for entities with GoapPlannerComponent + GoapBlackboard.
|
||||
* When planDirty is set:
|
||||
* 1. Selects the highest-priority valid goal from the planner's goal list
|
||||
* 2. Resolves action names against ActionDatabase
|
||||
* 3. Runs forward A* search to find action sequences reaching the goal
|
||||
* 4. Stores up to maxPlans plans in the planner's planQueue
|
||||
* 5. Sets status to PlansAvailable or NoPlanFound
|
||||
*/
|
||||
class GoapPlannerSystem {
|
||||
public:
|
||||
GoapPlannerSystem(flecs::world &world);
|
||||
~GoapPlannerSystem();
|
||||
|
||||
void update(float deltaTime);
|
||||
|
||||
/**
|
||||
* Set the EditorApp for game mode detection.
|
||||
* Planning only runs when playing (game mode) or always in editor.
|
||||
*/
|
||||
void setEditorApp(EditorApp *app)
|
||||
{
|
||||
m_editorApp = app;
|
||||
}
|
||||
|
||||
private:
|
||||
void planForEntity(flecs::entity e, GoapPlannerComponent &planner,
|
||||
const GoapBlackboard &blackboard);
|
||||
|
||||
std::vector<const GoapAction *>
|
||||
resolveActions(const GoapPlannerComponent &planner,
|
||||
const ActionDatabase *db);
|
||||
|
||||
const GoapGoal *selectGoal(const GoapPlannerComponent &planner,
|
||||
const ActionDatabase *db,
|
||||
const GoapBlackboard &blackboard);
|
||||
|
||||
// Hash a blackboard state for closed-set deduplication
|
||||
static size_t hashState(const GoapBlackboard &bb);
|
||||
static bool statesEqual(const GoapBlackboard &a,
|
||||
const GoapBlackboard &b);
|
||||
|
||||
flecs::world &m_world;
|
||||
EditorApp *m_editorApp = nullptr;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GOAP_PLANNER_SYSTEM_HPP
|
||||
325
src/features/editScene/systems/GoapRunnerSystem.cpp
Normal file
325
src/features/editScene/systems/GoapRunnerSystem.cpp
Normal file
@@ -0,0 +1,325 @@
|
||||
#include "GoapRunnerSystem.hpp"
|
||||
#include "SmartObjectSystem.hpp"
|
||||
#include "BehaviorTreeSystem.hpp"
|
||||
#include "AnimationTreeSystem.hpp"
|
||||
#include "NavMeshSystem.hpp"
|
||||
#include "../components/GoapPlanner.hpp"
|
||||
#include "../components/GoapRunner.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/ActionDebug.hpp"
|
||||
#include "../components/SmartObject.hpp"
|
||||
#include "../components/PathFollowing.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/Character.hpp"
|
||||
#include "../components/NavMesh.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <cmath>
|
||||
|
||||
GoapRunnerSystem::GoapRunnerSystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr,
|
||||
SmartObjectSystem *soSystem,
|
||||
BehaviorTreeSystem *btSystem,
|
||||
NavMeshSystem *navSystem)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
, m_soSystem(soSystem)
|
||||
, m_btSystem(btSystem)
|
||||
, m_navSystem(navSystem)
|
||||
{
|
||||
}
|
||||
|
||||
GoapRunnerSystem::~GoapRunnerSystem() = default;
|
||||
|
||||
flecs::entity GoapRunnerSystem::findNearestSmartObject(
|
||||
flecs::entity character, const Ogre::String &actionName,
|
||||
float maxDistance)
|
||||
{
|
||||
if (!character.is_alive() || !character.has<TransformComponent>())
|
||||
return flecs::entity::null();
|
||||
|
||||
Ogre::Vector3 charPos =
|
||||
character.get<TransformComponent>().node
|
||||
->_getDerivedPosition();
|
||||
|
||||
flecs::entity nearest = flecs::entity::null();
|
||||
float bestDist = maxDistance;
|
||||
|
||||
m_world.query<SmartObjectComponent, TransformComponent>()
|
||||
.each([&](flecs::entity so, SmartObjectComponent &soComp,
|
||||
TransformComponent &soTrans) {
|
||||
bool hasAction = false;
|
||||
for (const auto &name : soComp.actionNames) {
|
||||
if (name == actionName) {
|
||||
hasAction = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasAction)
|
||||
return;
|
||||
|
||||
Ogre::Vector3 soPos =
|
||||
soTrans.node->_getDerivedPosition();
|
||||
float dist = charPos.distance(soPos);
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
nearest = so;
|
||||
}
|
||||
});
|
||||
|
||||
return nearest;
|
||||
}
|
||||
|
||||
bool GoapRunnerSystem::startNextAction(flecs::entity e)
|
||||
{
|
||||
if (!e.has<GoapPlannerComponent>() || !e.has<GoapRunnerComponent>())
|
||||
return false;
|
||||
|
||||
auto &planner = e.get_mut<GoapPlannerComponent>();
|
||||
auto &runner = e.get_mut<GoapRunnerComponent>();
|
||||
|
||||
if (runner.currentActionIndex >= (int)runner.planActions.size()) {
|
||||
// Plan complete
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[GoapRunner] Plan complete.");
|
||||
runner.planActions.clear();
|
||||
runner.currentActionIndex = 0;
|
||||
runner.currentActionName.clear();
|
||||
runner.state = GoapRunnerComponent::State::Idle;
|
||||
|
||||
// Clear PathFollowing target
|
||||
if (e.has<PathFollowingComponent>()) {
|
||||
auto &pf = e.get_mut<PathFollowingComponent>();
|
||||
pf.hasTarget = false;
|
||||
pf.currentLocomotionState = "idle";
|
||||
pf.path.clear();
|
||||
pf.pathIndex = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const Ogre::String &actionName =
|
||||
runner.planActions[runner.currentActionIndex];
|
||||
runner.currentActionName = actionName;
|
||||
|
||||
// Find action database
|
||||
ActionDatabase *db = nullptr;
|
||||
m_world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
|
||||
const GoapAction *action = db ? db->findAction(actionName) : nullptr;
|
||||
|
||||
if (action) {
|
||||
// Normal action: run behavior tree via ActionDebug
|
||||
runner.state = GoapRunnerComponent::State::RunningAction;
|
||||
|
||||
if (!e.has<ActionDebug>()) {
|
||||
e.set<ActionDebug>({});
|
||||
m_managedActionDebugs.insert(e.id());
|
||||
}
|
||||
auto &debug = e.get_mut<ActionDebug>();
|
||||
debug.isRunning = true;
|
||||
debug.runTimer = 0.0f;
|
||||
debug.currentActionName = actionName;
|
||||
debug.selectedActionName = actionName;
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[GoapRunner] Starting normal action: " + actionName);
|
||||
} else {
|
||||
// Smart object action: pathfind and set up PathFollowingComponent
|
||||
flecs::entity so = findNearestSmartObject(
|
||||
e, actionName, planner.smartObjectDistance);
|
||||
if (so.is_alive()) {
|
||||
runner.state =
|
||||
GoapRunnerComponent::State::MovingToSmartObject;
|
||||
runner.targetSmartObjectId = so.id();
|
||||
|
||||
if (e.has<PathFollowingComponent>()) {
|
||||
auto &pf = e.get_mut<PathFollowingComponent>();
|
||||
auto &soTrans = so.get<TransformComponent>();
|
||||
pf.targetPosition =
|
||||
soTrans.node->_getDerivedPosition();
|
||||
pf.hasTarget = true;
|
||||
pf.path.clear();
|
||||
pf.pathIndex = 0;
|
||||
pf.pathRecalcTimer = 0.0f;
|
||||
|
||||
// Find path via navmesh
|
||||
if (m_navSystem) {
|
||||
flecs::entity navmeshEntity =
|
||||
flecs::entity::null();
|
||||
m_world
|
||||
.query<NavMeshComponent>()
|
||||
.each([&](flecs::entity ne,
|
||||
NavMeshComponent &) {
|
||||
if (!navmeshEntity.is_alive())
|
||||
navmeshEntity = ne;
|
||||
});
|
||||
if (navmeshEntity.is_alive()) {
|
||||
auto &charTrans =
|
||||
e.get<TransformComponent>();
|
||||
bool found = m_navSystem->findPath(
|
||||
navmeshEntity,
|
||||
charTrans.node
|
||||
->_getDerivedPosition(),
|
||||
pf.targetPosition,
|
||||
pf.path);
|
||||
if (!found || pf.path.empty()) {
|
||||
// Fallback: direct path
|
||||
pf.path.clear();
|
||||
pf.path.push_back(
|
||||
pf.targetPosition);
|
||||
}
|
||||
} else {
|
||||
pf.path.clear();
|
||||
pf.path.push_back(pf.targetPosition);
|
||||
}
|
||||
} else {
|
||||
pf.path.clear();
|
||||
pf.path.push_back(pf.targetPosition);
|
||||
}
|
||||
|
||||
// Choose walk or run based on distance
|
||||
auto &charTrans = e.get<TransformComponent>();
|
||||
float dist = charTrans.node
|
||||
->_getDerivedPosition()
|
||||
.distance(pf.targetPosition);
|
||||
if (dist > pf.walkSpeed * 3.0f)
|
||||
pf.currentLocomotionState = "run";
|
||||
else
|
||||
pf.currentLocomotionState = "walk";
|
||||
}
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[GoapRunner] Starting smart object action: " +
|
||||
actionName);
|
||||
} else {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[GoapRunner] No smart object found for action: " +
|
||||
actionName);
|
||||
// Skip this action
|
||||
runner.currentActionIndex++;
|
||||
return startNextAction(e);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void GoapRunnerSystem::update(float deltaTime)
|
||||
{
|
||||
bool shouldRun = true;
|
||||
if (m_editorApp) {
|
||||
if (m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
|
||||
m_editorApp->getGamePlayState() !=
|
||||
EditorApp::GamePlayState::Playing) {
|
||||
shouldRun = false;
|
||||
}
|
||||
}
|
||||
if (!shouldRun)
|
||||
return;
|
||||
|
||||
m_world.query<GoapPlannerComponent, GoapRunnerComponent>()
|
||||
.each([&](flecs::entity e, GoapPlannerComponent &planner,
|
||||
GoapRunnerComponent &runner) {
|
||||
// If idle and plans available, pop one and start
|
||||
if (runner.state ==
|
||||
GoapRunnerComponent::State::Idle &&
|
||||
planner.hasPlans()) {
|
||||
auto plan = planner.popCheapestPlan();
|
||||
runner.planActions = std::move(plan.actions);
|
||||
runner.currentActionIndex = 0;
|
||||
runner.currentActionName.clear();
|
||||
startNextAction(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// If idle and no plans, mark planner dirty for replanning
|
||||
if (runner.state ==
|
||||
GoapRunnerComponent::State::Idle &&
|
||||
!planner.hasPlans()) {
|
||||
if (!runner.planActions.empty()) {
|
||||
// Just finished a plan batch, need more
|
||||
runner.planActions.clear();
|
||||
runner.currentActionIndex = 0;
|
||||
}
|
||||
planner.planDirty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// If still idle, nothing to do
|
||||
if (runner.state ==
|
||||
GoapRunnerComponent::State::Idle)
|
||||
return;
|
||||
|
||||
// Update action timer
|
||||
runner.actionTimer += deltaTime;
|
||||
|
||||
// Handle smart object navigation completion
|
||||
if (runner.state == GoapRunnerComponent::State::
|
||||
MovingToSmartObject) {
|
||||
if (e.has<PathFollowingComponent>()) {
|
||||
auto &pf = e.get<PathFollowingComponent>();
|
||||
if (!pf.hasTarget) {
|
||||
// PathFollowingSystem has reached the
|
||||
// destination - execute the action
|
||||
Ogre::LogManager::getSingleton()
|
||||
.logMessage(
|
||||
"[GoapRunner] Reached smart object, executing: " +
|
||||
runner.currentActionName);
|
||||
|
||||
// Apply action effects
|
||||
if (e.has<GoapBlackboard>()) {
|
||||
ActionDatabase *db = nullptr;
|
||||
m_world
|
||||
.query<ActionDatabase>()
|
||||
.each([&](flecs::entity,
|
||||
ActionDatabase
|
||||
&database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
if (db) {
|
||||
const GoapAction *action =
|
||||
db->findAction(
|
||||
runner.currentActionName);
|
||||
if (action) {
|
||||
auto &bb = e.get_mut<GoapBlackboard>();
|
||||
bb.apply(action->effects);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Advance to next action
|
||||
runner.currentActionIndex++;
|
||||
runner.actionTimer = 0.0f;
|
||||
runner.targetSmartObjectId = 0;
|
||||
startNextAction(e);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if current normal action is complete
|
||||
if (runner.state == GoapRunnerComponent::State::
|
||||
RunningAction &&
|
||||
m_btSystem && e.has<ActionDebug>()) {
|
||||
auto &debug = e.get_mut<ActionDebug>();
|
||||
debug.runTimer += deltaTime;
|
||||
|
||||
auto &btState =
|
||||
m_btSystem->getActionDebugState(e.id());
|
||||
if (debug.runTimer > 0.5f &&
|
||||
btState.treeResult !=
|
||||
BehaviorTreeSystem::Status::running) {
|
||||
debug.isRunning = false;
|
||||
runner.currentActionIndex++;
|
||||
runner.actionTimer = 0.0f;
|
||||
startNextAction(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
68
src/features/editScene/systems/GoapRunnerSystem.hpp
Normal file
68
src/features/editScene/systems/GoapRunnerSystem.hpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#ifndef EDITSCENE_GOAP_RUNNER_SYSTEM_HPP
|
||||
#define EDITSCENE_GOAP_RUNNER_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <unordered_set>
|
||||
|
||||
// Forward declarations
|
||||
class SmartObjectSystem;
|
||||
class BehaviorTreeSystem;
|
||||
class EditorApp;
|
||||
class AnimationTreeSystem;
|
||||
class NavMeshSystem;
|
||||
|
||||
/**
|
||||
* System that executes GOAP plans from GoapPlannerComponent's plan queue.
|
||||
*
|
||||
* For each entity with GoapPlannerComponent + GoapRunnerComponent:
|
||||
* 1. If no active plan, pop the cheapest plan from planner.planQueue
|
||||
* 2. Normal actions: run via BehaviorTreeSystem (using ActionDebug)
|
||||
* 3. Smart object actions: pathfind to smart object using NavMeshSystem,
|
||||
* store path in PathFollowingComponent, then execute on arrival
|
||||
* 4. Advance to next action when current completes
|
||||
* 5. When plan finishes, pop next plan. If queue empty, mark planner dirty.
|
||||
*/
|
||||
class GoapRunnerSystem {
|
||||
public:
|
||||
GoapRunnerSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
SmartObjectSystem *soSystem,
|
||||
BehaviorTreeSystem *btSystem,
|
||||
NavMeshSystem *navSystem);
|
||||
~GoapRunnerSystem();
|
||||
|
||||
void update(float deltaTime);
|
||||
|
||||
void setAnimationTreeSystem(AnimationTreeSystem *system)
|
||||
{
|
||||
m_animTreeSystem = system;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the EditorApp for game mode detection.
|
||||
*/
|
||||
void setEditorApp(EditorApp *app)
|
||||
{
|
||||
m_editorApp = app;
|
||||
}
|
||||
|
||||
private:
|
||||
bool startNextAction(flecs::entity e);
|
||||
flecs::entity findNearestSmartObject(flecs::entity character,
|
||||
const Ogre::String &actionName,
|
||||
float maxDistance);
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
SmartObjectSystem *m_soSystem;
|
||||
BehaviorTreeSystem *m_btSystem;
|
||||
NavMeshSystem *m_navSystem;
|
||||
AnimationTreeSystem *m_animTreeSystem = nullptr;
|
||||
EditorApp *m_editorApp = nullptr;
|
||||
|
||||
// Track which entities we set up ActionDebug for
|
||||
std::unordered_set<flecs::entity_t> m_managedActionDebugs;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GOAP_RUNNER_SYSTEM_HPP
|
||||
205
src/features/editScene/systems/PathFollowingSystem.cpp
Normal file
205
src/features/editScene/systems/PathFollowingSystem.cpp
Normal file
@@ -0,0 +1,205 @@
|
||||
#include "PathFollowingSystem.hpp"
|
||||
#include "AnimationTreeSystem.hpp"
|
||||
#include "NavMeshSystem.hpp"
|
||||
#include "../components/PathFollowing.hpp"
|
||||
#include "../components/Character.hpp"
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/NavMesh.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <cmath>
|
||||
|
||||
PathFollowingSystem::PathFollowingSystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr,
|
||||
NavMeshSystem *navSystem)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
, m_navSystem(navSystem)
|
||||
{
|
||||
}
|
||||
|
||||
PathFollowingSystem::~PathFollowingSystem() = default;
|
||||
|
||||
Ogre::Vector3 PathFollowingSystem::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;
|
||||
}
|
||||
|
||||
void PathFollowingSystem::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
|
||||
Ogre::Vector3 frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z;
|
||||
if (e.has<CharacterSlotsComponent>()) {
|
||||
auto &slots = e.get<CharacterSlotsComponent>();
|
||||
frontAxis = slots.frontAxis;
|
||||
}
|
||||
|
||||
// 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));
|
||||
trans.rotation = Ogre::Quaternion(yaw, Ogre::Vector3::UNIT_Y);
|
||||
}
|
||||
|
||||
void PathFollowingSystem::applyLocomotionState(flecs::entity e)
|
||||
{
|
||||
if (!e.has<PathFollowingComponent>() || !m_animTreeSystem)
|
||||
return;
|
||||
|
||||
auto &path = e.get<PathFollowingComponent>();
|
||||
const PathFollowingState *state =
|
||||
path.findState(path.currentLocomotionState);
|
||||
if (!state)
|
||||
return;
|
||||
|
||||
for (const auto &pair : state->stateMachineStates) {
|
||||
m_animTreeSystem->setState(e, pair.first, pair.second, false);
|
||||
}
|
||||
}
|
||||
|
||||
void PathFollowingSystem::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;
|
||||
});
|
||||
|
||||
m_world
|
||||
.query<PathFollowingComponent, CharacterComponent, TransformComponent>()
|
||||
.each([&](flecs::entity e, PathFollowingComponent &pf,
|
||||
CharacterComponent &cc, TransformComponent &trans) {
|
||||
(void)trans;
|
||||
|
||||
if (!pf.hasTarget)
|
||||
return;
|
||||
|
||||
Ogre::Vector3 charPos = getEntityPosition(e);
|
||||
Ogre::Vector3 targetPos = pf.targetPosition;
|
||||
|
||||
// Check if we've reached the destination
|
||||
Ogre::Vector3 toFinal = targetPos - charPos;
|
||||
toFinal.y = 0;
|
||||
float distToFinal = toFinal.length();
|
||||
if (distToFinal < 0.5f) {
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
pf.hasTarget = false;
|
||||
pf.currentLocomotionState = "idle";
|
||||
pf.path.clear();
|
||||
pf.pathIndex = 0;
|
||||
applyLocomotionState(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Recalculate path periodically
|
||||
pf.pathRecalcTimer += deltaTime;
|
||||
if (pf.pathRecalcTimer > 2.0f) {
|
||||
pf.pathRecalcTimer = 0.0f;
|
||||
if (navmeshEntity.is_alive() && m_navSystem) {
|
||||
std::vector<Ogre::Vector3> newPath;
|
||||
bool found = m_navSystem->findPath(
|
||||
navmeshEntity, charPos, targetPos,
|
||||
newPath);
|
||||
if (found && !newPath.empty()) {
|
||||
pf.path = std::move(newPath);
|
||||
pf.pathIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if no path, go direct
|
||||
if (pf.path.empty()) {
|
||||
pf.path.clear();
|
||||
pf.path.push_back(targetPos);
|
||||
pf.pathIndex = 0;
|
||||
}
|
||||
|
||||
// Advance waypoints
|
||||
if (pf.pathIndex >= (int)pf.path.size()) {
|
||||
pf.path.clear();
|
||||
pf.path.push_back(targetPos);
|
||||
pf.pathIndex = 0;
|
||||
}
|
||||
|
||||
Ogre::Vector3 waypointPos = pf.path[pf.pathIndex];
|
||||
Ogre::Vector3 toTarget = waypointPos - charPos;
|
||||
|
||||
const float WAYPOINT_THRESHOLD = 0.5f;
|
||||
if (toTarget.length() < WAYPOINT_THRESHOLD) {
|
||||
pf.pathIndex++;
|
||||
if (pf.pathIndex >= (int)pf.path.size()) {
|
||||
// Last waypoint - check if close to final target
|
||||
if (distToFinal < 0.5f) {
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
pf.hasTarget = false;
|
||||
pf.currentLocomotionState = "idle";
|
||||
pf.path.clear();
|
||||
pf.pathIndex = 0;
|
||||
applyLocomotionState(e);
|
||||
return;
|
||||
}
|
||||
// Extend with direct target
|
||||
pf.path.clear();
|
||||
pf.path.push_back(targetPos);
|
||||
pf.pathIndex = 0;
|
||||
}
|
||||
waypointPos = pf.path[pf.pathIndex];
|
||||
toTarget = waypointPos - charPos;
|
||||
}
|
||||
|
||||
// Move toward waypoint
|
||||
toTarget.y = 0;
|
||||
if (toTarget.squaredLength() > 0.0001f) {
|
||||
toTarget.normalise();
|
||||
|
||||
// Determine speed based on distance to final target
|
||||
float speed = pf.walkSpeed;
|
||||
if (distToFinal > pf.walkSpeed * 3.0f) {
|
||||
speed = pf.runSpeed;
|
||||
pf.currentLocomotionState = "run";
|
||||
} else {
|
||||
speed = pf.walkSpeed;
|
||||
pf.currentLocomotionState = "walk";
|
||||
}
|
||||
|
||||
cc.linearVelocity = toTarget * speed;
|
||||
|
||||
// Rotate character to face movement direction
|
||||
rotateTowards(e, toTarget, deltaTime);
|
||||
} else {
|
||||
cc.linearVelocity = Ogre::Vector3::ZERO;
|
||||
}
|
||||
|
||||
applyLocomotionState(e);
|
||||
});
|
||||
}
|
||||
45
src/features/editScene/systems/PathFollowingSystem.hpp
Normal file
45
src/features/editScene/systems/PathFollowingSystem.hpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#ifndef EDITSCENE_PATH_FOLLOWING_SYSTEM_HPP
|
||||
#define EDITSCENE_PATH_FOLLOWING_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
|
||||
// Forward declarations
|
||||
class AnimationTreeSystem;
|
||||
class NavMeshSystem;
|
||||
|
||||
/**
|
||||
* System that follows navmesh paths and drives character movement.
|
||||
*
|
||||
* Reads path waypoints from PathFollowingComponent, advances waypoints,
|
||||
* rotates the character toward the movement direction (Y-axis only),
|
||||
* sets linear velocity for root motion, and applies animation states
|
||||
* via AnimationTreeSystem.
|
||||
*/
|
||||
class PathFollowingSystem {
|
||||
public:
|
||||
PathFollowingSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
NavMeshSystem *navSystem);
|
||||
~PathFollowingSystem();
|
||||
|
||||
void setAnimationTreeSystem(AnimationTreeSystem *system)
|
||||
{
|
||||
m_animTreeSystem = system;
|
||||
}
|
||||
|
||||
void update(float deltaTime);
|
||||
|
||||
private:
|
||||
void applyLocomotionState(flecs::entity e);
|
||||
Ogre::Vector3 getEntityPosition(flecs::entity e);
|
||||
void rotateTowards(flecs::entity e, const Ogre::Vector3 &direction,
|
||||
float deltaTime);
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
NavMeshSystem *m_navSystem;
|
||||
AnimationTreeSystem *m_animTreeSystem = nullptr;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_PATH_FOLLOWING_SYSTEM_HPP
|
||||
@@ -13,8 +13,7 @@ std::string PrefabSystem::getPrefabsDirectory()
|
||||
return "prefabs";
|
||||
}
|
||||
|
||||
PrefabSystem::PrefabSystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr)
|
||||
PrefabSystem::PrefabSystem(flecs::world &world, Ogre::SceneManager *sceneMgr)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
{
|
||||
@@ -35,7 +34,8 @@ void PrefabSystem::resolveInstances()
|
||||
m_lastError = serializer.getLastError();
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"PrefabSystem: Failed to instantiate '" +
|
||||
prefab.prefabPath + "': " + m_lastError);
|
||||
prefab.prefabPath +
|
||||
"': " + m_lastError);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -116,12 +116,12 @@ flecs::entity PrefabSystem::createInstance(const std::string &prefabPath,
|
||||
transform.node->_getDerivedPosition()) +
|
||||
" parent=" +
|
||||
(parent.is_valid() && parent != 0 ?
|
||||
std::to_string(parent.id()) :
|
||||
"<root>") +
|
||||
std::to_string(parent.id()) :
|
||||
"<root>") +
|
||||
" nodeParent=" +
|
||||
(parentNode != m_sceneMgr->getRootSceneNode() ?
|
||||
parentNode->getName() :
|
||||
"<root>"));
|
||||
parentNode->getName() :
|
||||
"<root>"));
|
||||
|
||||
if (uiSystem)
|
||||
uiSystem->addEntity(instance);
|
||||
@@ -151,3 +151,24 @@ bool PrefabSystem::savePrefab(flecs::entity rootEntity,
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrefabSystem::deletePrefab(const std::string &prefabPath)
|
||||
{
|
||||
try {
|
||||
if (!std::filesystem::exists(prefabPath)) {
|
||||
m_lastError = "Prefab file not found: " + prefabPath;
|
||||
return false;
|
||||
}
|
||||
if (!std::filesystem::remove(prefabPath)) {
|
||||
m_lastError = "Failed to delete prefab: " + prefabPath;
|
||||
return false;
|
||||
}
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"PrefabSystem: Deleted prefab '" + prefabPath + "'");
|
||||
return true;
|
||||
} catch (const std::exception &e) {
|
||||
m_lastError =
|
||||
std::string("Failed to delete prefab: ") + e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,13 @@ public:
|
||||
bool savePrefab(flecs::entity rootEntity,
|
||||
const std::string &prefabPath);
|
||||
|
||||
/**
|
||||
* @brief Delete a prefab JSON file.
|
||||
* @param prefabPath Path to the prefab file to delete.
|
||||
* @return true on success.
|
||||
*/
|
||||
bool deletePrefab(const std::string &prefabPath);
|
||||
|
||||
/**
|
||||
* @brief Get the directory where prefabs are stored.
|
||||
*/
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/ActionDebug.hpp"
|
||||
#include "../components/SmartObject.hpp"
|
||||
#include "../components/GoapPlanner.hpp"
|
||||
#include "../components/PathFollowing.hpp"
|
||||
#include "../components/BehaviorTree.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "../components/NavMesh.hpp"
|
||||
@@ -295,9 +297,15 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
|
||||
if (entity.has<ActionDebug>()) {
|
||||
json["actionDebug"] = serializeActionDebug(entity);
|
||||
}
|
||||
if (entity.has<PathFollowingComponent>()) {
|
||||
json["pathFollowing"] = serializePathFollowing(entity);
|
||||
}
|
||||
if (entity.has<SmartObjectComponent>()) {
|
||||
json["smartObject"] = serializeSmartObject(entity);
|
||||
}
|
||||
if (entity.has<GoapPlannerComponent>()) {
|
||||
json["goapPlanner"] = serializeGoapPlanner(entity);
|
||||
}
|
||||
if (entity.has<BehaviorTreeComponent>()) {
|
||||
json["behaviorTree"] = serializeBehaviorTree(entity);
|
||||
}
|
||||
@@ -495,9 +503,15 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json,
|
||||
if (json.contains("actionDebug")) {
|
||||
deserializeActionDebug(entity, json["actionDebug"]);
|
||||
}
|
||||
if (json.contains("pathFollowing")) {
|
||||
deserializePathFollowing(entity, json["pathFollowing"]);
|
||||
}
|
||||
if (json.contains("smartObject")) {
|
||||
deserializeSmartObject(entity, json["smartObject"]);
|
||||
}
|
||||
if (json.contains("goapPlanner")) {
|
||||
deserializeGoapPlanner(entity, json["goapPlanner"]);
|
||||
}
|
||||
if (json.contains("behaviorTree")) {
|
||||
deserializeBehaviorTree(entity, json["behaviorTree"]);
|
||||
}
|
||||
@@ -771,9 +785,15 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
if (json.contains("actionDebug")) {
|
||||
deserializeActionDebug(entity, json["actionDebug"]);
|
||||
}
|
||||
if (json.contains("pathFollowing")) {
|
||||
deserializePathFollowing(entity, json["pathFollowing"]);
|
||||
}
|
||||
if (json.contains("smartObject")) {
|
||||
deserializeSmartObject(entity, json["smartObject"]);
|
||||
}
|
||||
if (json.contains("goapPlanner")) {
|
||||
deserializeGoapPlanner(entity, json["goapPlanner"]);
|
||||
}
|
||||
if (json.contains("behaviorTree")) {
|
||||
deserializeBehaviorTree(entity, json["behaviorTree"]);
|
||||
}
|
||||
@@ -3172,6 +3192,8 @@ static nlohmann::json serializeGoapBlackboard(const GoapBlackboard &bb)
|
||||
nlohmann::json json;
|
||||
json["bits"] = (uint64_t)bb.bits;
|
||||
json["mask"] = (uint64_t)bb.mask;
|
||||
if (bb.bitmask != ~0ULL)
|
||||
json["bitmask"] = (uint64_t)bb.bitmask;
|
||||
if (!bb.values.empty()) {
|
||||
json["values"] = nlohmann::json::object();
|
||||
for (const auto &pair : bb.values)
|
||||
@@ -3200,6 +3222,7 @@ static void deserializeGoapBlackboard(GoapBlackboard &bb,
|
||||
{
|
||||
bb.bits = json.value("bits", (uint64_t)0);
|
||||
bb.mask = json.value("mask", (uint64_t)0);
|
||||
bb.bitmask = json.value("bitmask", ~0ULL);
|
||||
bb.values.clear();
|
||||
if (json.contains("values") && json["values"].is_object()) {
|
||||
for (auto &[key, val] : json["values"].items())
|
||||
@@ -3262,6 +3285,8 @@ static nlohmann::json serializeGoapAction(const GoapAction &action)
|
||||
json["cost"] = action.cost;
|
||||
json["preconditions"] = serializeGoapBlackboard(action.preconditions);
|
||||
json["effects"] = serializeGoapBlackboard(action.effects);
|
||||
if (action.preconditionMask != ~0ULL)
|
||||
json["preconditionMask"] = action.preconditionMask;
|
||||
json["behaviorTree"] = serializeBehaviorTreeNode(action.behaviorTree);
|
||||
if (!action.behaviorTreeName.empty())
|
||||
json["behaviorTreeName"] = action.behaviorTreeName;
|
||||
@@ -3278,6 +3303,7 @@ static void deserializeGoapAction(GoapAction &action,
|
||||
json["preconditions"]);
|
||||
if (json.contains("effects"))
|
||||
deserializeGoapBlackboard(action.effects, json["effects"]);
|
||||
action.preconditionMask = json.value("preconditionMask", ~0ULL);
|
||||
if (json.contains("behaviorTree"))
|
||||
deserializeBehaviorTreeNode(action.behaviorTree,
|
||||
json["behaviorTree"]);
|
||||
@@ -3377,10 +3403,15 @@ nlohmann::json SceneSerializer::serializeActionDebug(flecs::entity entity)
|
||||
json["selectedActionName"] = debug.selectedActionName;
|
||||
if (!debug.selectedGoalName.empty())
|
||||
json["selectedGoalName"] = debug.selectedGoalName;
|
||||
return json;
|
||||
}
|
||||
|
||||
// Serialize path following animation states
|
||||
nlohmann::json SceneSerializer::serializePathFollowing(flecs::entity entity)
|
||||
{
|
||||
const PathFollowingComponent &pf = entity.get<PathFollowingComponent>();
|
||||
nlohmann::json json;
|
||||
json["pathFollowingStates"] = nlohmann::json::array();
|
||||
for (const auto &pfState : debug.pathFollowingStates) {
|
||||
for (const auto &pfState : pf.pathFollowingStates) {
|
||||
nlohmann::json entry;
|
||||
entry["name"] = pfState.name;
|
||||
entry["stateMachineStates"] = nlohmann::json::array();
|
||||
@@ -3392,26 +3423,30 @@ nlohmann::json SceneSerializer::serializeActionDebug(flecs::entity entity)
|
||||
}
|
||||
json["pathFollowingStates"].push_back(entry);
|
||||
}
|
||||
|
||||
json["walkSpeed"] = debug.walkSpeed;
|
||||
json["runSpeed"] = debug.runSpeed;
|
||||
json["useRootMotion"] = debug.useRootMotion;
|
||||
json["walkSpeed"] = pf.walkSpeed;
|
||||
json["runSpeed"] = pf.runSpeed;
|
||||
json["useRootMotion"] = pf.useRootMotion;
|
||||
return json;
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeActionDebug(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
ActionDebug debug;
|
||||
if (json.contains("blackboard"))
|
||||
deserializeGoapBlackboard(debug.blackboard, json["blackboard"]);
|
||||
debug.selectedActionName = json.value("selectedActionName", "");
|
||||
debug.selectedGoalName = json.value("selectedGoalName", "");
|
||||
entity.set<ActionDebug>(debug);
|
||||
}
|
||||
|
||||
// Deserialize path following animation states
|
||||
void SceneSerializer::deserializePathFollowing(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
PathFollowingComponent pf;
|
||||
if (json.contains("pathFollowingStates") &&
|
||||
json["pathFollowingStates"].is_array()) {
|
||||
debug.pathFollowingStates.clear();
|
||||
pf.pathFollowingStates.clear();
|
||||
for (const auto &entry : json["pathFollowingStates"]) {
|
||||
PathFollowingState pfState;
|
||||
pfState.name = entry.value("name", "");
|
||||
@@ -3427,12 +3462,12 @@ void SceneSerializer::deserializeActionDebug(flecs::entity entity,
|
||||
{ sm, state });
|
||||
}
|
||||
}
|
||||
debug.pathFollowingStates.push_back(pfState);
|
||||
pf.pathFollowingStates.push_back(pfState);
|
||||
}
|
||||
} else if (json.contains("animStates") &&
|
||||
json["animStates"].is_array()) {
|
||||
// Backward compatibility: old animStates format
|
||||
debug.pathFollowingStates.clear();
|
||||
pf.pathFollowingStates.clear();
|
||||
for (const auto &entry : json["animStates"]) {
|
||||
PathFollowingState pfState;
|
||||
pfState.name = entry.value("name", "");
|
||||
@@ -3445,40 +3480,20 @@ void SceneSerializer::deserializeActionDebug(flecs::entity entity,
|
||||
pfState.stateMachineStates.push_back({ mainSM, subSM });
|
||||
pfState.stateMachineStates.push_back(
|
||||
{ subSM, stateName });
|
||||
debug.pathFollowingStates.push_back(pfState);
|
||||
pf.pathFollowingStates.push_back(pfState);
|
||||
}
|
||||
} else {
|
||||
// Backward compatibility: old individual fields format
|
||||
debug.pathFollowingStates.clear();
|
||||
Ogre::String sm =
|
||||
json.value("locomotionStateMachine", "Locomotion");
|
||||
{
|
||||
PathFollowingState idle("idle");
|
||||
idle.stateMachineStates.push_back({ "main", sm });
|
||||
idle.stateMachineStates.push_back(
|
||||
{ sm, json.value("idleStateName", "Idle") });
|
||||
debug.pathFollowingStates.push_back(idle);
|
||||
}
|
||||
{
|
||||
PathFollowingState walk("walk");
|
||||
walk.stateMachineStates.push_back({ "main", sm });
|
||||
walk.stateMachineStates.push_back(
|
||||
{ sm, json.value("walkStateName", "Walk") });
|
||||
debug.pathFollowingStates.push_back(walk);
|
||||
}
|
||||
{
|
||||
PathFollowingState run("run");
|
||||
run.stateMachineStates.push_back({ "main", sm });
|
||||
run.stateMachineStates.push_back(
|
||||
{ sm, json.value("runStateName", "Run") });
|
||||
debug.pathFollowingStates.push_back(run);
|
||||
}
|
||||
// Default states
|
||||
pf.pathFollowingStates.clear();
|
||||
pf.pathFollowingStates.push_back(PathFollowingState("idle"));
|
||||
pf.pathFollowingStates.push_back(PathFollowingState("walk"));
|
||||
pf.pathFollowingStates.push_back(PathFollowingState("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);
|
||||
pf.walkSpeed = json.value("walkSpeed", 2.5f);
|
||||
pf.runSpeed = json.value("runSpeed", 5.0f);
|
||||
pf.useRootMotion = json.value("useRootMotion", true);
|
||||
entity.set<PathFollowingComponent>(pf);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeSmartObject(flecs::entity entity)
|
||||
@@ -3507,6 +3522,45 @@ void SceneSerializer::deserializeSmartObject(flecs::entity entity,
|
||||
entity.set<SmartObjectComponent>(so);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeGoapPlanner(flecs::entity entity)
|
||||
{
|
||||
const GoapPlannerComponent &planner = entity.get<GoapPlannerComponent>();
|
||||
nlohmann::json json;
|
||||
json["actionNames"] = planner.actionNames;
|
||||
if (!planner.goalNames.empty())
|
||||
json["goalNames"] = planner.goalNames;
|
||||
json["smartObjectDistance"] = planner.smartObjectDistance;
|
||||
json["includeSmartObjects"] = planner.includeSmartObjects;
|
||||
if (!planner.actionDatabaseRef.empty())
|
||||
json["actionDatabaseRef"] = planner.actionDatabaseRef;
|
||||
return json;
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeGoapPlanner(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
GoapPlannerComponent planner;
|
||||
if (json.contains("actionNames") && json["actionNames"].is_array()) {
|
||||
planner.actionNames.clear();
|
||||
for (const auto &name : json["actionNames"]) {
|
||||
if (name.is_string())
|
||||
planner.actionNames.push_back(name);
|
||||
}
|
||||
}
|
||||
if (json.contains("goalNames") && json["goalNames"].is_array()) {
|
||||
planner.goalNames.clear();
|
||||
for (const auto &name : json["goalNames"]) {
|
||||
if (name.is_string())
|
||||
planner.goalNames.push_back(name);
|
||||
}
|
||||
}
|
||||
planner.smartObjectDistance = json.value("smartObjectDistance", 50.0f);
|
||||
planner.includeSmartObjects = json.value("includeSmartObjects", true);
|
||||
planner.actionDatabaseRef = json.value("actionDatabaseRef", "");
|
||||
planner.planDirty = true;
|
||||
entity.set<GoapPlannerComponent>(planner);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeBehaviorTree(flecs::entity entity)
|
||||
{
|
||||
const BehaviorTreeComponent &bt = entity.get<BehaviorTreeComponent>();
|
||||
|
||||
@@ -208,14 +208,20 @@ private:
|
||||
// AI/GOAP serialization
|
||||
nlohmann::json serializeActionDatabase(flecs::entity entity);
|
||||
nlohmann::json serializeActionDebug(flecs::entity entity);
|
||||
nlohmann::json serializePathFollowing(flecs::entity entity);
|
||||
nlohmann::json serializeSmartObject(flecs::entity entity);
|
||||
nlohmann::json serializeGoapPlanner(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 deserializePathFollowing(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeSmartObject(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeGoapPlanner(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeBehaviorTree(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/ActionDebug.hpp"
|
||||
#include "../components/PathFollowing.hpp"
|
||||
#include "../components/NavMesh.hpp"
|
||||
#include "../components/AnimationTree.hpp"
|
||||
#include "../components/PlayerController.hpp"
|
||||
@@ -115,11 +116,11 @@ void SmartObjectSystem::setLocomotionState(flecs::entity e,
|
||||
if (!e.has<AnimationTreeComponent>())
|
||||
return;
|
||||
|
||||
// Look up the path following state in ActionDebug
|
||||
if (e.has<ActionDebug>()) {
|
||||
auto &debug = e.get<ActionDebug>();
|
||||
// Look up the path following state in PathFollowingComponent
|
||||
if (e.has<PathFollowingComponent>()) {
|
||||
auto &pf = e.get<PathFollowingComponent>();
|
||||
const PathFollowingState *pfState =
|
||||
debug.findPathState(animName);
|
||||
pf.findState(animName);
|
||||
if (pfState) {
|
||||
// Apply ALL state machine name -> state name pairs
|
||||
for (const auto &pair : pfState->stateMachineStates) {
|
||||
@@ -174,6 +175,16 @@ bool SmartObjectSystem::testSmartObjectAction(flecs::entity character,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SmartObjectSystem::isIdle(flecs::entity character) const
|
||||
{
|
||||
if (!character.is_alive())
|
||||
return true;
|
||||
auto it = m_states.find(character.id());
|
||||
if (it == m_states.end())
|
||||
return true;
|
||||
return it->second.state == State::Idle;
|
||||
}
|
||||
|
||||
void SmartObjectSystem::update(float deltaTime)
|
||||
{
|
||||
// Find the navmesh entity
|
||||
@@ -550,16 +561,16 @@ void SmartObjectSystem::update(float deltaTime)
|
||||
float speed = 2.5f; // walk speed
|
||||
|
||||
// Use run speed if far away
|
||||
if (e.has<ActionDebug>()) {
|
||||
auto &debug =
|
||||
e.get<ActionDebug>();
|
||||
if (e.has<PathFollowingComponent>()) {
|
||||
auto &pf =
|
||||
e.get<PathFollowingComponent>();
|
||||
if (distToTarget >
|
||||
debug.walkSpeed * 3.0f) {
|
||||
speed = debug.runSpeed;
|
||||
pf.walkSpeed * 3.0f) {
|
||||
speed = pf.runSpeed;
|
||||
setLocomotionState(
|
||||
e, "run");
|
||||
} else {
|
||||
speed = debug.walkSpeed;
|
||||
speed = pf.walkSpeed;
|
||||
setLocomotionState(
|
||||
e, "walk");
|
||||
}
|
||||
@@ -919,14 +930,19 @@ void SmartObjectSystem::update(float deltaTime)
|
||||
|
||||
float distToTarget =
|
||||
(charPos - objPos).length();
|
||||
float speed = debug.walkSpeed;
|
||||
|
||||
if (distToTarget >
|
||||
debug.walkSpeed * 3.0f) {
|
||||
speed = debug.runSpeed;
|
||||
setLocomotionState(e, "run");
|
||||
float speed = 2.5f;
|
||||
if (e.has<PathFollowingComponent>()) {
|
||||
auto &pf = e.get<PathFollowingComponent>();
|
||||
speed = pf.walkSpeed;
|
||||
if (distToTarget >
|
||||
pf.walkSpeed * 3.0f) {
|
||||
speed = pf.runSpeed;
|
||||
setLocomotionState(e, "run");
|
||||
} else {
|
||||
speed = pf.walkSpeed;
|
||||
setLocomotionState(e, "walk");
|
||||
}
|
||||
} else {
|
||||
speed = debug.walkSpeed;
|
||||
setLocomotionState(e, "walk");
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,13 @@ public:
|
||||
flecs::entity smartObject,
|
||||
const Ogre::String &actionName);
|
||||
|
||||
/**
|
||||
* Check if a character's smart object interaction is idle.
|
||||
* Returns true if the character is not currently pathfinding,
|
||||
* moving, or executing a smart object action.
|
||||
*/
|
||||
bool isIdle(flecs::entity character) const;
|
||||
|
||||
private:
|
||||
struct SmartObjectTarget {
|
||||
flecs::entity smartObject;
|
||||
|
||||
@@ -173,6 +173,39 @@ void ActionDatabaseEditor::renderActionEditor(GoapAction &action)
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
// Precondition mask editor
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Precondition Mask (bits to check)");
|
||||
for (int i = 0; i < 64; i += 8) {
|
||||
for (int j = 0; j < 8 && (i + j) < 64; j++) {
|
||||
int bit = i + j;
|
||||
bool enabled = (action.preconditionMask >>
|
||||
bit) & 1ULL;
|
||||
char label[8];
|
||||
snprintf(label, sizeof(label), "%d", bit);
|
||||
if (j > 0)
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox(label, &enabled)) {
|
||||
if (enabled)
|
||||
action.preconditionMask |=
|
||||
(1ULL << bit);
|
||||
else
|
||||
action.preconditionMask &=
|
||||
~(1ULL << bit);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::Text("Mask: 0x%016llX",
|
||||
(unsigned long long)action.preconditionMask);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Set All##mask")) {
|
||||
action.preconditionMask = ~0ULL;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Clear All##mask")) {
|
||||
action.preconditionMask = 0ULL;
|
||||
}
|
||||
|
||||
if (ImGui::TreeNode("Effects")) {
|
||||
GoapBlackboardEditor::render(action.effects, "effects");
|
||||
ImGui::TreePop();
|
||||
|
||||
@@ -34,14 +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 (ImGui::CollapsingHeader("Planner",
|
||||
ImGuiTreeNodeFlags_DefaultOpen))
|
||||
renderPlanner(entity, debug);
|
||||
|
||||
if (!debug.lastResult.empty()) {
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Last result: %s", debug.lastResult.c_str());
|
||||
@@ -151,122 +151,6 @@ void ActionDebugEditor::renderGoalTester(flecs::entity entity,
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
void ActionDebugEditor::renderAnimationConfig(ActionDebug &debug)
|
||||
{
|
||||
ImGui::Text("Path Following Animation States:");
|
||||
ImGui::TextDisabled(
|
||||
"Each state (idle/walk/run/swim-idle/swim/swim-fast) has unlimited\n"
|
||||
"state machine name -> state name pairs applied when activated.");
|
||||
ImGui::Separator();
|
||||
|
||||
// Render each path following state
|
||||
int removeStateIdx = -1;
|
||||
for (int i = 0; i < (int)debug.pathFollowingStates.size(); i++) {
|
||||
auto &pfState = debug.pathFollowingStates[i];
|
||||
ImGui::PushID(i);
|
||||
|
||||
char buf[256];
|
||||
|
||||
ImGui::Text("State %d:", i);
|
||||
ImGui::Indent();
|
||||
|
||||
// Logical name (e.g. "idle", "walk", "run")
|
||||
strncpy(buf, pfState.name.c_str(), sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
if (ImGui::InputText("Name", buf, sizeof(buf))) {
|
||||
pfState.name = buf;
|
||||
}
|
||||
|
||||
ImGui::Text("State Machine -> State pairs:");
|
||||
ImGui::Indent();
|
||||
|
||||
// Render each name-value pair
|
||||
int removePairIdx = -1;
|
||||
for (int j = 0; j < (int)pfState.stateMachineStates.size();
|
||||
j++) {
|
||||
auto &pair = pfState.stateMachineStates[j];
|
||||
ImGui::PushID(j);
|
||||
|
||||
char smBuf[128];
|
||||
char stateBuf[128];
|
||||
|
||||
strncpy(smBuf, pair.first.c_str(), sizeof(smBuf) - 1);
|
||||
smBuf[sizeof(smBuf) - 1] = '\0';
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
if (ImGui::InputText("##sm", smBuf, sizeof(smBuf))) {
|
||||
pair.first = smBuf;
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("->");
|
||||
|
||||
ImGui::SameLine();
|
||||
strncpy(stateBuf, pair.second.c_str(),
|
||||
sizeof(stateBuf) - 1);
|
||||
stateBuf[sizeof(stateBuf) - 1] = '\0';
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
if (ImGui::InputText("##state", stateBuf,
|
||||
sizeof(stateBuf))) {
|
||||
pair.second = stateBuf;
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X")) {
|
||||
removePairIdx = j;
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (removePairIdx >= 0) {
|
||||
pfState.stateMachineStates.erase(
|
||||
pfState.stateMachineStates.begin() +
|
||||
removePairIdx);
|
||||
}
|
||||
|
||||
if (ImGui::SmallButton("+ Add Pair")) {
|
||||
pfState.stateMachineStates.push_back(
|
||||
{ "main", "Idle" });
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
|
||||
// Remove state button
|
||||
if (debug.pathFollowingStates.size() > 1) {
|
||||
if (ImGui::SmallButton("Remove State")) {
|
||||
removeStateIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
ImGui::Separator();
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (removeStateIdx >= 0) {
|
||||
debug.pathFollowingStates.erase(
|
||||
debug.pathFollowingStates.begin() + removeStateIdx);
|
||||
}
|
||||
|
||||
// Add new state button
|
||||
if (ImGui::Button("Add Path Following State")) {
|
||||
debug.pathFollowingStates.push_back(
|
||||
PathFollowingState("new_state"));
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -339,3 +223,113 @@ void ActionDebugEditor::renderSmartObjectTester(flecs::entity entity,
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
void ActionDebugEditor::renderPlanner(flecs::entity entity,
|
||||
ActionDebug &debug)
|
||||
{
|
||||
(void)debug;
|
||||
|
||||
if (!entity.has<GoapPlannerComponent>()) {
|
||||
ImGui::TextDisabled(
|
||||
"No GoapPlannerComponent found on this entity.");
|
||||
return;
|
||||
}
|
||||
|
||||
auto &planner = entity.get_mut<GoapPlannerComponent>();
|
||||
|
||||
// Status
|
||||
const char *statusStr = "Idle";
|
||||
ImVec4 statusColor(0.7f, 0.7f, 0.7f, 1.0f);
|
||||
switch (planner.status) {
|
||||
case GoapPlannerComponent::Status::Idle:
|
||||
statusStr = "Idle";
|
||||
break;
|
||||
case GoapPlannerComponent::Status::Planning:
|
||||
statusStr = "Planning...";
|
||||
statusColor = ImVec4(1.0f, 0.8f, 0.2f, 1.0f);
|
||||
break;
|
||||
case GoapPlannerComponent::Status::PlansAvailable:
|
||||
statusStr = "Plans Available";
|
||||
statusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f);
|
||||
break;
|
||||
case GoapPlannerComponent::Status::NoPlanFound:
|
||||
statusStr = "No Plan Found";
|
||||
statusColor = ImVec4(1.0f, 0.3f, 0.3f, 1.0f);
|
||||
break;
|
||||
}
|
||||
ImGui::Text("Status: ");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(statusColor, "%s", statusStr);
|
||||
|
||||
if (planner.status == GoapPlannerComponent::Status::Planning) {
|
||||
ImGui::Text("Nodes explored: %d",
|
||||
planner.nodesExplored);
|
||||
}
|
||||
|
||||
if (!planner.currentGoalName.empty()) {
|
||||
ImGui::Text("Current Goal: %s",
|
||||
planner.currentGoalName.c_str());
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Display plan queue
|
||||
if (planner.planQueue.empty()) {
|
||||
ImGui::TextDisabled("No plans in queue.");
|
||||
} else {
|
||||
ImGui::Text("Plan Queue (%zu plans):",
|
||||
planner.planQueue.size());
|
||||
ImGui::Indent();
|
||||
|
||||
ActionDatabase *db = findDatabase(entity);
|
||||
|
||||
for (int p = 0; p < (int)planner.planQueue.size(); p++) {
|
||||
const auto &plan = planner.planQueue[p];
|
||||
ImGui::Text("Plan %d (cost: %d, goal: %s):",
|
||||
p + 1, plan.totalCost,
|
||||
plan.goalName.c_str());
|
||||
ImGui::Indent();
|
||||
for (int i = 0;
|
||||
i < (int)plan.actions.size(); i++) {
|
||||
const auto &actionName = plan.actions[i];
|
||||
bool isSmartObjectAction = false;
|
||||
if (db) {
|
||||
const GoapAction *action =
|
||||
db->findAction(actionName);
|
||||
if (!action)
|
||||
isSmartObjectAction = true;
|
||||
}
|
||||
if (isSmartObjectAction) {
|
||||
ImGui::TextColored(
|
||||
ImVec4(0.3f, 0.8f, 1.0f,
|
||||
1.0f),
|
||||
"%d. %s [SO]", i + 1,
|
||||
actionName.c_str());
|
||||
} else {
|
||||
ImGui::Text("%d. %s", i + 1,
|
||||
actionName.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Trigger replan button
|
||||
if (ImGui::Button("Force Replan")) {
|
||||
planner.planDirty = true;
|
||||
}
|
||||
|
||||
// Show selected planner actions
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Planner Action Pool (%zu):",
|
||||
planner.actionNames.size());
|
||||
ImGui::Indent();
|
||||
for (const auto &name : planner.actionNames) {
|
||||
ImGui::Text("%s", name.c_str());
|
||||
}
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "../components/ActionDebug.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/SmartObject.hpp"
|
||||
#include "../components/GoapPlanner.hpp"
|
||||
|
||||
/**
|
||||
* Editor for ActionDebug component.
|
||||
@@ -28,8 +29,8 @@ 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);
|
||||
void renderPlanner(flecs::entity entity, ActionDebug &debug);
|
||||
|
||||
ActionDatabase *findDatabase(flecs::entity entity);
|
||||
};
|
||||
|
||||
296
src/features/editScene/ui/GoapPlannerEditor.cpp
Normal file
296
src/features/editScene/ui/GoapPlannerEditor.cpp
Normal file
@@ -0,0 +1,296 @@
|
||||
#include "GoapPlannerEditor.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
ActionDatabase *GoapPlannerEditor::findDatabase(
|
||||
flecs::entity entity, GoapPlannerComponent &planner)
|
||||
{
|
||||
auto world = entity.world();
|
||||
|
||||
// 1. Try to find by referenced entity name
|
||||
if (!planner.actionDatabaseRef.empty()) {
|
||||
ActionDatabase *found = nullptr;
|
||||
world.query<ActionDatabase>()
|
||||
.each([&](flecs::entity dbEntity, ActionDatabase &db) {
|
||||
if (found)
|
||||
return;
|
||||
if (dbEntity.has<EntityNameComponent>()) {
|
||||
auto &name =
|
||||
dbEntity.get<EntityNameComponent>();
|
||||
if (name.name ==
|
||||
planner.actionDatabaseRef)
|
||||
found = &db;
|
||||
}
|
||||
});
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
|
||||
// 2. Fall back to any ActionDatabase in the world
|
||||
ActionDatabase *db = nullptr;
|
||||
world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
return db;
|
||||
}
|
||||
|
||||
bool GoapPlannerEditor::renderComponent(flecs::entity entity,
|
||||
GoapPlannerComponent &planner)
|
||||
{
|
||||
bool modified = false;
|
||||
ImGui::PushID("GoapPlanner");
|
||||
|
||||
// External ActionDatabase reference
|
||||
char refBuf[256];
|
||||
snprintf(refBuf, sizeof(refBuf), "%s",
|
||||
planner.actionDatabaseRef.c_str());
|
||||
if (ImGui::InputText("Action Database Ref", refBuf,
|
||||
sizeof(refBuf))) {
|
||||
planner.actionDatabaseRef = refBuf;
|
||||
modified = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Name of the entity that owns the ActionDatabase. "
|
||||
"Used to resolve action and goal names when editing prefabs.");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Smart object settings
|
||||
if (ImGui::DragFloat("Smart Object Distance",
|
||||
&planner.smartObjectDistance, 0.5f,
|
||||
1.0f, 500.0f)) {
|
||||
modified = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Maximum distance to search for smart objects "
|
||||
"with matching actions");
|
||||
}
|
||||
|
||||
if (ImGui::Checkbox("Include Smart Objects",
|
||||
&planner.includeSmartObjects)) {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Action and Goal selectors
|
||||
ActionDatabase *db = findDatabase(entity, planner);
|
||||
renderActionSelector(planner, db);
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
renderGoalSelector(planner, db);
|
||||
|
||||
ImGui::PopID();
|
||||
return modified;
|
||||
}
|
||||
|
||||
void GoapPlannerEditor::renderActionSelector(
|
||||
GoapPlannerComponent &planner, ActionDatabase *db)
|
||||
{
|
||||
ImGui::Text("Selected Actions (%zu):",
|
||||
planner.actionNames.size());
|
||||
|
||||
// Show currently selected actions with remove buttons
|
||||
int removeIdx = -1;
|
||||
for (int i = 0; i < (int)planner.actionNames.size(); i++) {
|
||||
ImGui::PushID(i);
|
||||
ImGui::Text("%s", planner.actionNames[i].c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X")) {
|
||||
removeIdx = i;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (removeIdx >= 0) {
|
||||
planner.actionNames.erase(
|
||||
planner.actionNames.begin() + removeIdx);
|
||||
planner.planDirty = true;
|
||||
}
|
||||
|
||||
if (planner.actionNames.empty()) {
|
||||
ImGui::TextDisabled("No actions selected");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Add actions from database
|
||||
if (!db) {
|
||||
ImGui::TextDisabled(
|
||||
"No ActionDatabase found. Add actions manually:");
|
||||
static char manualAction[256] = { 0 };
|
||||
if (ImGui::InputText("Action Name", manualAction,
|
||||
sizeof(manualAction),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
if (manualAction[0] != '\0') {
|
||||
planner.actionNames.push_back(manualAction);
|
||||
planner.planDirty = true;
|
||||
manualAction[0] = '\0';
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::Text("Available Actions from Database:");
|
||||
ImGui::Indent();
|
||||
|
||||
for (const auto &action : db->actions) {
|
||||
bool alreadySelected = false;
|
||||
for (const auto &name : planner.actionNames) {
|
||||
if (name == action.name) {
|
||||
alreadySelected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PushID(action.name.c_str());
|
||||
|
||||
if (alreadySelected) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
|
||||
if (ImGui::Button("+")) {
|
||||
planner.actionNames.push_back(action.name);
|
||||
planner.planDirty = true;
|
||||
}
|
||||
|
||||
if (alreadySelected) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%s (cost: %d)", action.name.c_str(),
|
||||
action.cost);
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
void GoapPlannerEditor::renderGoalSelector(
|
||||
GoapPlannerComponent &planner, ActionDatabase *db)
|
||||
{
|
||||
ImGui::Text("Selected Goals (%zu):",
|
||||
planner.goalNames.size());
|
||||
|
||||
// Show currently selected goals with remove buttons
|
||||
int removeIdx = -1;
|
||||
for (int i = 0; i < (int)planner.goalNames.size(); i++) {
|
||||
ImGui::PushID(i + 10000);
|
||||
ImGui::Text("%s", planner.goalNames[i].c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X")) {
|
||||
removeIdx = i;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (removeIdx >= 0) {
|
||||
planner.goalNames.erase(
|
||||
planner.goalNames.begin() + removeIdx);
|
||||
planner.planDirty = true;
|
||||
}
|
||||
|
||||
if (planner.goalNames.empty()) {
|
||||
ImGui::TextDisabled("No goals selected");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Add goals from database
|
||||
if (!db) {
|
||||
ImGui::TextDisabled(
|
||||
"No ActionDatabase found. Add goals manually:");
|
||||
static char manualGoal[256] = { 0 };
|
||||
if (ImGui::InputText("Goal Name", manualGoal,
|
||||
sizeof(manualGoal),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
if (manualGoal[0] != '\0') {
|
||||
planner.goalNames.push_back(manualGoal);
|
||||
planner.planDirty = true;
|
||||
manualGoal[0] = '\0';
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::Text("Available Goals from Database:");
|
||||
ImGui::Indent();
|
||||
|
||||
for (const auto &goal : db->goals) {
|
||||
bool alreadySelected = false;
|
||||
for (const auto &name : planner.goalNames) {
|
||||
if (name == goal.name) {
|
||||
alreadySelected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PushID(goal.name.c_str());
|
||||
|
||||
if (alreadySelected) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
|
||||
if (ImGui::Button("+")) {
|
||||
planner.goalNames.push_back(goal.name);
|
||||
planner.planDirty = true;
|
||||
}
|
||||
|
||||
if (alreadySelected) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%s (prio: %d)", goal.name.c_str(),
|
||||
goal.priority);
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
void GoapPlannerEditor::renderPreconditionMaskEditor(
|
||||
GoapAction &action)
|
||||
{
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Precondition Mask (bit filter)");
|
||||
ImGui::TextDisabled(
|
||||
"Only bits set here are checked during planning.");
|
||||
|
||||
for (int i = 0; i < 64; i += 8) {
|
||||
for (int j = 0; j < 8 && (i + j) < 64; j++) {
|
||||
int bit = i + j;
|
||||
bool enabled = (action.preconditionMask >>
|
||||
bit) & 1ULL;
|
||||
char label[8];
|
||||
snprintf(label, sizeof(label), "%d", bit);
|
||||
if (j > 0)
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox(label, &enabled)) {
|
||||
if (enabled)
|
||||
action.preconditionMask |= (1ULL << bit);
|
||||
else
|
||||
action.preconditionMask &=
|
||||
~(1ULL << bit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Text("Mask: 0x%016llX",
|
||||
(unsigned long long)action.preconditionMask);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Set All")) {
|
||||
action.preconditionMask = ~0ULL;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Clear All")) {
|
||||
action.preconditionMask = 0ULL;
|
||||
}
|
||||
}
|
||||
37
src/features/editScene/ui/GoapPlannerEditor.hpp
Normal file
37
src/features/editScene/ui/GoapPlannerEditor.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#ifndef EDITSCENE_GOAP_PLANNER_EDITOR_HPP
|
||||
#define EDITSCENE_GOAP_PLANNER_EDITOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/GoapPlanner.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
|
||||
/**
|
||||
* Editor for GoapPlannerComponent.
|
||||
*
|
||||
* Allows selecting actions from an ActionDatabase,
|
||||
* configuring smart-object distance, and referencing
|
||||
* an external ActionDatabase entity.
|
||||
*/
|
||||
class GoapPlannerEditor : public ComponentEditor<GoapPlannerComponent> {
|
||||
public:
|
||||
const char *getName() const override
|
||||
{
|
||||
return "GOAP Planner";
|
||||
}
|
||||
|
||||
protected:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
GoapPlannerComponent &planner) override;
|
||||
|
||||
private:
|
||||
ActionDatabase *findDatabase(flecs::entity entity,
|
||||
GoapPlannerComponent &planner);
|
||||
void renderActionSelector(GoapPlannerComponent &planner,
|
||||
ActionDatabase *db);
|
||||
void renderGoalSelector(GoapPlannerComponent &planner,
|
||||
ActionDatabase *db);
|
||||
void renderPreconditionMaskEditor(GoapAction &action);
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GOAP_PLANNER_EDITOR_HPP
|
||||
99
src/features/editScene/ui/GoapRunnerEditor.cpp
Normal file
99
src/features/editScene/ui/GoapRunnerEditor.cpp
Normal file
@@ -0,0 +1,99 @@
|
||||
#include "GoapRunnerEditor.hpp"
|
||||
#include "../components/GoapPlanner.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
const char *GoapRunnerEditor::stateName(GoapRunnerComponent::State state)
|
||||
{
|
||||
switch (state) {
|
||||
case GoapRunnerComponent::State::Idle:
|
||||
return "Idle";
|
||||
case GoapRunnerComponent::State::RunningAction:
|
||||
return "Running Action";
|
||||
case GoapRunnerComponent::State::MovingToSmartObject:
|
||||
return "Moving to Smart Object";
|
||||
case GoapRunnerComponent::State::ExecutingSmartObject:
|
||||
return "Executing Smart Object";
|
||||
case GoapRunnerComponent::State::PlanComplete:
|
||||
return "Plan Complete";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
bool GoapRunnerEditor::renderComponent(flecs::entity entity,
|
||||
GoapRunnerComponent &runner)
|
||||
{
|
||||
bool modified = false;
|
||||
ImGui::PushID("GoapRunner");
|
||||
|
||||
ImGui::Text("State: %s", stateName(runner.state));
|
||||
ImGui::Text("Action Index: %d", runner.currentActionIndex);
|
||||
if (!runner.currentActionName.empty()) {
|
||||
ImGui::Text("Current Action: %s",
|
||||
runner.currentActionName.c_str());
|
||||
}
|
||||
if (runner.targetSmartObjectId != 0) {
|
||||
ImGui::Text("Target Smart Object: %llu",
|
||||
(unsigned long long)runner.targetSmartObjectId);
|
||||
}
|
||||
|
||||
if (ImGui::Checkbox("Auto Replan", &runner.autoReplan))
|
||||
modified = true;
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Display active plan
|
||||
if (!runner.planActions.empty()) {
|
||||
ImGui::Text("Active Plan (%zu actions):",
|
||||
runner.planActions.size());
|
||||
ImGui::Indent();
|
||||
for (int i = 0; i < (int)runner.planActions.size(); i++) {
|
||||
bool isCurrent = (i == runner.currentActionIndex);
|
||||
if (isCurrent) {
|
||||
ImGui::TextColored(ImVec4(0, 1, 0, 1),
|
||||
"-> %d. %s", i + 1,
|
||||
runner.planActions[i].c_str());
|
||||
} else {
|
||||
ImGui::Text(" %d. %s", i + 1,
|
||||
runner.planActions[i].c_str());
|
||||
}
|
||||
}
|
||||
ImGui::Unindent();
|
||||
} else {
|
||||
ImGui::TextDisabled("No active plan.");
|
||||
}
|
||||
|
||||
// Display plan queue
|
||||
if (entity.has<GoapPlannerComponent>()) {
|
||||
auto &planner = entity.get<GoapPlannerComponent>();
|
||||
if (!planner.planQueue.empty()) {
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Plan Queue (%zu plans):",
|
||||
planner.planQueue.size());
|
||||
ImGui::Indent();
|
||||
for (int p = 0;
|
||||
p < (int)planner.planQueue.size(); p++) {
|
||||
const auto &plan = planner.planQueue[p];
|
||||
ImGui::Text("Plan %d: cost=%d, %zu actions",
|
||||
p + 1, plan.totalCost,
|
||||
plan.actions.size());
|
||||
}
|
||||
ImGui::Unindent();
|
||||
}
|
||||
}
|
||||
|
||||
// Manual controls
|
||||
if (runner.state != GoapRunnerComponent::State::Idle) {
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Stop")) {
|
||||
runner.state = GoapRunnerComponent::State::Idle;
|
||||
runner.currentActionIndex = 0;
|
||||
runner.currentActionName.clear();
|
||||
runner.targetSmartObjectId = 0;
|
||||
runner.planActions.clear();
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
return modified;
|
||||
}
|
||||
26
src/features/editScene/ui/GoapRunnerEditor.hpp
Normal file
26
src/features/editScene/ui/GoapRunnerEditor.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
#ifndef EDITSCENE_GOAP_RUNNER_EDITOR_HPP
|
||||
#define EDITSCENE_GOAP_RUNNER_EDITOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/GoapRunner.hpp"
|
||||
|
||||
/**
|
||||
* Editor for GoapRunnerComponent.
|
||||
*/
|
||||
class GoapRunnerEditor : public ComponentEditor<GoapRunnerComponent> {
|
||||
public:
|
||||
const char *getName() const override
|
||||
{
|
||||
return "GOAP Runner";
|
||||
}
|
||||
|
||||
protected:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
GoapRunnerComponent &runner) override;
|
||||
|
||||
private:
|
||||
const char *stateName(GoapRunnerComponent::State state);
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GOAP_RUNNER_EDITOR_HPP
|
||||
103
src/features/editScene/ui/PathFollowingEditor.cpp
Normal file
103
src/features/editScene/ui/PathFollowingEditor.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
#include "PathFollowingEditor.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
bool PathFollowingEditor::renderComponent(flecs::entity entity,
|
||||
PathFollowingComponent &pf)
|
||||
{
|
||||
(void)entity;
|
||||
bool modified = false;
|
||||
ImGui::PushID("PathFollowing");
|
||||
|
||||
// Current state display
|
||||
ImGui::Text("Current State: %s", pf.currentLocomotionState.c_str());
|
||||
ImGui::Separator();
|
||||
|
||||
// Path following states
|
||||
ImGui::Text("Animation States:");
|
||||
int removeStateIdx = -1;
|
||||
for (int i = 0; i < (int)pf.pathFollowingStates.size(); i++) {
|
||||
auto &pfState = pf.pathFollowingStates[i];
|
||||
ImGui::PushID(i);
|
||||
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf), "%s", pfState.name.c_str());
|
||||
if (ImGui::InputText("Name", buf, sizeof(buf))) {
|
||||
pfState.name = buf;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Indent();
|
||||
int removePairIdx = -1;
|
||||
for (int j = 0; j < (int)pfState.stateMachineStates.size(); j++) {
|
||||
auto &pair = pfState.stateMachineStates[j];
|
||||
ImGui::PushID(j);
|
||||
|
||||
char smBuf[128];
|
||||
char stateBuf[128];
|
||||
snprintf(smBuf, sizeof(smBuf), "%s", pair.first.c_str());
|
||||
snprintf(stateBuf, sizeof(stateBuf), "%s",
|
||||
pair.second.c_str());
|
||||
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
if (ImGui::InputText("##sm", smBuf, sizeof(smBuf))) {
|
||||
pair.first = smBuf;
|
||||
modified = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("->");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
if (ImGui::InputText("##state", stateBuf,
|
||||
sizeof(stateBuf))) {
|
||||
pair.second = stateBuf;
|
||||
modified = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X")) {
|
||||
removePairIdx = j;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (removePairIdx >= 0) {
|
||||
pfState.stateMachineStates.erase(
|
||||
pfState.stateMachineStates.begin() +
|
||||
removePairIdx);
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (ImGui::SmallButton("+ Add Pair")) {
|
||||
pfState.stateMachineStates.push_back({ "main", "Idle" });
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (pf.pathFollowingStates.size() > 1) {
|
||||
if (ImGui::SmallButton("Remove State")) {
|
||||
removeStateIdx = i;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
ImGui::Unindent();
|
||||
ImGui::Separator();
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (removeStateIdx >= 0) {
|
||||
pf.pathFollowingStates.erase(
|
||||
pf.pathFollowingStates.begin() + removeStateIdx);
|
||||
}
|
||||
|
||||
if (ImGui::Button("Add State")) {
|
||||
pf.pathFollowingStates.push_back(PathFollowingState("new_state"));
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::DragFloat("Walk Speed", &pf.walkSpeed, 0.1f, 0.5f, 20.0f);
|
||||
ImGui::DragFloat("Run Speed", &pf.runSpeed, 0.1f, 0.5f, 20.0f);
|
||||
if (ImGui::Checkbox("Use Root Motion", &pf.useRootMotion))
|
||||
modified = true;
|
||||
|
||||
ImGui::PopID();
|
||||
return modified;
|
||||
}
|
||||
23
src/features/editScene/ui/PathFollowingEditor.hpp
Normal file
23
src/features/editScene/ui/PathFollowingEditor.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#ifndef EDITSCENE_PATH_FOLLOWING_EDITOR_HPP
|
||||
#define EDITSCENE_PATH_FOLLOWING_EDITOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/PathFollowing.hpp"
|
||||
|
||||
/**
|
||||
* Editor for PathFollowingComponent.
|
||||
*/
|
||||
class PathFollowingEditor : public ComponentEditor<PathFollowingComponent> {
|
||||
public:
|
||||
const char *getName() const override
|
||||
{
|
||||
return "Path Following";
|
||||
}
|
||||
|
||||
protected:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
PathFollowingComponent &pf) override;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_PATH_FOLLOWING_EDITOR_HPP
|
||||
Reference in New Issue
Block a user