Motion fixed

This commit is contained in:
2026-04-27 06:04:14 +03:00
parent a1b74aa2d5
commit 37441aa8fd

View File

@@ -20,6 +20,20 @@
#include <cmath>
#include <unordered_set>
/**
* Check if a character entity has root motion enabled in its
* AnimationTreeComponent. When root motion is active, the
* AnimationTreeSystem computes linearVelocity from animation
* root bone displacement, so SmartObjectSystem should NOT
* overwrite it with direct movement velocity.
*/
static bool hasRootMotion(flecs::entity e)
{
if (!e.has<AnimationTreeComponent>())
return false;
return e.get<AnimationTreeComponent>().useRootMotion;
}
SmartObjectSystem *SmartObjectSystem::s_instance = nullptr;
SmartObjectSystem::SmartObjectSystem(flecs::world &world,
@@ -119,8 +133,7 @@ void SmartObjectSystem::setLocomotionState(flecs::entity e,
// Look up the path following state in PathFollowingComponent
if (e.has<PathFollowingComponent>()) {
auto &pf = e.get<PathFollowingComponent>();
const PathFollowingState *pfState =
pf.findState(animName);
const PathFollowingState *pfState = pf.findState(animName);
if (pfState) {
// Apply ALL state machine name -> state name pairs
for (const auto &pair : pfState->stateMachineStates) {
@@ -562,8 +575,8 @@ void SmartObjectSystem::update(float deltaTime)
// Use run speed if far away
if (e.has<PathFollowingComponent>()) {
auto &pf =
e.get<PathFollowingComponent>();
auto &pf = e.get<
PathFollowingComponent>();
if (distToTarget >
pf.walkSpeed * 3.0f) {
speed = pf.runSpeed;
@@ -578,12 +591,19 @@ void SmartObjectSystem::update(float deltaTime)
setLocomotionState(e, "walk");
}
cc.linearVelocity = toTarget * speed;
// When root motion is enabled, the
// AnimationTreeSystem already computes
// linearVelocity from animation root
// bone displacement. Do NOT overwrite
// it with direct movement velocity.
if (!hasRootMotion(e))
cc.linearVelocity =
toTarget * speed;
// Rotate character to face movement
// direction (Y plane only)
rotateTowards(e, toTarget, deltaTime);
} else {
} else if (!hasRootMotion(e)) {
cc.linearVelocity = Ogre::Vector3::ZERO;
}
return;
@@ -642,167 +662,213 @@ void SmartObjectSystem::update(float deltaTime)
});
// Also process characters with ActionDebug (for editor testing)
m_world.query<CharacterComponent, TransformComponent, ActionDebug>()
.each([&](flecs::entity e, CharacterComponent &cc,
TransformComponent &trans, ActionDebug &debug) {
(void)trans;
(void)debug;
m_world.query<CharacterComponent, TransformComponent, ActionDebug>().each([&](flecs::entity
e,
CharacterComponent
&cc,
TransformComponent
&trans,
ActionDebug
&debug) {
(void)trans;
(void)debug;
// Skip player-controlled characters - they are managed
// by PlayerControllerSystem, not by SmartObjectSystem AI.
if (playerCharacterIds.find(e.id()) !=
playerCharacterIds.end())
return;
// Skip player-controlled characters - they are managed
// by PlayerControllerSystem, not by SmartObjectSystem AI.
if (playerCharacterIds.find(e.id()) != playerCharacterIds.end())
return;
auto &state = m_states[e.id()];
auto &state = m_states[e.id()];
// When the behavior tree is running (via ActionDebug),
// do NOT override animation states - the behavior tree's
// setAnimationState nodes should be the sole controller.
if (debug.isRunning &&
!debug.currentActionName.empty()) {
// Behavior tree controls animations, skip
// locomotion state override
} else if (state.state == State::Idle) {
setLocomotionState(e, "idle");
// When the behavior tree is running (via ActionDebug),
// do NOT override animation states - the behavior tree's
// setAnimationState nodes should be the sole controller.
if (debug.isRunning && !debug.currentActionName.empty()) {
// Behavior tree controls animations, skip
// locomotion state override
} else if (state.state == State::Idle) {
setLocomotionState(e, "idle");
return;
}
GoapBlackboard &bb = debug.blackboard;
// --- State: Pathfinding ---
if (state.state == State::Pathfinding) {
if (!state.target.smartObject.is_alive()) {
std::cout
<< "[SO] Pathfinding: target dead, going idle"
<< std::endl;
state.state = State::Idle;
return;
}
GoapBlackboard &bb = debug.blackboard;
Ogre::Vector3 charPos = getEntityPosition(e);
Ogre::Vector3 objPos =
getEntityPosition(state.target.smartObject);
// --- State: Pathfinding ---
if (state.state == State::Pathfinding) {
if (!state.target.smartObject.is_alive()) {
auto &soComp = state.target.smartObject
.get<SmartObjectComponent>();
if (isInRange(charPos, objPos, soComp.radius,
soComp.height)) {
std::cout
<< "[SO] Pathfinding: already in range, executing"
<< std::endl;
state.state = State::Executing;
state.target.isExecuting = true;
state.target.executionTimer = 0.0f;
cc.linearVelocity = Ogre::Vector3::ZERO;
return;
}
if (navmeshEntity.is_alive() && m_navSystem) {
state.target.path.clear();
state.target.pathIndex = 0;
bool found = m_navSystem->findPath(
navmeshEntity, charPos, objPos,
state.target.path);
if (found && !state.target.path.empty()) {
std::cout
<< "[SO] Pathfinding: target dead, going idle"
<< std::endl;
state.state = State::Idle;
return;
}
Ogre::Vector3 charPos = getEntityPosition(e);
Ogre::Vector3 objPos = getEntityPosition(
state.target.smartObject);
auto &soComp =
state.target.smartObject
.get<SmartObjectComponent>();
if (isInRange(charPos, objPos, soComp.radius,
soComp.height)) {
std::cout
<< "[SO] Pathfinding: already in range, executing"
<< std::endl;
state.state = State::Executing;
state.target.isExecuting = true;
state.target.executionTimer = 0.0f;
cc.linearVelocity = Ogre::Vector3::ZERO;
return;
}
if (navmeshEntity.is_alive() && m_navSystem) {
state.target.path.clear();
state.target.pathIndex = 0;
bool found = m_navSystem->findPath(
navmeshEntity, charPos, objPos,
state.target.path);
if (found &&
!state.target.path.empty()) {
std::cout
<< "[SO] Pathfinding: path found with "
<< state.target.path
.size()
<< " waypoints"
<< std::endl;
state.state = State::Moving;
} else {
std::cout
<< "[SO] Pathfinding: no path found, going idle"
<< std::endl;
state.state = State::Idle;
cc.linearVelocity =
Ogre::Vector3::ZERO;
}
<< "[SO] Pathfinding: path found with "
<< state.target.path.size()
<< " waypoints" << std::endl;
state.state = State::Moving;
} else {
std::cout
<< "[SO] Pathfinding: no navmesh, going direct"
<< std::endl;
state.target.path.clear();
state.target.path.push_back(objPos);
state.target.pathIndex = 0;
state.state = State::Moving;
}
return;
}
// --- State: Moving ---
if (state.state == State::Moving) {
if (!state.target.smartObject.is_alive() ||
state.target.path.empty()) {
std::cout
<< "[SO] Moving: target dead or path empty, idle"
<< "[SO] Pathfinding: no path found, going idle"
<< std::endl;
state.state = State::Idle;
cc.linearVelocity = Ogre::Vector3::ZERO;
return;
}
} else {
std::cout
<< "[SO] Pathfinding: no navmesh, going direct"
<< std::endl;
state.target.path.clear();
state.target.path.push_back(objPos);
state.target.pathIndex = 0;
state.state = State::Moving;
}
return;
}
Ogre::Vector3 charPos = getEntityPosition(e);
Ogre::Vector3 objPos = getEntityPosition(
state.target.smartObject);
// --- State: Moving ---
if (state.state == State::Moving) {
if (!state.target.smartObject.is_alive() ||
state.target.path.empty()) {
std::cout
<< "[SO] Moving: target dead or path empty, idle"
<< std::endl;
state.state = State::Idle;
cc.linearVelocity = Ogre::Vector3::ZERO;
return;
}
auto &soComp =
state.target.smartObject
.get<SmartObjectComponent>();
Ogre::Vector3 charPos = getEntityPosition(e);
Ogre::Vector3 objPos =
getEntityPosition(state.target.smartObject);
// Always check range first
if (isInRange(charPos, objPos, soComp.radius,
soComp.height)) {
auto &soComp = state.target.smartObject
.get<SmartObjectComponent>();
// Always check range first
if (isInRange(charPos, objPos, soComp.radius,
soComp.height)) {
std::cout << "[SO] Moving: in range, executing"
<< std::endl;
state.state = State::Executing;
state.target.isExecuting = true;
state.target.executionTimer = 0.0f;
cc.linearVelocity = Ogre::Vector3::ZERO;
return;
}
state.target.pathRecalcTimer += deltaTime;
if (state.target.pathRecalcTimer > 2.0f) {
state.target.pathRecalcTimer = 0.0f;
if (navmeshEntity.is_alive() && m_navSystem) {
std::vector<Ogre::Vector3> newPath;
bool found = m_navSystem->findPath(
navmeshEntity, charPos, objPos,
newPath);
if (found && !newPath.empty()) {
state.target.path = newPath;
state.target.pathIndex = 0;
}
}
}
// Check if path is exhausted
if (state.target.pathIndex >=
(int)state.target.path.size()) {
float distToObj = charPos.distance(objPos);
std::cout
<< "[SO] Moving: path exhausted, dist="
<< distToObj
<< " radius*2=" << soComp.radius * 2.0f
<< std::endl;
// If close enough, teleport to range
// and execute
if (distToObj < soComp.radius * 2.0f) {
std::cout
<< "[SO] Moving: in range, executing"
<< "[SO] Moving: close enough, teleporting"
<< std::endl;
Ogre::Vector3 dirToObj =
objPos - charPos;
dirToObj.y = 0;
float xzDist = dirToObj.length();
if (xzDist > 0.01f) {
dirToObj /= xzDist;
Ogre::Vector3 newPos =
objPos -
dirToObj *
(soComp.radius *
0.5f);
newPos.y = charPos.y;
if (trans.node) {
trans.node->setPosition(
newPos);
}
trans.position = newPos;
}
state.state = State::Executing;
state.target.isExecuting = true;
state.target.executionTimer = 0.0f;
cc.linearVelocity = Ogre::Vector3::ZERO;
return;
}
// Too far - recalculate path
std::cout << "[SO] Moving: too far, recalc path"
<< std::endl;
state.target.path.clear();
state.target.path.push_back(objPos);
state.target.pathIndex = 0;
}
state.target.pathRecalcTimer += deltaTime;
if (state.target.pathRecalcTimer > 2.0f) {
state.target.pathRecalcTimer = 0.0f;
if (navmeshEntity.is_alive() &&
m_navSystem) {
std::vector<Ogre::Vector3>
newPath;
bool found =
m_navSystem->findPath(
navmeshEntity,
charPos, objPos,
newPath);
if (found && !newPath.empty()) {
state.target.path =
newPath;
state.target.pathIndex =
0;
}
}
}
Ogre::Vector3 targetPos =
state.target.path[state.target.pathIndex];
Ogre::Vector3 toTarget = targetPos - charPos;
// Check if path is exhausted
// Waypoint arrival threshold - generous since
// waypoints are just guide points
const float WAYPOINT_THRESHOLD = 1.0f;
if (toTarget.length() < WAYPOINT_THRESHOLD) {
state.target.pathIndex++;
if (state.target.pathIndex >=
(int)state.target.path.size()) {
// Path exhausted - teleport to
// range and execute if close
// enough
float distToObj =
charPos.distance(objPos);
std::cout
<< "[SO] Moving: path exhausted, dist="
<< "[SO] Moving: last waypoint reached, dist="
<< distToObj << " radius*2="
<< soComp.radius * 2.0f
<< std::endl;
// If close enough, teleport to range
// and execute
if (distToObj < soComp.radius * 2.0f) {
std::cout
<< "[SO] Moving: close enough, teleporting"
<< "[SO] Moving: close enough at waypoint, teleporting"
<< std::endl;
Ogre::Vector3 dirToObj =
objPos - charPos;
@@ -833,212 +899,134 @@ void SmartObjectSystem::update(float deltaTime)
}
// Too far - recalculate path
std::cout
<< "[SO] Moving: too far, recalc path"
<< "[SO] Moving: too far at waypoint, recalc"
<< std::endl;
state.target.path.clear();
state.target.path.push_back(objPos);
state.target.pathIndex = 0;
cc.linearVelocity = Ogre::Vector3::ZERO;
return;
}
Ogre::Vector3 targetPos =
targetPos =
state.target
.path[state.target.pathIndex];
Ogre::Vector3 toTarget = targetPos - charPos;
// Waypoint arrival threshold - generous since
// waypoints are just guide points
const float WAYPOINT_THRESHOLD = 1.0f;
if (toTarget.length() < WAYPOINT_THRESHOLD) {
state.target.pathIndex++;
if (state.target.pathIndex >=
(int)state.target.path.size()) {
// Path exhausted - teleport to
// range and execute if close
// enough
float distToObj =
charPos.distance(
objPos);
std::cout
<< "[SO] Moving: last waypoint reached, dist="
<< distToObj
<< " radius*2="
<< soComp.radius * 2.0f
<< std::endl;
if (distToObj <
soComp.radius * 2.0f) {
std::cout
<< "[SO] Moving: close enough at waypoint, teleporting"
<< std::endl;
Ogre::Vector3 dirToObj =
objPos -
charPos;
dirToObj.y = 0;
float xzDist =
dirToObj.length();
if (xzDist > 0.01f) {
dirToObj /=
xzDist;
Ogre::Vector3 newPos =
objPos -
dirToObj *
(soComp.radius *
0.5f);
newPos.y =
charPos.y;
if (trans.node) {
trans.node
->setPosition(
newPos);
}
trans.position =
newPos;
}
state.state =
State::Executing;
state.target
.isExecuting =
true;
state.target
.executionTimer =
0.0f;
cc.linearVelocity =
Ogre::Vector3::
ZERO;
return;
}
// Too far - recalculate path
std::cout
<< "[SO] Moving: too far at waypoint, recalc"
<< std::endl;
state.target.path.clear();
state.target.path.push_back(
objPos);
state.target.pathIndex = 0;
cc.linearVelocity =
Ogre::Vector3::ZERO;
return;
}
targetPos =
state.target.path
[state.target.pathIndex];
toTarget = targetPos - charPos;
}
toTarget.y = 0;
if (toTarget.squaredLength() > 0.0001f) {
toTarget.normalise();
float distToTarget =
(charPos - objPos).length();
float speed = 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 {
setLocomotionState(e, "walk");
}
cc.linearVelocity = toTarget * speed;
// Rotate character to face movement
// direction (Y plane only)
rotateTowards(e, toTarget, deltaTime);
} else {
cc.linearVelocity = Ogre::Vector3::ZERO;
}
return;
toTarget = targetPos - charPos;
}
// --- State: Executing ---
if (state.state == State::Executing) {
cc.linearVelocity = Ogre::Vector3::ZERO;
toTarget.y = 0;
if (toTarget.squaredLength() > 0.0001f) {
toTarget.normalise();
// When the behavior tree is running (via ActionDebug),
// do NOT override animation states - the behavior tree's
// setAnimationState nodes should be the sole controller.
if (!debug.isRunning ||
debug.currentActionName.empty()) {
setLocomotionState(e, "idle");
float distToTarget =
(charPos - objPos).length();
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 {
setLocomotionState(e, "walk");
}
if (state.target.isExecuting && db) {
const GoapAction *action =
db->findAction(
state.target.actionName);
if (action) {
// Kick off the action's
// behavior tree via
// BehaviorTreeSystem's
// ActionDebug path.
// Only reset runTimer on the
// first frame of execution
// (when isRunning is false) so
// BehaviorTreeSystem re-initializes
// the runner state for a fresh
// execution. Do NOT reset every
// frame or isNewlyActive will
// fire repeatedly.
if (!debug.isRunning) {
debug.runTimer = 0.0f;
}
debug.isRunning = true;
debug.currentActionName =
state.target.actionName;
// When root motion is enabled, the
// AnimationTreeSystem already computes
// linearVelocity from animation root
// bone displacement. Do NOT overwrite
// it with direct movement velocity.
if (!hasRootMotion(e))
cc.linearVelocity = toTarget * speed;
// Check if the behavior tree
// has finished (sequence
// completed or failed).
// Only check completion when
// runTimer > 0 (at least one
// frame has passed since
// starting) to avoid
// immediately detecting
// completion on the same frame
// the tree was started.
auto &btState =
m_btSystem
->getActionDebugState(
e.id());
if (debug.runTimer > 0.0f &&
btState.treeResult !=
BehaviorTreeSystem::
Status::running) {
// Behavior tree
// completed - stop
// evaluation
debug.isRunning = false;
debug.currentActionName
.clear();
// Rotate character to face movement
// direction (Y plane only)
rotateTowards(e, toTarget, deltaTime);
} else if (!hasRootMotion(e)) {
cc.linearVelocity = Ogre::Vector3::ZERO;
}
return;
}
bb.apply(
action->effects);
state.state =
State::Idle;
state.target
.isExecuting =
false;
debug.lastResult =
"Smart object action '" +
state.target
.actionName +
"' completed";
}
} else {
// --- State: Executing ---
if (state.state == State::Executing) {
cc.linearVelocity = Ogre::Vector3::ZERO;
// When the behavior tree is running (via ActionDebug),
// do NOT override animation states - the behavior tree's
// setAnimationState nodes should be the sole controller.
if (!debug.isRunning ||
debug.currentActionName.empty()) {
setLocomotionState(e, "idle");
}
if (state.target.isExecuting && db) {
const GoapAction *action =
db->findAction(state.target.actionName);
if (action) {
// Kick off the action's
// behavior tree via
// BehaviorTreeSystem's
// ActionDebug path.
// Only reset runTimer on the
// first frame of execution
// (when isRunning is false) so
// BehaviorTreeSystem re-initializes
// the runner state for a fresh
// execution. Do NOT reset every
// frame or isNewlyActive will
// fire repeatedly.
if (!debug.isRunning) {
debug.runTimer = 0.0f;
}
debug.isRunning = true;
debug.currentActionName =
state.target.actionName;
// Check if the behavior tree
// has finished (sequence
// completed or failed).
// Only check completion when
// runTimer > 0 (at least one
// frame has passed since
// starting) to avoid
// immediately detecting
// completion on the same frame
// the tree was started.
auto &btState =
m_btSystem->getActionDebugState(
e.id());
if (debug.runTimer > 0.0f &&
btState.treeResult !=
BehaviorTreeSystem::Status::
running) {
// Behavior tree
// completed - stop
// evaluation
debug.isRunning = false;
debug.currentActionName.clear();
bb.apply(action->effects);
state.state = State::Idle;
state.target.isExecuting =
false;
debug.lastResult =
"Smart object action '" +
state.target.actionName +
"' completed";
}
} else {
state.state = State::Idle;
state.target.isExecuting = false;
}
} else {
state.state = State::Idle;
}
});
}
});
}