From c773ed9f9ad71c144cafbee40677a04bc1e3305c Mon Sep 17 00:00:00 2001 From: mrcheko Date: Sat, 4 Jul 2015 18:00:16 +0300 Subject: [PATCH 01/11] move checkWayIsClear to pathfinding; move shortcut logic to separate func (AiPackage::shortcutPath); rework AiPackage::pathTo --- apps/openmw/mwmechanics/aiactivate.cpp | 11 +- apps/openmw/mwmechanics/aicombat.cpp | 42 +------ apps/openmw/mwmechanics/aifollow.cpp | 14 +-- apps/openmw/mwmechanics/aipackage.cpp | 153 ++++++++++++++++++++++-- apps/openmw/mwmechanics/aipackage.hpp | 15 ++- apps/openmw/mwmechanics/aipursue.cpp | 8 +- apps/openmw/mwmechanics/aiwander.cpp | 4 +- apps/openmw/mwmechanics/pathfinding.cpp | 78 +++++++++--- apps/openmw/mwmechanics/pathfinding.hpp | 26 +++- 9 files changed, 248 insertions(+), 103 deletions(-) diff --git a/apps/openmw/mwmechanics/aiactivate.cpp b/apps/openmw/mwmechanics/aiactivate.cpp index 9e25084d3..aa937022d 100644 --- a/apps/openmw/mwmechanics/aiactivate.cpp +++ b/apps/openmw/mwmechanics/aiactivate.cpp @@ -34,20 +34,17 @@ bool MWMechanics::AiActivate::execute (const MWWorld::Ptr& actor, AiState& state ) return true; //Target doesn't exist - //Set the target desition from the actor + //Set the target destination for the actor ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; - if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < 200) { //Stop when you get close - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; + if (pathTo(actor, dest, duration, 200)) //Go to the destination + { + // activate when reached MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getPtr(mObjectId,false); - MWBase::Environment::get().getWorld()->activate(target, actor); return true; } - else { - pathTo(actor, dest, duration); //Go to the destination - } return false; } diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 8f43c6280..fb47c1109 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -37,46 +37,6 @@ namespace Ogre::Vector3 AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const Ogre::Vector3& vLastTargetPos, float duration, int weapType, float strength); - - float getZAngleToDir(const Ogre::Vector3& dir) - { - return Ogre::Math::ATan2(dir.x,dir.y).valueDegrees(); - } - - float getXAngleToDir(const Ogre::Vector3& dir, float dirLen = 0.0f) - { - float len = (dirLen > 0.0f)? dirLen : dir.length(); - return -Ogre::Math::ASin(dir.z / len).valueDegrees(); - } - - - const float PATHFIND_Z_REACH = 50.0f; - // distance at which actor pays more attention to decide whether to shortcut or stick to pathgrid - const float PATHFIND_CAUTION_DIST = 500.0f; - // distance after which actor (failed previously to shortcut) will try again - const float PATHFIND_SHORTCUT_RETRY_DIST = 300.0f; - - // cast up-down ray with some offset from actor position to check for pits/obstacles on the way to target; - // magnitude of pits/obstacles is defined by PATHFIND_Z_REACH - bool checkWayIsClear(const Ogre::Vector3& from, const Ogre::Vector3& to, float offsetXY) - { - if((to - from).length() >= PATHFIND_CAUTION_DIST || std::abs(from.z - to.z) <= PATHFIND_Z_REACH) - { - Ogre::Vector3 dir = to - from; - dir.z = 0; - dir.normalise(); - float verticalOffset = 200; // instead of '200' here we want the height of the actor - Ogre::Vector3 _from = from + dir*offsetXY + Ogre::Vector3::UNIT_Z * verticalOffset; - - // cast up-down ray and find height in world space of hit - float h = _from.z - MWBase::Environment::get().getWorld()->getDistToNearestRayHit(_from, -Ogre::Vector3::UNIT_Z, verticalOffset + PATHFIND_Z_REACH + 1); - - if(std::abs(from.z - h) <= PATHFIND_Z_REACH) - return true; - } - - return false; - } } namespace MWMechanics @@ -702,7 +662,7 @@ namespace MWMechanics if (doesPathNeedRecalc(newPathTarget, actor.getCell()->getCell())) { ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(actor.getRefData().getPosition())); - mPathFinder.buildSyncedPath(start, newPathTarget, actor.getCell(), false); + mPathFinder.buildSyncedPath(start, newPathTarget, actor.getCell()); } } diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index ddfc14581..55414bfff 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -130,19 +130,7 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, AiState& state, float duratio //Set the target destination from the actor ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; - if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < followDistance) //Stop when you get close - { - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - - // turn towards target anyway - float directionX = target.getRefData().getPosition().pos[0] - actor.getRefData().getPosition().pos[0]; - float directionY = target.getRefData().getPosition().pos[1] - actor.getRefData().getPosition().pos[1]; - zTurn(actor, Ogre::Math::ATan2(directionX,directionY), Ogre::Degree(5)); - } - else - { - pathTo(actor, dest, duration); //Go to the destination - } + pathTo(actor, dest, duration, followDistance); //Go to the destination //Check if you're far away if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) > 450) diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 216bf7b09..425593624 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -16,12 +16,14 @@ MWMechanics::AiPackage::~AiPackage() {} -MWMechanics::AiPackage::AiPackage() : mTimer(0.26f), mStuckTimer(0) { //mTimer starts at .26 to force initial pathbuild - +MWMechanics::AiPackage::AiPackage() : + mTimer(0.26f), mStuckTimer(0), //mTimer starts at .26 to force initial pathbuild + mShortcutProhibited(false), mShortcutFailPos() +{ } -bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Point dest, float duration) +bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest, float duration, float destTolerance) { //Update various Timers mTimer += duration; //Update timer @@ -63,13 +65,51 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Po //*********************** /// Checks if you can't get to the end position at all, adds end position to end of path - /// Rebuilds path every quarter of a second, in case the target has moved + /// Rebuilds path every [AI_REACTION_TIME] seconds, in case the target has moved //*********************** - if(mTimer > 0.25) + + bool isStuck = distance(start, mStuckPos.pos[0], mStuckPos.pos[1], mStuckPos.pos[2]) < actor.getClass().getSpeed(actor)*0.05 + && distance(dest, start) > 20; + + float distToNextWaypoint = distance(start, dest); + + bool isDestReached = (distToNextWaypoint <= destTolerance); + + if (!isDestReached && mTimer > AI_REACTION_TIME) { - if (doesPathNeedRecalc(dest, cell)) { //Only rebuild path if it's moved - mPathFinder.buildSyncedPath(start, dest, actor.getCell(), true); //Rebuild path, in case the target has moved - mPrevDest = dest; + bool needPathRecalc = doesPathNeedRecalc(dest, cell); + + bool isWayClear = true; + + if (!needPathRecalc) // TODO: add check if actor is actually shortcutting + { + isWayClear = checkWayIsClearForActor(start, dest, actor); // check if current shortcut is safe to follow + } + + if (!isWayClear || needPathRecalc) // Only rebuild path if the target has moved or can't follow current shortcut + { + bool destInLOS = false; + + if (isStuck || !isWayClear || !shortcutPath(start, dest, actor, &destInLOS)) + { + mPathFinder.buildSyncedPath(start, dest, actor.getCell()); + + // give priority to go directly on target if there is minimal opportunity + if (destInLOS && mPathFinder.getPath().size() > 1) + { + // get point just before dest + std::list::const_iterator pPointBeforeDest = mPathFinder.getPath().end(); + --pPointBeforeDest; + --pPointBeforeDest; + + // if start point is closer to the target then last point of path (excluding target itself) then go straight on the target + if (distance(start, dest) <= distance(dest, *pPointBeforeDest)) + { + mPathFinder.clearPath(); + mPathFinder.addPointToPath(dest); + } + } + } } if(!mPathFinder.getPath().empty()) //Path has points in it @@ -86,13 +126,22 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Po //************************ /// Checks if you aren't moving; attempts to unstick you //************************ - if(mPathFinder.checkPathCompleted(pos.pos[0],pos.pos[1])) //Path finished? + if (isDestReached || mPathFinder.checkPathCompleted(pos.pos[0],pos.pos[1])) //Path finished? + { + actor.getClass().getMovementSettings(actor).mPosition[0] = 0; // stop moving + actor.getClass().getMovementSettings(actor).mPosition[1] = 0; + actor.getClass().getMovementSettings(actor).mPosition[2] = 0; + + // turn to destination point + zTurn(actor, Ogre::Degree(getZAngleToPoint(start, dest))); + smoothTurn(actor, Ogre::Degree(getXAngleToPoint(start, dest)), 0); return true; + } else if(mStuckTimer>0.5) //Every half second see if we need to take action to avoid something { /// TODO (tluppi#1#): Use ObstacleCheck here. Not working for some reason //if(mObstacleCheck.check(actor, duration)) { - if(distance(start, mStuckPos.pos[0], mStuckPos.pos[1], mStuckPos.pos[2]) < actor.getClass().getSpeed(actor)*0.05 && distance(dest, start) > 20) { //Actually stuck, and far enough away from destination to care + if (isStuck) { //Actually stuck, and far enough away from destination to care // first check if we're walking into a door MWWorld::Ptr door = getNearbyDoor(actor); if(door != MWWorld::Ptr()) // NOTE: checks interior cells only @@ -119,12 +168,94 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Po actor.getClass().getMovementSettings(actor).mPosition[1] = 1; //Just run forward the rest of the time } + // turn to next path point by X,Z axes zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]))); + smoothTurn(actor, Ogre::Degree(mPathFinder.getXAngleToNext(pos.pos[0], pos.pos[1], pos.pos[2])), 0); return false; } +bool MWMechanics::AiPackage::shortcutPath(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor, bool *destInLOS) +{ + const MWWorld::Class& actorClass = actor.getClass(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + + // check if actor can move along z-axis + bool actorCanMoveByZ = (actorClass.canSwim(actor) && MWBase::Environment::get().getWorld()->isSwimming(actor)) + || world->isFlying(actor); + + // don't use pathgrid when actor can move in 3 dimensions + bool isPathClear = actorCanMoveByZ; + + if (!isPathClear + && (!mShortcutProhibited || (PathFinder::MakeOgreVector3(mShortcutFailPos) - PathFinder::MakeOgreVector3(startPoint)).length() >= PATHFIND_SHORTCUT_RETRY_DIST)) + { + // take the direct path only if there aren't any obstacles + isPathClear = !MWBase::Environment::get().getWorld()->castRay( + static_cast(startPoint.mX), static_cast(startPoint.mY), static_cast(startPoint.mZ), + static_cast(endPoint.mX), static_cast(endPoint.mY), static_cast(endPoint.mZ)); + + if (destInLOS != NULL) *destInLOS = isPathClear; + + if (!isPathClear) + return false; + + // check if an actor can move along the shortcut path + isPathClear = checkWayIsClearForActor(startPoint, endPoint, actor); + } + + if (isPathClear) // can shortcut the path + { + mPathFinder.clearPath(); + mPathFinder.addPointToPath(endPoint); + return true; + } + + return false; +} + +bool MWMechanics::AiPackage::checkWayIsClearForActor(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor) +{ + bool actorCanMoveByZ = (actor.getClass().canSwim(actor) && MWBase::Environment::get().getWorld()->isSwimming(actor)) + || MWBase::Environment::get().getWorld()->isFlying(actor); + + if (actorCanMoveByZ) + return true; + + float actorSpeed = actor.getClass().getSpeed(actor); + float maxAvoidDist = AI_REACTION_TIME * actorSpeed + actorSpeed / MAX_VEL_ANGULAR.valueRadians() * 2; // *2 - for reliability + Ogre::Real distToTarget = Ogre::Vector3(static_cast(endPoint.mX), static_cast(endPoint.mY), 0).length(); + + float offsetXY = distToTarget > maxAvoidDist*1.5? maxAvoidDist : maxAvoidDist/2; + + bool isClear = checkWayIsClear(PathFinder::MakeOgreVector3(startPoint), PathFinder::MakeOgreVector3(endPoint), offsetXY); + + // update shortcut prohibit state + if (isClear) + { + if (mShortcutProhibited) + { + mShortcutProhibited = false; + mShortcutFailPos = ESM::Pathgrid::Point(); + } + } + if (!isClear) + { + if (mShortcutFailPos.mX == 0 && mShortcutFailPos.mY == 0 && mShortcutFailPos.mZ == 0) + { + mShortcutProhibited = true; + mShortcutFailPos = startPoint; + } + } + + return isClear; +} + bool MWMechanics::AiPackage::doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell) { - return distance(mPrevDest, dest) > 10; + bool needRecalc = distance(mPrevDest, dest) > 10; + if (needRecalc) + mPrevDest = dest; + + return needRecalc; } \ No newline at end of file diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 78a2bfd9f..a22ac9357 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -24,6 +24,7 @@ namespace ESM namespace MWMechanics { + const float AI_REACTION_TIME = 0.25f; /// \brief Base class for AI packages class AiPackage @@ -70,7 +71,16 @@ namespace MWMechanics protected: /// Causes the actor to attempt to walk to the specified location /** \return If the actor has arrived at his destination **/ - bool pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Point dest, float duration); + bool pathTo(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest, float duration, float destTolerance = 0.0f); + + /// Check if there aren't any obstacles along the path to make shortcut possible + /// If a shortcut is possible then path will be cleared and filled with the destination point. + /// \param destInLOS If not NULL function will return ray cast check result + /// \return If can shortcut the path + bool shortcutPath(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor, bool *destInLOS); + + /// Check if the way to the destination is clear, taking into account actor speed + bool checkWayIsClearForActor(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor); virtual bool doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell); @@ -83,6 +93,9 @@ namespace MWMechanics ESM::Position mStuckPos; ESM::Pathgrid::Point mPrevDest; + + bool mShortcutProhibited; // shortcutting may be prohibited after unsuccessful attempt + ESM::Pathgrid::Point mShortcutFailPos; // position of last shortcut fail }; } diff --git a/apps/openmw/mwmechanics/aipursue.cpp b/apps/openmw/mwmechanics/aipursue.cpp index 8c31a10db..af007231b 100644 --- a/apps/openmw/mwmechanics/aipursue.cpp +++ b/apps/openmw/mwmechanics/aipursue.cpp @@ -55,14 +55,10 @@ bool AiPursue::execute (const MWWorld::Ptr& actor, AiState& state, float duratio //Set the target desition from the actor ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; - if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < 100) { //Stop when you get close - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - target.getClass().activate(target,actor).get()->execute(actor); //Arrest player + if (pathTo(actor, dest, duration, 100)) { + target.getClass().activate(target,actor).get()->execute(actor); //Arrest player when reached return true; } - else { - pathTo(actor, dest, duration); //Go to the destination - } actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, true); //Make NPC run diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 1f4133c0a..4566740e3 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -413,7 +413,7 @@ namespace MWMechanics ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos)); // don't take shortcuts for wandering - storage.mPathFinder.buildPath(start, dest, actor.getCell(), false); + storage.mPathFinder.buildPath(start, dest, actor.getCell()); if(storage.mPathFinder.isPathConstructed()) { @@ -520,7 +520,7 @@ namespace MWMechanics ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos)); // don't take shortcuts for wandering - storage.mPathFinder.buildPath(start, dest, actor.getCell(), false); + storage.mPathFinder.buildPath(start, dest, actor.getCell()); if(storage.mPathFinder.isPathConstructed()) { diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index fea993e23..c32899dde 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -113,6 +113,49 @@ namespace MWMechanics return sqrt(x * x + y * y + z * z); } + float getZAngleToDir(const Ogre::Vector3& dir) + { + return Ogre::Math::ATan2(dir.x,dir.y).valueDegrees(); + } + + float getXAngleToDir(const Ogre::Vector3& dir, float dirLen) + { + float len = (dirLen > 0.0f)? dirLen : dir.length(); + return -Ogre::Math::ASin(dir.z / len).valueDegrees(); + } + + float getZAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest) + { + Ogre::Vector3 dir = PathFinder::MakeOgreVector3(dest) - PathFinder::MakeOgreVector3(origin); + return getZAngleToDir(dir); + } + + float getXAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest) + { + Ogre::Vector3 dir = PathFinder::MakeOgreVector3(dest) - PathFinder::MakeOgreVector3(origin); + return getXAngleToDir(dir); + } + + bool checkWayIsClear(const Ogre::Vector3& from, const Ogre::Vector3& to, float offsetXY) + { + if((to - from).length() >= PATHFIND_CAUTION_DIST || std::abs(from.z - to.z) <= PATHFIND_Z_REACH) + { + Ogre::Vector3 dir = to - from; + dir.z = 0; + dir.normalise(); + float verticalOffset = 200; // instead of '200' here we want the height of the actor + Ogre::Vector3 _from = from + dir*offsetXY + Ogre::Vector3::UNIT_Z * verticalOffset; + + // cast up-down ray and find height in world space of hit + float h = _from.z - MWBase::Environment::get().getWorld()->getDistToNearestRayHit(_from, -Ogre::Vector3::UNIT_Z, verticalOffset + PATHFIND_Z_REACH + 1); + + if(std::abs(from.z - h) <= PATHFIND_Z_REACH) + return true; + } + + return false; + } + PathFinder::PathFinder() : mPathgrid(NULL), mCell(NULL) @@ -165,23 +208,10 @@ namespace MWMechanics */ void PathFinder::buildPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint, - const MWWorld::CellStore* cell, - bool allowShortcuts) + const MWWorld::CellStore* cell) { mPath.clear(); - if(allowShortcuts) - { - // if there's a ray cast hit, can't take a direct path - if (!MWBase::Environment::get().getWorld()->castRay( - static_cast(startPoint.mX), static_cast(startPoint.mY), static_cast(startPoint.mZ), - static_cast(endPoint.mX), static_cast(endPoint.mY), static_cast(endPoint.mZ))) - { - mPath.push_back(endPoint); - return; - } - } - if(mCell != cell || !mPathgrid) { mCell = cell; @@ -270,6 +300,19 @@ namespace MWMechanics return Ogre::Math::ATan2(directionX,directionY).valueDegrees(); } + float PathFinder::getXAngleToNext(float x, float y, float z) const + { + // This should never happen (programmers should have an if statement checking + // isPathConstructed that prevents this call if otherwise). + if(mPath.empty()) + return 0.; + + const ESM::Pathgrid::Point &nextPoint = *mPath.begin(); + Ogre::Vector3 dir = MakeOgreVector3(nextPoint) - Ogre::Vector3(x,y,z); + + return -Ogre::Math::ASin(dir.z / dir.length()).valueDegrees(); + } + bool PathFinder::checkPathCompleted(float x, float y, float tolerance) { if(mPath.empty()) @@ -291,19 +334,18 @@ namespace MWMechanics // see header for the rationale void PathFinder::buildSyncedPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint, - const MWWorld::CellStore* cell, - bool allowShortcuts) + const MWWorld::CellStore* cell) { if (mPath.size() < 2) { // if path has one point, then it's the destination. // don't need to worry about bad path for this case - buildPath(startPoint, endPoint, cell, allowShortcuts); + buildPath(startPoint, endPoint, cell); } else { const ESM::Pathgrid::Point oldStart(*getPath().begin()); - buildPath(startPoint, endPoint, cell, allowShortcuts); + buildPath(startPoint, endPoint, cell); if (mPath.size() >= 2) { // if 2nd waypoint of new path == 1st waypoint of old, diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 644d79236..c978c9afa 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -15,8 +15,24 @@ namespace MWWorld namespace MWMechanics { - float distance(ESM::Pathgrid::Point point, float x, float y, float); + float distance(ESM::Pathgrid::Point point, float x, float y, float z); float distance(ESM::Pathgrid::Point a, ESM::Pathgrid::Point b); + float getZAngleToDir(const Ogre::Vector3& dir); + float getXAngleToDir(const Ogre::Vector3& dir, float dirLen = 0.0f); + float getZAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest); + float getXAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest); + + const float PATHFIND_Z_REACH = 50.0f; + //static const float sMaxSlope = 49.0f; // duplicate as in physicssystem + // distance at which actor pays more attention to decide whether to shortcut or stick to pathgrid + const float PATHFIND_CAUTION_DIST = 500.0f; + // distance after which actor (failed previously to shortcut) will try again + const float PATHFIND_SHORTCUT_RETRY_DIST = 300.0f; + + // cast up-down ray with some offset from actor position to check for pits/obstacles on the way to target; + // magnitude of pits/obstacles is defined by PATHFIND_Z_REACH + bool checkWayIsClear(const Ogre::Vector3& from, const Ogre::Vector3& to, float offsetXY); + class PathFinder { public: @@ -39,13 +55,15 @@ namespace MWMechanics void clearPath(); void buildPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint, - const MWWorld::CellStore* cell, bool allowShortcuts = true); + const MWWorld::CellStore* cell); bool checkPathCompleted(float x, float y, float tolerance=32.f); ///< \Returns true if we are within \a tolerance units of the last path point. float getZAngleToNext(float x, float y) const; + float getXAngleToNext(float x, float y, float z) const; + bool isPathConstructed() const { return !mPath.empty(); @@ -69,9 +87,9 @@ namespace MWMechanics Which results in NPC "running in a circle" back to the just passed waypoint. */ void buildSyncedPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint, - const MWWorld::CellStore* cell, bool allowShortcuts = true); + const MWWorld::CellStore* cell); - void addPointToPath(ESM::Pathgrid::Point &point) + void addPointToPath(const ESM::Pathgrid::Point &point) { mPath.push_back(point); } From b960b0af9368643d30edc1394179efbbd65c4fc8 Mon Sep 17 00:00:00 2001 From: mrcheko Date: Fri, 1 Jan 2016 16:41:45 +0300 Subject: [PATCH 02/11] rewrite pathTo for clearer logic; reapply Scrawl's aifollow threshold --- apps/openmw/mwmechanics/aifollow.cpp | 24 ++++---- apps/openmw/mwmechanics/aipackage.cpp | 88 ++++++++++----------------- apps/openmw/mwmechanics/aipackage.hpp | 3 +- 3 files changed, 45 insertions(+), 70 deletions(-) diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index f0e67af08..342ee9714 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -127,21 +127,21 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte //Set the target destination from the actor ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; - float dist = distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]); - //const float threshold = 10; - - //if (storage.mMoving) //Stop when you get close - // storage.mMoving = (dist > followDistance); - //else - // storage.mMoving = (dist > followDistance + threshold); + const float threshold = 10; // to avoid constant switching between moving/stopping + if (!storage.mMoving) followDistance += threshold; storage.mMoving = !pathTo(actor, dest, duration, followDistance); // Go to the destination - //Check if you're far away - if(dist > 450) - actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, true); //Make NPC run - else if(dist < 325) //Have a bit of a dead zone, otherwise npc will constantly flip between running and not when right on the edge of the running threshhold - actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, false); //make NPC walk + if (storage.mMoving) + { + //Check if you're far away + float dist = distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]); + + if (dist > 450) + actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, true); //Make NPC run + else if (dist < 325) //Have a bit of a dead zone, otherwise npc will constantly flip between running and not when right on the edge of the running threshhold + actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, false); //make NPC walk + } return false; } diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 571bf0ac8..2999e8a19 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -22,6 +22,7 @@ MWMechanics::AiPackage::~AiPackage() {} MWMechanics::AiPackage::AiPackage() : mTimer(AI_REACTION_TIME + 1.0f), // to force initial pathbuild + mIsShortcutting(false), mShortcutProhibited(false), mShortcutFailPos() { } @@ -41,10 +42,8 @@ bool MWMechanics::AiPackage::followTargetThroughDoors() const return false; } - bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest, float duration, float destTolerance) { - //Update various Timers mTimer += duration; //Update timer ESM::Position pos = actor.getRefData().getPosition(); //position of the actor @@ -59,39 +58,21 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgr return false; } - //*********************** - /// Checks if you can't get to the end position at all, adds end position to end of path - /// Rebuilds path every [AI_REACTION_TIME] seconds, in case the target has moved - //*********************** - + // handle path building and shortcutting ESM::Pathgrid::Point start = pos.pos; - float distToNextWaypoint = distance(start, dest); - bool isDestReached = (distToNextWaypoint <= destTolerance); + float distToTarget = distance(start, dest); + bool isDestReached = (distToTarget <= destTolerance); if (!isDestReached && mTimer > AI_REACTION_TIME) { - osg::Vec3f& lastActorPos = mLastActorPos; - bool isStuck = distance(start, lastActorPos.x(), lastActorPos.y(), lastActorPos.z()) < actor.getClass().getSpeed(actor)*mTimer - && distance(dest, start) > 20; - - lastActorPos = pos.asVec3(); + bool wasShortcutting = mIsShortcutting; + bool destInLOS = false; + mIsShortcutting = shortcutPath(start, dest, actor, &destInLOS); // try to shortcut first - const ESM::Cell *cell = actor.getCell()->getCell(); - bool needPathRecalc = doesPathNeedRecalc(dest, cell); //Only rebuild path if dest point has changed - - bool isWayClear = true; - - if (!needPathRecalc) // TODO: add check if actor is actually shortcutting + if (!mIsShortcutting) { - isWayClear = checkWayIsClearForActor(start, dest, actor); // check if current shortcut is safe to follow - } - - if (!isWayClear || needPathRecalc) // Only rebuild path if the target has moved or can't follow current shortcut - { - bool destInLOS = false; - - if (isStuck || !isWayClear || !shortcutPath(start, dest, actor, &destInLOS)) + if (wasShortcutting || doesPathNeedRecalc(dest, actor.getCell()->getCell())) // only rebuild path if the target has moved { mPathFinder.buildSyncedPath(start, dest, actor.getCell()); @@ -111,28 +92,21 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgr } } } - } - if(!mPathFinder.getPath().empty()) //Path has points in it - { - ESM::Pathgrid::Point lastPos = mPathFinder.getPath().back(); //Get the end of the proposed path + if(!mPathFinder.getPath().empty()) //Path has points in it + { + ESM::Pathgrid::Point lastPos = mPathFinder.getPath().back(); //Get the end of the proposed path - if(distance(dest, lastPos) > 100) //End of the path is far from the destination - mPathFinder.addPointToPath(dest); //Adds the final destination to the path, to try to get to where you want to go + if(distance(dest, lastPos) > 100) //End of the path is far from the destination + mPathFinder.addPointToPath(dest); //Adds the final destination to the path, to try to get to where you want to go + } } mTimer = 0; } - //************************ - /// Checks if you aren't moving; attempts to unstick you - //************************ if (isDestReached || mPathFinder.checkPathCompleted(pos.pos[0],pos.pos[1])) //Path finished? { - actor.getClass().getMovementSettings(actor).mPosition[0] = 0; // stop moving - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - actor.getClass().getMovementSettings(actor).mPosition[2] = 0; - // turn to destination point zTurn(actor, getZAngleToPoint(start, dest)); smoothTurn(actor, getXAngleToPoint(start, dest), 0); @@ -140,6 +114,8 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgr } else { + actor.getClass().getMovementSettings(actor).mPosition[1] = 1; // run to target + // handle obstacles on the way evadeObstacles(actor, duration, pos); } @@ -155,24 +131,22 @@ void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor, float dur zTurn(actor, mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1])); MWMechanics::Movement& movement = actor.getClass().getMovementSettings(actor); - if (mObstacleCheck.check(actor, duration)) + + // check if stuck due to obstacles + if (!mObstacleCheck.check(actor, duration)) return; + + // first check if obstacle is a door + MWWorld::Ptr door = getNearbyDoor(actor); // NOTE: checks interior cells only + if (door != MWWorld::Ptr()) { - // first check if we're walking into a door - MWWorld::Ptr door = getNearbyDoor(actor); - if (door != MWWorld::Ptr()) // NOTE: checks interior cells only - { - if (!door.getCellRef().getTeleport() && door.getCellRef().getTrap().empty() - && door.getCellRef().getLockLevel() <= 0 && door.getClass().getDoorState(door) == 0) { - MWBase::Environment::get().getWorld()->activateDoor(door, 1); - } - } - else // probably walking into another NPC - { - mObstacleCheck.takeEvasiveAction(movement); + if (!door.getCellRef().getTeleport() && door.getCellRef().getTrap().empty() + && door.getCellRef().getLockLevel() <= 0 && door.getClass().getDoorState(door) == 0) { + MWBase::Environment::get().getWorld()->activateDoor(door, 1); } } - else { //Not stuck, so reset things - movement.mPosition[1] = 1; //Just run forward + else // any other obstacle (NPC, crate, etc.) + { + mObstacleCheck.takeEvasiveAction(movement); } } @@ -191,7 +165,7 @@ bool MWMechanics::AiPackage::shortcutPath(const ESM::Pathgrid::Point& startPoint if (!isPathClear && (!mShortcutProhibited || (PathFinder::MakeOsgVec3(mShortcutFailPos) - PathFinder::MakeOsgVec3(startPoint)).length() >= PATHFIND_SHORTCUT_RETRY_DIST)) { - // take the direct path only if there aren't any obstacles + // check if target is clearly visible isPathClear = !MWBase::Environment::get().getWorld()->castRay( static_cast(startPoint.mX), static_cast(startPoint.mY), static_cast(startPoint.mZ), static_cast(endPoint.mX), static_cast(endPoint.mY), static_cast(endPoint.mZ)); diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 67c2b0c1b..79537e616 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -82,7 +82,7 @@ namespace MWMechanics bool isTargetMagicallyHidden(const MWWorld::Ptr& target); protected: - /// Causes the actor to attempt to walk to the specified location + /// Handles path building and shortcutting with obstacles avoiding /** \return If the actor has arrived at his destination **/ bool pathTo(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest, float duration, float destTolerance = 0.0f); @@ -108,6 +108,7 @@ namespace MWMechanics ESM::Pathgrid::Point mPrevDest; osg::Vec3f mLastActorPos; + bool mIsShortcutting; // if shortcutting at the moment bool mShortcutProhibited; // shortcutting may be prohibited after unsuccessful attempt ESM::Pathgrid::Point mShortcutFailPos; // position of last shortcut fail From bcb1f4ed056b6207c2e038247b3ef002c784b52d Mon Sep 17 00:00:00 2001 From: mrcheko Date: Sun, 3 Jan 2016 15:33:52 +0300 Subject: [PATCH 03/11] refactor AiCombat: remove all pathfinding code, adopt new version of AiPackage::pathTo; fix couple of warnings; --- apps/openmw/mwmechanics/aiactivate.cpp | 1 - apps/openmw/mwmechanics/aicombat.cpp | 255 +++++-------------------- apps/openmw/mwmechanics/aicombat.hpp | 9 +- apps/openmw/mwmechanics/aipackage.cpp | 10 +- apps/openmw/mwmechanics/aipackage.hpp | 3 +- apps/openmw/mwmechanics/aipursue.cpp | 1 - 6 files changed, 51 insertions(+), 228 deletions(-) diff --git a/apps/openmw/mwmechanics/aiactivate.cpp b/apps/openmw/mwmechanics/aiactivate.cpp index 348ddd692..b3c011d50 100644 --- a/apps/openmw/mwmechanics/aiactivate.cpp +++ b/apps/openmw/mwmechanics/aiactivate.cpp @@ -22,7 +22,6 @@ MWMechanics::AiActivate *MWMechanics::AiActivate::clone() const } bool MWMechanics::AiActivate::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { - ESM::Position pos = actor.getRefData().getPosition(); //position of the actor const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mObjectId, false); //The target to follow actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 6251cff8d..abf4109ba 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -41,7 +41,7 @@ namespace MWMechanics float mTimerCombatMove; bool mReadyToAttack; bool mAttack; - bool mFollowTarget; + float mAttackRange; bool mCombatMove; osg::Vec3f mLastTargetPos; const MWWorld::CellStore* mCell; @@ -50,8 +50,8 @@ namespace MWMechanics float mStrength; bool mForceNoShortcut; ESM::Position mShortcutFailPos; - osg::Vec3f mLastActorPos; MWMechanics::Movement mMovement; + bool mAdjustAiming; AiCombatStorage(): mAttackCooldown(0), @@ -59,7 +59,7 @@ namespace MWMechanics mTimerCombatMove(0), mReadyToAttack(false), mAttack(false), - mFollowTarget(false), + mAttackRange(200), // default attack range (same as in Creature::Hit) mCombatMove(false), mLastTargetPos(0,0,0), mCell(NULL), @@ -67,8 +67,9 @@ namespace MWMechanics mActionCooldown(0), mStrength(), mForceNoShortcut(false), - mLastActorPos(0,0,0), - mMovement(){} + mMovement(), + mAdjustAiming(false) + {} void startCombatMove(bool isNpc, bool isDistantCombat, float distToTarget, float rangeAttack); void updateCombatMove(float duration); @@ -139,6 +140,7 @@ namespace MWMechanics * Use the Observer Pattern to co-ordinate attacks, provide intelligence on * whether the target was hit, etc. */ + bool AiCombat::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { // get or create temporary storage @@ -158,33 +160,33 @@ namespace MWMechanics return true; //Update every frame + storage.mReadyToAttack = pathTo(actor, target.getRefData().getPosition().pos, duration, storage.mAttackRange); + storage.updateCombatMove(duration); - updateActorsMovement(actor, duration, storage.mMovement); + if (storage.mReadyToAttack) updateActorsMovement(actor, duration, storage); storage.updateAttack(characterController); storage.mActionCooldown -= duration; - + float& timerReact = storage.mTimerReact; - if(timerReact < AI_REACTION_TIME) + if (timerReact < AI_REACTION_TIME) { timerReact += duration; - return false; } else { timerReact = 0; - return reactionTimeActions(actor, characterController, storage, target); + attack(actor, target, storage, characterController); } + + return false; } - bool AiCombat::reactionTimeActions(const MWWorld::Ptr& actor, CharacterController& characterController, - AiCombatStorage& storage, MWWorld::Ptr target) + void AiCombat::attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController) { - MWMechanics::Movement& movement = storage.mMovement; - if (isTargetMagicallyHidden(target)) { storage.stopAttack(); - return false; // TODO: run away instead of doing nothing + return; // TODO: run away instead of doing nothing } const MWWorld::CellStore*& currentCell = storage.mCell; @@ -199,10 +201,9 @@ namespace MWMechanics float& actionCooldown = storage.mActionCooldown; if (actionCooldown > 0) - return false; + return; - float rangeAttack = 0; - float rangeFollow = 0; + float &rangeAttack = storage.mAttackRange; boost::shared_ptr& currentAction = storage.mCurrentAction; if (characterController.readyToPrepareAttack()) { @@ -210,6 +211,7 @@ namespace MWMechanics actionCooldown = currentAction->getActionCooldown(); } + float rangeFollow; if (currentAction.get()) currentAction->getCombatRange(rangeAttack, rangeFollow); @@ -243,7 +245,7 @@ namespace MWMechanics else //is creature { weaptype = actorClass.getCreatureStats(actor).getDrawState() == DrawState_Spell ? WeapType_Spell : WeapType_HandToHand; - weapRange = 150.0f; //TODO: use true attack range (the same problem in Creature::hit) + weapRange = 200; //TODO: use true attack range (the same problem in Creature::hit) } bool distantCombat = false; @@ -253,55 +255,19 @@ namespace MWMechanics if (weaptype == WeapType_BowAndArrow || weaptype == WeapType_Crossbow || weaptype == WeapType_Thrown) { rangeAttack = 1000; - rangeFollow = 0; // not needed in ranged combat distantCombat = true; } else { rangeAttack = weapRange; - rangeFollow = 300; } } else { distantCombat = (rangeAttack > 500); - weapRange = 150.f; + weapRange = 200; } - - bool& readyToAttack = storage.mReadyToAttack; - // start new attack - storage.startAttackIfReady(actor, characterController, weapon, distantCombat); - - /* - * Some notes on meanings of variables: - * - * rangeAttack: - * - * - Distance where attack using the actor's weapon is possible: - * longer for ranged weapons (obviously?) vs. melee weapons - * - Determined by weapon's reach parameter; hardcoded value - * for ranged weapon and for creatures - * - Once within this distance mFollowTarget is triggered - * - * rangeFollow: - * - * - Applies to melee weapons or hand to hand only (or creatures without - * weapons) - * - Distance a little further away than the actor's weapon reach - * i.e. rangeFollow > rangeAttack for melee weapons - * - Hardcoded value (0 for ranged weapons) - * - Once the target gets beyond this distance mFollowTarget is cleared - * and a path to the target needs to be found - * - * mFollowTarget: - * - * - Once triggered, the actor follows the target with LOS shortcut - * (the shortcut really only applies to cells where pathgrids are - * available, since the default path without pathgrids is direct to - * target even if LOS is not achieved) - */ - ESM::Position pos = actor.getRefData().getPosition(); osg::Vec3f vActorPos(pos.asVec3()); osg::Vec3f vTargetPos(target.getRefData().getPosition().asVec3()); @@ -309,153 +275,49 @@ namespace MWMechanics osg::Vec3f vAimDir = MWBase::Environment::get().getWorld()->aimToTarget(actor, target); float distToTarget = MWBase::Environment::get().getWorld()->getHitDistance(actor, target); - osg::Vec3f& lastActorPos = storage.mLastActorPos; - bool& followTarget = storage.mFollowTarget; - - bool isStuck = false; - float speed = 0.0f; - if(movement.mPosition[1] && (lastActorPos - vActorPos).length() < (speed = actorClass.getSpeed(actor)) * AI_REACTION_TIME / 2) - isStuck = true; - - lastActorPos = vActorPos; - - // check if actor can move along z-axis - bool canMoveByZ = (actorClass.canSwim(actor) && world->isSwimming(actor)) - || world->isFlying(actor); - // can't fight if attacker can't go where target is. E.g. A fish can't attack person on land. if (distToTarget >= rangeAttack && !actorClass.isNpc() && !MWMechanics::isEnvironmentCompatible(actor, target)) { // TODO: start fleeing? storage.stopAttack(); - return false; + return; } - // for distant combat we should know if target is in LOS even if distToTarget < rangeAttack - bool inLOS = distantCombat ? world->getLOS(actor, target) : true; - - // (within attack dist) || (not quite attack dist while following) - if(inLOS && (distToTarget < rangeAttack || (distToTarget <= rangeFollow && followTarget && !isStuck))) + if (storage.mReadyToAttack) { - mPathFinder.clearPath(); - //Melee and Close-up combat - - // getXAngleToDir determines vertical angle to target: - // if actor can move along z-axis it will control movement dir - // if can't - it will control correct aiming. - // note: in getZAngleToDir if we preserve dir.z then horizontal angle can be inaccurate + storage.startCombatMove(actorClass.isNpc(), distantCombat, distToTarget, rangeAttack); + // start new attack + storage.startAttackIfReady(actor, characterController, weapon, distantCombat); + if (distantCombat) { + // rotate actor taking into account target movement direction and projectile speed osg::Vec3f& lastTargetPos = storage.mLastTargetPos; - vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, AI_REACTION_TIME, weaptype, - storage.mStrength); + vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, AI_REACTION_TIME, weaptype, storage.mStrength); lastTargetPos = vTargetPos; - movement.mRotation[0] = getXAngleToDir(vAimDir); - movement.mRotation[2] = getZAngleToDir(vAimDir); - } - else - { - movement.mRotation[0] = getXAngleToDir(vAimDir); - movement.mRotation[2] = getZAngleToDir((vTargetPos-vActorPos)); // using vAimDir results in spastic movements since the head is animated - } - - // (not quite attack dist while following) - if (followTarget && distToTarget > rangeAttack) - { - //Close-up combat: just run up on target - storage.stopCombatMove(); - movement.mPosition[1] = 1; - } - else // (within attack dist) - { - storage.startCombatMove(actorClass.isNpc(), distantCombat, distToTarget, rangeAttack); - readyToAttack = true; - //only once got in melee combat, actor is allowed to use close-up shortcutting - followTarget = true; - } - } - else // remote pathfinding - { - bool preferShortcut = false; - if (!distantCombat) inLOS = world->getLOS(actor, target); - - // check if shortcut is available - bool& forceNoShortcut = storage.mForceNoShortcut; - ESM::Position& shortcutFailPos = storage.mShortcutFailPos; - - if(inLOS && (!isStuck || readyToAttack) - && (!forceNoShortcut || (shortcutFailPos.asVec3() - vActorPos).length() >= PATHFIND_SHORTCUT_RETRY_DIST)) - { - if(speed == 0.0f) speed = actorClass.getSpeed(actor); - // maximum dist before pit/obstacle for actor to avoid them depending on his speed - float maxAvoidDist = AI_REACTION_TIME * speed + speed / MAX_VEL_ANGULAR_RADIANS * 2; // *2 - for reliability - preferShortcut = checkWayIsClear(vActorPos, vTargetPos, osg::Vec3f(vAimDir.x(), vAimDir.y(), 0).length() > maxAvoidDist*1.5? maxAvoidDist : maxAvoidDist/2); - } - - // don't use pathgrid when actor can move in 3 dimensions - if (canMoveByZ) - { - preferShortcut = true; + MWMechanics::Movement& movement = storage.mMovement; movement.mRotation[0] = getXAngleToDir(vAimDir); + movement.mRotation[2] = getZAngleToDir(vAimDir); } - if(preferShortcut) - { - movement.mRotation[2] = getZAngleToDir((vTargetPos-vActorPos)); - forceNoShortcut = false; - shortcutFailPos.pos[0] = shortcutFailPos.pos[1] = shortcutFailPos.pos[2] = 0; - mPathFinder.clearPath(); - } - else // if shortcut failed stick to path grid - { - if(!isStuck && shortcutFailPos.pos[0] == 0.0f && shortcutFailPos.pos[1] == 0.0f && shortcutFailPos.pos[2] == 0.0f) - { - forceNoShortcut = true; - shortcutFailPos = pos; - } - - followTarget = false; - - buildNewPath(actor, target); - - // should always return a path (even if it's just go straight on target.) - assert(mPathFinder.isPathConstructed()); - } - - if (readyToAttack) - { - // to stop possible sideway moving after target moved out of attack range - storage.stopCombatMove(); - readyToAttack = false; - } - movement.mPosition[1] = 1; + storage.mAdjustAiming = distantCombat; } - - return false; } - void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, float duration, MWMechanics::Movement& desiredMovement) + void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage) { + // apply combat movement MWMechanics::Movement& actorMovementSettings = actor.getClass().getMovementSettings(actor); - if (mPathFinder.isPathConstructed()) - { - const ESM::Position& pos = actor.getRefData().getPosition(); - if (mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1])) - { - actorMovementSettings.mPosition[1] = 0; - } - else - { - evadeObstacles(actor, duration, pos); - } - } - else + actorMovementSettings.mPosition[0] = storage.mMovement.mPosition[0]; + actorMovementSettings.mPosition[1] = storage.mMovement.mPosition[1]; + actorMovementSettings.mPosition[2] = storage.mMovement.mPosition[2]; + + if (storage.mAdjustAiming) { - actorMovementSettings = desiredMovement; - rotateActorOnAxis(actor, 2, actorMovementSettings, desiredMovement); - rotateActorOnAxis(actor, 0, actorMovementSettings, desiredMovement); + rotateActorOnAxis(actor, 2, actorMovementSettings, storage.mMovement); + rotateActorOnAxis(actor, 0, actorMovementSettings, storage.mMovement); } } @@ -474,35 +336,6 @@ namespace MWMechanics } } - bool AiCombat::doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell) - { - if (!mPathFinder.getPath().empty()) - { - osg::Vec3f currPathTarget(PathFinder::MakeOsgVec3(mPathFinder.getPath().back())); - osg::Vec3f newPathTarget = PathFinder::MakeOsgVec3(dest); - float dist = (newPathTarget - currPathTarget).length(); - float targetPosThreshold = (cell->isExterior()) ? 300.0f : 100.0f; - return dist > targetPosThreshold; - } - else - { - // necessarily construct a new path - return true; - } - } - - void AiCombat::buildNewPath(const MWWorld::Ptr& actor, const MWWorld::Ptr& target) - { - ESM::Pathgrid::Point newPathTarget = PathFinder::MakePathgridPoint(target.getRefData().getPosition()); - - //construct new path only if target has moved away more than on [targetPosThreshold] - if (doesPathNeedRecalc(newPathTarget, actor.getCell()->getCell())) - { - ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(actor.getRefData().getPosition())); - mPathFinder.buildSyncedPath(start, newPathTarget, actor.getCell()); - } - } - int AiCombat::getTypeId() const { return TypeIdCombat; @@ -542,13 +375,13 @@ namespace MWMechanics mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); mCombatMove = true; } - // only NPCs are smart enough to use dodge movements + // dodge movements (for NPCs only) else if (isNpc && (!isDistantCombat || (distToTarget < rangeAttack / 2))) { //apply sideway movement (kind of dodging) with some probability if (Misc::Rng::rollClosedProbability() < 0.25) { - mMovement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f; + mMovement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f; // to the left/right mTimerCombatMove = 0.05f + 0.15f * Misc::Rng::rollClosedProbability(); mCombatMove = true; } diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index 93d630529..ad683b505 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -53,19 +53,14 @@ namespace MWMechanics virtual void writeState(ESM::AiSequence::AiSequence &sequence) const; - protected: - virtual bool doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell); - private: int mTargetActorId; - void buildNewPath(const MWWorld::Ptr& actor, const MWWorld::Ptr& target); - bool reactionTimeActions(const MWWorld::Ptr& actor, CharacterController& characterController, - AiCombatStorage& storage, MWWorld::Ptr target); + void attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController); /// Transfer desired movement (from AiCombatStorage) to Actor - void updateActorsMovement(const MWWorld::Ptr& actor, float duration, MWMechanics::Movement& movement); + void updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage); void rotateActorOnAxis(const MWWorld::Ptr& actor, int axis, MWMechanics::Movement& actorMovementSettings, MWMechanics::Movement& desiredMovement); }; diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 2999e8a19..4be464018 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -72,7 +72,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgr if (!mIsShortcutting) { - if (wasShortcutting || doesPathNeedRecalc(dest, actor.getCell()->getCell())) // only rebuild path if the target has moved + if (wasShortcutting || doesPathNeedRecalc(dest)) // only rebuild path if the target has moved { mPathFinder.buildSyncedPath(start, dest, actor.getCell()); @@ -226,13 +226,11 @@ bool MWMechanics::AiPackage::checkWayIsClearForActor(const ESM::Pathgrid::Point& return isClear; } -bool MWMechanics::AiPackage::doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell) +bool MWMechanics::AiPackage::doesPathNeedRecalc(const ESM::Pathgrid::Point& newDest) { - bool needRecalc = distance(mPrevDest, dest) > 10; - if (needRecalc) - mPrevDest = dest; + if (mPathFinder.getPath().empty()) return true; - return needRecalc; + return (distance(mPathFinder.getPath().back(), newDest) > 10); } bool MWMechanics::AiPackage::isTargetMagicallyHidden(const MWWorld::Ptr& target) diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 79537e616..2dc8bd3c7 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -95,7 +95,7 @@ namespace MWMechanics /// Check if the way to the destination is clear, taking into account actor speed bool checkWayIsClearForActor(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor); - virtual bool doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell); + virtual bool doesPathNeedRecalc(const ESM::Pathgrid::Point& newDest); void evadeObstacles(const MWWorld::Ptr& actor, float duration, const ESM::Position& pos); @@ -105,7 +105,6 @@ namespace MWMechanics float mTimer; - ESM::Pathgrid::Point mPrevDest; osg::Vec3f mLastActorPos; bool mIsShortcutting; // if shortcutting at the moment diff --git a/apps/openmw/mwmechanics/aipursue.cpp b/apps/openmw/mwmechanics/aipursue.cpp index 649a72cc0..7909b02c9 100644 --- a/apps/openmw/mwmechanics/aipursue.cpp +++ b/apps/openmw/mwmechanics/aipursue.cpp @@ -35,7 +35,6 @@ bool AiPursue::execute (const MWWorld::Ptr& actor, CharacterController& characte if(actor.getClass().getCreatureStats(actor).isDead()) return true; - ESM::Position pos = actor.getRefData().getPosition(); //position of the actor const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); //The target to follow if(target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered From b304e98568c15f7e180eef68414cabfa076d9753 Mon Sep 17 00:00:00 2001 From: mrcheko Date: Fri, 15 Jan 2016 21:49:27 +0300 Subject: [PATCH 04/11] implement ActionWeapon::getCombatRange (move logic from AiCombat) --- apps/openmw/mwmechanics/aicombat.cpp | 68 ++++------------------ apps/openmw/mwmechanics/aicombataction.cpp | 62 +++++++++++++++----- apps/openmw/mwmechanics/aicombataction.hpp | 13 +++-- 3 files changed, 64 insertions(+), 79 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index abf4109ba..acdbef299 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -211,61 +211,13 @@ namespace MWMechanics actionCooldown = currentAction->getActionCooldown(); } - float rangeFollow; - if (currentAction.get()) - currentAction->getCombatRange(rangeAttack, rangeFollow); - - // FIXME: consider moving this stuff to ActionWeapon::getCombatRange const ESM::Weapon *weapon = NULL; - MWMechanics::WeaponType weaptype = WeapType_None; - float weapRange = 1.0f; - - // Get weapon characteristics - MWBase::World* world = MWBase::Environment::get().getWorld(); - if (actorClass.hasInventoryStore(actor)) - { - //Get weapon range - MWWorld::ContainerStoreIterator weaponSlot = - MWMechanics::getActiveWeapon(actorClass.getCreatureStats(actor), actorClass.getInventoryStore(actor), &weaptype); - - if (weaptype == WeapType_HandToHand) - { - static float fHandToHandReach = - world->getStore().get().find("fHandToHandReach")->getFloat(); - weapRange = fHandToHandReach; - } - else if (weaptype != WeapType_PickProbe && weaptype != WeapType_Spell && weaptype != WeapType_None) - { - // All other WeapTypes are actually weapons, so get is safe. - weapon = weaponSlot->get()->mBase; - weapRange = weapon->mData.mReach; - } - weapRange *= 100.0f; - } - else //is creature - { - weaptype = actorClass.getCreatureStats(actor).getDrawState() == DrawState_Spell ? WeapType_Spell : WeapType_HandToHand; - weapRange = 200; //TODO: use true attack range (the same problem in Creature::hit) - } - - bool distantCombat = false; - if (weaptype != WeapType_Spell) - { - // TODO: move to ActionWeapon - if (weaptype == WeapType_BowAndArrow || weaptype == WeapType_Crossbow || weaptype == WeapType_Thrown) - { - rangeAttack = 1000; - distantCombat = true; - } - else - { - rangeAttack = weapRange; - } - } - else + bool isRangedCombat = false; + if (currentAction.get()) { - distantCombat = (rangeAttack > 500); - weapRange = 200; + rangeAttack = currentAction->getCombatRange(isRangedCombat); + // Get weapon characteristics + weapon = currentAction->getWeapon(); } ESM::Position pos = actor.getRefData().getPosition(); @@ -286,15 +238,15 @@ namespace MWMechanics if (storage.mReadyToAttack) { - storage.startCombatMove(actorClass.isNpc(), distantCombat, distToTarget, rangeAttack); + storage.startCombatMove(actorClass.isNpc(), isRangedCombat, distToTarget, rangeAttack); // start new attack - storage.startAttackIfReady(actor, characterController, weapon, distantCombat); + storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat); - if (distantCombat) + if (isRangedCombat) { // rotate actor taking into account target movement direction and projectile speed osg::Vec3f& lastTargetPos = storage.mLastTargetPos; - vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, AI_REACTION_TIME, weaptype, storage.mStrength); + vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, AI_REACTION_TIME, (weapon ? weapon->mData.mType : 0), storage.mStrength); lastTargetPos = vTargetPos; MWMechanics::Movement& movement = storage.mMovement; @@ -302,7 +254,7 @@ namespace MWMechanics movement.mRotation[2] = getZAngleToDir(vAimDir); } - storage.mAdjustAiming = distantCombat; + storage.mAdjustAiming = isRangedCombat; } } diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index 60446e524..76d5ed952 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -40,23 +40,20 @@ int getRangeTypes (const ESM::EffectList& effects) return types; } -void suggestCombatRange(int rangeTypes, float& rangeAttack, float& rangeFollow) +float suggestCombatRange(int rangeTypes) { if (rangeTypes & Touch) { - rangeAttack = 100.f; - rangeFollow = 300.f; + return 100.f; } else if (rangeTypes & Target) { - rangeAttack = 1000.f; - rangeFollow = 0.f; + return 1000.f; } else { // For Self spells, distance doesn't matter, so back away slightly to avoid enemy hits - rangeAttack = 600.f; - rangeFollow = 0.f; + return 600.f; } } @@ -394,11 +391,13 @@ namespace MWMechanics } } - void ActionSpell::getCombatRange(float& rangeAttack, float& rangeFollow) + float ActionSpell::getCombatRange (bool& isRanged) const { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(mSpellId); int types = getRangeTypes(spell->mEffects); - suggestCombatRange(types, rangeAttack, rangeFollow); + + isRanged = (types & RangeTypes::Target); + return suggestCombatRange(types); } void ActionEnchantedItem::prepare(const MWWorld::Ptr &actor) @@ -408,18 +407,17 @@ namespace MWMechanics actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Spell); } - void ActionEnchantedItem::getCombatRange(float& rangeAttack, float& rangeFollow) + float ActionEnchantedItem::getCombatRange(bool& isRanged) const { const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(mItem->getClass().getEnchantment(*mItem)); int types = getRangeTypes(enchantment->mEffects); - suggestCombatRange(types, rangeAttack, rangeFollow); + return suggestCombatRange(types); } - void ActionPotion::getCombatRange(float& rangeAttack, float& rangeFollow) + float ActionPotion::getCombatRange(bool& isRanged) const { // distance doesn't matter, so back away slightly to avoid enemy hits - rangeAttack = 600.f; - rangeFollow = 0.f; + return 600.f; } void ActionPotion::prepare(const MWWorld::Ptr &actor) @@ -430,6 +428,8 @@ namespace MWMechanics void ActionWeapon::prepare(const MWWorld::Ptr &actor) { + mIsNpc = actor.getClass().isNpc(); + if (actor.getClass().hasInventoryStore(actor)) { if (mWeapon.isEmpty()) @@ -449,9 +449,39 @@ namespace MWMechanics actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Weapon); } - void ActionWeapon::getCombatRange(float& rangeAttack, float& rangeFollow) + float ActionWeapon::getCombatRange(bool& isRanged) const + { + isRanged = false; + + if (mWeapon.isEmpty()) + { + if (!mIsNpc) + return 200.f; // TODO: use true attack range (the same problem in Creature::hit) + else + { + static float fHandToHandReach = + MWBase::Environment::get().getWorld()->getStore().get().find("fHandToHandReach")->getFloat(); + + return fHandToHandReach * 100.0f; + } + } + + const ESM::Weapon* weapon = mWeapon.get()->mBase; + + if (weapon->mData.mType >= ESM::Weapon::MarksmanBow) + { + isRanged = true; + return 1000.f; + } + else + return weapon->mData.mReach * 100.0f; + } + + const ESM::Weapon* ActionWeapon::getWeapon() const { - // Already done in AiCombat itself + if (mWeapon.isEmpty()) + return NULL; + return mWeapon.get()->mBase; } boost::shared_ptr prepareNextAction(const MWWorld::Ptr &actor, const MWWorld::Ptr &target) diff --git a/apps/openmw/mwmechanics/aicombataction.hpp b/apps/openmw/mwmechanics/aicombataction.hpp index a4a398d05..22f543418 100644 --- a/apps/openmw/mwmechanics/aicombataction.hpp +++ b/apps/openmw/mwmechanics/aicombataction.hpp @@ -16,8 +16,9 @@ namespace MWMechanics public: virtual ~Action() {} virtual void prepare(const MWWorld::Ptr& actor) = 0; - virtual void getCombatRange (float& rangeAttack, float& rangeFollow) = 0; + virtual float getCombatRange (bool& isRanged) const = 0; virtual float getActionCooldown() { return 0.f; } + virtual const ESM::Weapon* getWeapon() const { return NULL; }; }; class ActionSpell : public Action @@ -28,7 +29,7 @@ namespace MWMechanics /// Sets the given spell as selected on the actor's spell list. virtual void prepare(const MWWorld::Ptr& actor); - virtual void getCombatRange (float& rangeAttack, float& rangeFollow); + virtual float getCombatRange (bool& isRanged) const; }; class ActionEnchantedItem : public Action @@ -38,7 +39,7 @@ namespace MWMechanics MWWorld::ContainerStoreIterator mItem; /// Sets the given item as selected enchanted item in the actor's InventoryStore. virtual void prepare(const MWWorld::Ptr& actor); - virtual void getCombatRange (float& rangeAttack, float& rangeFollow); + virtual float getCombatRange (bool& isRanged) const; /// Since this action has no animation, apply a small cool down for using it virtual float getActionCooldown() { return 1.f; } @@ -51,7 +52,7 @@ namespace MWMechanics MWWorld::Ptr mPotion; /// Drinks the given potion. virtual void prepare(const MWWorld::Ptr& actor); - virtual void getCombatRange (float& rangeAttack, float& rangeFollow); + virtual float getCombatRange (bool& isRanged) const; /// Since this action has no animation, apply a small cool down for using it virtual float getActionCooldown() { return 1.f; } @@ -62,6 +63,7 @@ namespace MWMechanics private: MWWorld::Ptr mAmmunition; MWWorld::Ptr mWeapon; + bool mIsNpc; public: /// \a weapon may be empty for hand-to-hand combat @@ -69,7 +71,8 @@ namespace MWMechanics : mAmmunition(ammo), mWeapon(weapon) {} /// Equips the given weapon. virtual void prepare(const MWWorld::Ptr& actor); - virtual void getCombatRange (float& rangeAttack, float& rangeFollow); + virtual float getCombatRange (bool& isRanged) const; + virtual const ESM::Weapon* getWeapon() const; }; float rateSpell (const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& target); From 0793e4a80e03330c3b989f11c009e34fdf8afe37 Mon Sep 17 00:00:00 2001 From: mrcheko Date: Tue, 5 Jul 2016 21:38:41 +0300 Subject: [PATCH 05/11] refactor pathfinding code in AiWander: use AiPackage::pathTo, reuse AiPackage::ObstacleCheck --- apps/openmw/mwmechanics/aipackage.cpp | 9 +++-- apps/openmw/mwmechanics/aiwander.cpp | 47 ++++++++++----------------- apps/openmw/mwmechanics/aiwander.hpp | 2 +- apps/openmw/mwmechanics/obstacle.cpp | 5 +++ apps/openmw/mwmechanics/obstacle.hpp | 1 + 5 files changed, 31 insertions(+), 33 deletions(-) diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 4be464018..f2d75001a 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -68,7 +68,8 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgr { bool wasShortcutting = mIsShortcutting; bool destInLOS = false; - mIsShortcutting = shortcutPath(start, dest, actor, &destInLOS); // try to shortcut first + if (getTypeId() != TypeIdWander) // prohibit shortcuts for AiWander + mIsShortcutting = shortcutPath(start, dest, actor, &destInLOS); // try to shortcut first if (!mIsShortcutting) { @@ -139,8 +140,10 @@ void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor, float dur MWWorld::Ptr door = getNearbyDoor(actor); // NOTE: checks interior cells only if (door != MWWorld::Ptr()) { - if (!door.getCellRef().getTeleport() && door.getCellRef().getTrap().empty() - && door.getCellRef().getLockLevel() <= 0 && door.getClass().getDoorState(door) == 0) { + // note: AiWander currently does not open doors + if (getTypeId() != TypeIdWander && !door.getCellRef().getTeleport() && door.getCellRef().getTrap().empty() + && door.getCellRef().getLockLevel() <= 0 && door.getClass().getDoorState(door) == 0) + { MWBase::Environment::get().getWorld()->activateDoor(door, 1); } } diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 16c8d3156..d7dcad2dd 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -70,8 +70,6 @@ namespace MWMechanics unsigned short mIdleAnimation; std::vector mBadIdles; // Idle animations that when called cause errors - PathFinder mPathFinder; - AiWanderStorage(): mTargetAngleRadians(0), mTurnActorGivingGreetingToFacePlayer(false), @@ -87,7 +85,7 @@ namespace MWMechanics AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat): mDistance(distance), mDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle), mRepeat(repeat) - , mStoredInitialActorPosition(false) + , mStoredInitialActorPosition(false), mIsWanderDestReady(false) { mIdle.resize(8, 0); init(); @@ -252,7 +250,7 @@ namespace MWMechanics if ((wanderState == Wander_MoveNow) && mDistance) { // Construct a new path if there isn't one - if(!storage.mPathFinder.isPathConstructed()) + if(!mPathFinder.isPathConstructed()) { if (mAllowedNodes.size()) { @@ -290,7 +288,7 @@ namespace MWMechanics void AiWander::returnToStartLocation(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos) { - if (!storage.mPathFinder.isPathConstructed()) + if (!mPathFinder.isPathConstructed()) { ESM::Pathgrid::Point dest(PathFinder::MakePathgridPoint(mReturnPosition)); @@ -298,10 +296,11 @@ namespace MWMechanics ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos)); // don't take shortcuts for wandering - storage.mPathFinder.buildSyncedPath(start, dest, actor.getCell()); + mPathFinder.buildSyncedPath(start, dest, actor.getCell()); - if (storage.mPathFinder.isPathConstructed()) + if (mPathFinder.isPathConstructed()) { + mIsWanderDestReady = true; storage.mState = Wander_Walking; } } @@ -371,7 +370,7 @@ namespace MWMechanics float duration, AiWanderStorage& storage, ESM::Position& pos) { // Are we there yet? - if (storage.mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], DESTINATION_TOLERANCE)) + if (mIsWanderDestReady && pathTo(actor, mPathFinder.getPath().back(), duration, DESTINATION_TOLERANCE)) { stopWalking(actor, storage); storage.mState = Wander_ChooseAction; @@ -414,34 +413,21 @@ namespace MWMechanics void AiWander::evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage, float duration, ESM::Position& pos) { - // turn towards the next point in mPath - zTurn(actor, storage.mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1])); - - MWMechanics::Movement& movement = actor.getClass().getMovementSettings(actor); - if (mObstacleCheck.check(actor, duration)) + if (mObstacleCheck.isEvading()) { // first check if we're walking into a door if (proximityToDoor(actor)) // NOTE: checks interior cells only { // remove allowed points then select another random destination mTrimCurrentNode = true; - trimAllowedNodes(mAllowedNodes, storage.mPathFinder); + trimAllowedNodes(mAllowedNodes, mPathFinder); mObstacleCheck.clear(); - storage.mPathFinder.clearPath(); + mPathFinder.clearPath(); storage.mState = Wander_MoveNow; } - else // probably walking into another NPC - { - // TODO: diagonal should have same animation as walk forward - // but doesn't seem to do that? - mObstacleCheck.takeEvasiveAction(movement); - } + mStuckCount++; // TODO: maybe no longer needed } - else - { - movement.mPosition[1] = 1; - } // if stuck for sufficiently long, act like current location was the destination if (mStuckCount >= COUNT_BEFORE_RESET) // something has gone wrong, reset @@ -564,11 +550,12 @@ namespace MWMechanics ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(actorPos)); // don't take shortcuts for wandering - storage.mPathFinder.buildSyncedPath(start, dest, actor.getCell()); - storage.mPathFinder.buildPath(start, dest, actor.getCell()); + mPathFinder.buildSyncedPath(start, dest, actor.getCell()); - if (storage.mPathFinder.isPathConstructed()) + if (mPathFinder.isPathConstructed()) { + mIsWanderDestReady = true; + // Remove this node as an option and add back the previously used node (stops NPC from picking the same node): ESM::Pathgrid::Point temp = mAllowedNodes[randNode]; mAllowedNodes.erase(mAllowedNodes.begin() + randNode); @@ -624,7 +611,8 @@ namespace MWMechanics void AiWander::stopWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage) { - storage.mPathFinder.clearPath(); + mPathFinder.clearPath(); + mIsWanderDestReady = false; actor.getClass().getMovementSettings(actor).mPosition[1] = 0; } @@ -850,6 +838,7 @@ namespace MWMechanics , mRepeat(wander->mData.mShouldRepeat != 0) , mStoredInitialActorPosition(wander->mStoredInitialActorPosition) , mStartTime(MWWorld::TimeStamp(wander->mStartTime)) + , mIsWanderDestReady(false) { if (mStoredInitialActorPosition) mInitialActorPosition = wander->mInitialActorPosition; diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index b0fabfce3..eacadbcfd 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -130,9 +130,9 @@ namespace MWMechanics const PathFinder& pathfinder); -// ObstacleCheck mObstacleCheck; float mDoorCheckDuration; int mStuckCount; + bool mIsWanderDestReady; // constants for converting idleSelect values into groupNames enum GroupIndex diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp index 5815d8cbe..f0ffc92e1 100644 --- a/apps/openmw/mwmechanics/obstacle.cpp +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -93,6 +93,11 @@ namespace MWMechanics return mWalkState == State_Norm; } + bool ObstacleCheck::isEvading() const + { + return mWalkState == State_Evade; + } + /* * input - actor, duration (time since last check) * output - true if evasive action needs to be taken diff --git a/apps/openmw/mwmechanics/obstacle.hpp b/apps/openmw/mwmechanics/obstacle.hpp index 98cc4e7a0..37ccf72b2 100644 --- a/apps/openmw/mwmechanics/obstacle.hpp +++ b/apps/openmw/mwmechanics/obstacle.hpp @@ -33,6 +33,7 @@ namespace MWMechanics void clear(); bool isNormalState() const; + bool isEvading() const; // Returns true if there is an obstacle and an evasive action // should be taken From 05794505c82ebca95226012a6b7beb91844ea2f6 Mon Sep 17 00:00:00 2001 From: mrcheko Date: Sun, 10 Jul 2016 17:25:50 +0300 Subject: [PATCH 06/11] travis build fix --- apps/openmw/mwmechanics/aicombataction.cpp | 2 +- apps/openmw/mwmechanics/pathfinding.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index 8953c8a2a..ae28261ba 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -429,7 +429,7 @@ namespace MWMechanics const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(mSpellId); int types = getRangeTypes(spell->mEffects); - isRanged = (types & RangeTypes::Target); + isRanged = (types & Target); return suggestCombatRange(types); } diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index a3e4da76f..2a452907d 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -111,7 +111,7 @@ namespace MWMechanics osg::Vec3f dir = to - from; dir.z() = 0; dir.normalize(); - float verticalOffset = 200; // instead of '200' here we want the height of the actor + float verticalOffset = 200; // instead of '200' here we want the height of the actor osg::Vec3f _from = from + dir*offsetXY + osg::Z_AXIS * verticalOffset; // cast up-down ray and find height in world space of hit From 8d4f0660ce801b159d4b920c34634ced49b98e6e Mon Sep 17 00:00:00 2001 From: mrcheko Date: Tue, 12 Jul 2016 00:17:43 +0300 Subject: [PATCH 07/11] fix hardcoded melee attack ranges --- apps/openmw/mwmechanics/aicombat.cpp | 11 +++++++---- apps/openmw/mwmechanics/aicombataction.cpp | 3 ++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index e00a71f24..130c146b6 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -54,11 +54,11 @@ namespace MWMechanics AiCombatStorage(): mAttackCooldown(0), - mTimerReact(0), + mTimerReact(AI_REACTION_TIME), mTimerCombatMove(0), mReadyToAttack(false), mAttack(false), - mAttackRange(200), // default attack range (same as in Creature::Hit) + mAttackRange(0), mCombatMove(false), mLastTargetPos(0,0,0), mCell(NULL), @@ -159,8 +159,11 @@ namespace MWMechanics || target.getClass().getCreatureStats(target).isDead()) return true; - //Update every frame - storage.mReadyToAttack = pathTo(actor, target.getRefData().getPosition().pos, duration, storage.mAttackRange); + if (storage.mCurrentAction.get()) // need to wait to init action with it's attack range + { + //Update every frame + storage.mReadyToAttack = pathTo(actor, target.getRefData().getPosition().pos, duration, storage.mAttackRange); + } storage.updateCombatMove(duration); if (storage.mReadyToAttack) updateActorsMovement(actor, duration, storage); diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index ae28261ba..a70410035 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -44,7 +44,8 @@ float suggestCombatRange(int rangeTypes) { if (rangeTypes & Touch) { - return 100.f; + static const float fCombatDistance = MWBase::Environment::get().getWorld()->getStore().get().find("fCombatDistance")->getFloat(); + return fCombatDistance; } else if (rangeTypes & Target) { From 59a1a6d117da291c7db953fab14963f63486fe44 Mon Sep 17 00:00:00 2001 From: mrcheko Date: Sat, 16 Jul 2016 19:07:48 +0300 Subject: [PATCH 08/11] checkWayIsClear: remove PATHFIND_CAUTION_DIST check --- apps/openmw/mwmechanics/pathfinding.cpp | 22 ++++++++-------------- apps/openmw/mwmechanics/pathfinding.hpp | 2 -- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 2a452907d..ef60a85a4 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -106,22 +106,16 @@ namespace MWMechanics bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY) { - if((to - from).length() >= PATHFIND_CAUTION_DIST || std::abs(from.z() - to.z()) <= PATHFIND_Z_REACH) - { - osg::Vec3f dir = to - from; - dir.z() = 0; - dir.normalize(); - float verticalOffset = 200; // instead of '200' here we want the height of the actor - osg::Vec3f _from = from + dir*offsetXY + osg::Z_AXIS * verticalOffset; + osg::Vec3f dir = to - from; + dir.z() = 0; + dir.normalize(); + float verticalOffset = 200; // instead of '200' here we want the height of the actor + osg::Vec3f _from = from + dir*offsetXY + osg::Z_AXIS * verticalOffset; - // cast up-down ray and find height in world space of hit - float h = _from.z() - MWBase::Environment::get().getWorld()->getDistToNearestRayHit(_from, -osg::Z_AXIS, verticalOffset + PATHFIND_Z_REACH + 1); + // cast up-down ray and find height of hit in world space + float h = _from.z() - MWBase::Environment::get().getWorld()->getDistToNearestRayHit(_from, -osg::Z_AXIS, verticalOffset + PATHFIND_Z_REACH + 1); - if(std::abs(from.z() - h) <= PATHFIND_Z_REACH) - return true; - } - - return false; + return (std::abs(from.z() - h) <= PATHFIND_Z_REACH); } PathFinder::PathFinder() diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 05f846991..64608979b 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -23,8 +23,6 @@ namespace MWMechanics const float PATHFIND_Z_REACH = 50.0f; //static const float sMaxSlope = 49.0f; // duplicate as in physicssystem - // distance at which actor pays more attention to decide whether to shortcut or stick to pathgrid - const float PATHFIND_CAUTION_DIST = 500.0f; // distance after which actor (failed previously to shortcut) will try again const float PATHFIND_SHORTCUT_RETRY_DIST = 300.0f; From 203804ff15c0692125924972a592c1eb998b6c9b Mon Sep 17 00:00:00 2001 From: mrcheko Date: Sat, 16 Jul 2016 19:08:49 +0300 Subject: [PATCH 09/11] fix AiPackage repeating --- apps/openmw/mwmechanics/aipackage.cpp | 12 ++++++++++++ apps/openmw/mwmechanics/aipackage.hpp | 3 +++ apps/openmw/mwmechanics/aisequence.cpp | 1 + 3 files changed, 16 insertions(+) diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 90fe8c21b..1131b5e6f 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -58,6 +58,18 @@ bool MWMechanics::AiPackage::getRepeat() const return false; } +void MWMechanics::AiPackage::reset() +{ + // reset all members + mTimer = AI_REACTION_TIME + 1.0f; + mIsShortcutting = false; + mShortcutProhibited = false; + mShortcutFailPos = ESM::Pathgrid::Point(); + + mPathFinder.clearPath(); + mObstacleCheck.clear(); +} + bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest, float duration, float destTolerance) { mTimer += duration; //Update timer diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index d30ed89ca..4feb13fe0 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -92,6 +92,9 @@ namespace MWMechanics /// Return true if this package should repeat. Currently only used for Wander packages. virtual bool getRepeat() const; + /// Reset pathfinding state + void reset(); + bool isTargetMagicallyHidden(const MWWorld::Ptr& target); protected: diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index f05725cc2..b03586c3b 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -234,6 +234,7 @@ void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& charac // Put repeating noncombat AI packages on the end of the stack so they can be used again if (isActualAiPackage(packageTypeId) && (mRepeat || package->getRepeat())) { + package->reset(); mPackages.push_back(package->clone()); } // To account for the rare case where AiPackage::execute() queued another AI package From 293a0f768c38d2ff816d29236b253a95c7e43f01 Mon Sep 17 00:00:00 2001 From: mrcheko Date: Wed, 27 Jul 2016 23:43:32 +0300 Subject: [PATCH 10/11] use real distance to target to determinate 'ready to attack' state --- apps/openmw/mwmechanics/aicombat.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 130c146b6..9cba7f2b9 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -162,7 +162,8 @@ namespace MWMechanics if (storage.mCurrentAction.get()) // need to wait to init action with it's attack range { //Update every frame - storage.mReadyToAttack = pathTo(actor, target.getRefData().getPosition().pos, duration, storage.mAttackRange); + bool is_target_reached = pathTo(actor, target.getRefData().getPosition().pos, duration, storage.mAttackRange); + if (is_target_reached) storage.mReadyToAttack = true; } storage.updateCombatMove(duration); @@ -229,9 +230,11 @@ namespace MWMechanics osg::Vec3f vAimDir = MWBase::Environment::get().getWorld()->aimToTarget(actor, target); float distToTarget = MWBase::Environment::get().getWorld()->getHitDistance(actor, target); + + storage.mReadyToAttack = (distToTarget <= rangeAttack); // can't fight if attacker can't go where target is. E.g. A fish can't attack person on land. - if (distToTarget >= rangeAttack + if (distToTarget > rangeAttack && !actorClass.isNpc() && !MWMechanics::isEnvironmentCompatible(actor, target)) { // TODO: start fleeing? From e2cd87fcc761bb2b7194eb3e27a2a8948b16c8fe Mon Sep 17 00:00:00 2001 From: mrcheko Date: Sun, 7 Aug 2016 19:23:08 +0300 Subject: [PATCH 11/11] fix veritcal aiming --- apps/openmw/mwmechanics/aicombat.cpp | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 9cba7f2b9..f5c64d4ab 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -50,7 +50,6 @@ namespace MWMechanics bool mForceNoShortcut; ESM::Position mShortcutFailPos; MWMechanics::Movement mMovement; - bool mAdjustAiming; AiCombatStorage(): mAttackCooldown(0), @@ -67,8 +66,7 @@ namespace MWMechanics mStrength(), mForceNoShortcut(false), mShortcutFailPos(), - mMovement(), - mAdjustAiming(false) + mMovement() {} void startCombatMove(bool isNpc, bool isDistantCombat, float distToTarget, float rangeAttack); @@ -255,12 +253,14 @@ namespace MWMechanics vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, AI_REACTION_TIME, (weapon ? weapon->mData.mType : 0), storage.mStrength); lastTargetPos = vTargetPos; - MWMechanics::Movement& movement = storage.mMovement; - movement.mRotation[0] = getXAngleToDir(vAimDir); - movement.mRotation[2] = getZAngleToDir(vAimDir); + storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir); + storage.mMovement.mRotation[2] = getZAngleToDir(vAimDir); + } + else + { + storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir); + storage.mMovement.mRotation[2] = getZAngleToDir((vTargetPos-vActorPos)); // using vAimDir results in spastic movements since the head is animated } - - storage.mAdjustAiming = isRangedCombat; } } @@ -272,11 +272,8 @@ namespace MWMechanics actorMovementSettings.mPosition[1] = storage.mMovement.mPosition[1]; actorMovementSettings.mPosition[2] = storage.mMovement.mPosition[2]; - if (storage.mAdjustAiming) - { - rotateActorOnAxis(actor, 2, actorMovementSettings, storage.mMovement); - rotateActorOnAxis(actor, 0, actorMovementSettings, storage.mMovement); - } + rotateActorOnAxis(actor, 2, actorMovementSettings, storage.mMovement); + rotateActorOnAxis(actor, 0, actorMovementSettings, storage.mMovement); } void AiCombat::rotateActorOnAxis(const MWWorld::Ptr& actor, int axis,