diff --git a/src/features/editScene/systems/SmartObjectSystem.cpp b/src/features/editScene/systems/SmartObjectSystem.cpp index 292268a..253e09e 100644 --- a/src/features/editScene/systems/SmartObjectSystem.cpp +++ b/src/features/editScene/systems/SmartObjectSystem.cpp @@ -20,6 +20,20 @@ #include #include +/** + * 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()) + return false; + return e.get().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()) { auto &pf = e.get(); - 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()) { - auto &pf = - e.get(); + 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() - .each([&](flecs::entity e, CharacterComponent &cc, - TransformComponent &trans, ActionDebug &debug) { - (void)trans; - (void)debug; + m_world.query().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(); + 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(); - 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(); + 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(); + + // 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 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 - 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()) { - auto &pf = e.get(); - 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()) { + auto &pf = + e.get(); + 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; } - }); + } + }); }