From f811abb752efdd8306c081df67d92edd57d86728 Mon Sep 17 00:00:00 2001 From: mrcheko Date: Sun, 20 Apr 2014 20:35:07 +0400 Subject: [PATCH] pathgrid shortcutting extended --- apps/openmw/mwbase/world.hpp | 2 + apps/openmw/mwmechanics/aicombat.cpp | 204 ++++++++++++++++-------- apps/openmw/mwmechanics/aicombat.hpp | 5 +- apps/openmw/mwmechanics/pathfinding.cpp | 11 +- apps/openmw/mwmechanics/pathfinding.hpp | 13 +- apps/openmw/mwmechanics/steering.cpp | 3 +- apps/openmw/mwmechanics/steering.hpp | 3 + apps/openmw/mwworld/worldimp.cpp | 15 +- apps/openmw/mwworld/worldimp.hpp | 2 + 9 files changed, 181 insertions(+), 77 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index f03a9197d..98ec9cfce 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -407,6 +407,8 @@ namespace MWBase virtual bool getLOS(const MWWorld::Ptr& npc,const MWWorld::Ptr& targetNpc) = 0; ///< get Line of Sight (morrowind stupid implementation) + virtual float getDistToNearestRayHit(const Ogre::Vector3& from, const Ogre::Vector3& dir, float maxDist) = 0; + virtual void enableActorCollision(const MWWorld::Ptr& actor, bool enable) = 0; virtual int canRest() = 0; diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index fd2fa61ba..84798ca02 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -31,6 +31,40 @@ namespace //chooses an attack depending on probability to avoid uniformity void chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement); + + float getZAngleToDir(const Ogre::Vector3& dir, float dirLen = 0.0f) + { + float len = (dirLen >= 0.0f)? dirLen : dir.length(); + return Ogre::Radian( Ogre::Math::ACos(dir.y / len) * sgn(Ogre::Math::ASin(dir.x / 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 offset) + { + if((to - from).length() >= PATHFIND_CAUTION_DIST || 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*offset + 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(abs(from.z - h) <= PATHFIND_Z_REACH) + return true; + } + + return false; + } } namespace MWMechanics @@ -44,9 +78,10 @@ namespace MWMechanics mReadyToAttack(false), mStrike(false), mCombatMove(false), - mRotate(false), mMovement(), - mTargetAngle(0) + mTargetAngle(0), + mForceNoShortcut(false), + mShortcutFailPos() { } @@ -71,13 +106,12 @@ namespace MWMechanics mCombatMove = false; } } - + actor.getClass().getMovementSettings(actor) = mMovement; - if (mRotate) + if(actor.getRefData().getPosition().rot[2] != mTargetAngle) { - if (zTurn(actor, Ogre::Degree(mTargetAngle))) - mRotate = false; + zTurn(actor, Ogre::Degree(mTargetAngle)); } @@ -92,7 +126,7 @@ namespace MWMechanics } //Update with period = tReaction - + mTimerReact = 0; //actual attacking logic @@ -136,6 +170,7 @@ namespace MWMechanics actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); + // Get weapon characteristics if (actor.getClass().hasInventoryStore(actor)) { MWMechanics::DrawState_ state = actor.getClass().getCreatureStats(actor).getDrawState(); @@ -166,15 +201,13 @@ namespace MWMechanics weapRange = 150; //TODO: use true attack range (the same problem in Creature::hit) } - ESM::Position pos = actor.getRefData().getPosition(); - float rangeMelee; float rangeCloseUp; bool distantCombat = false; if (weaptype==WeapType_BowAndArrow || weaptype==WeapType_Crossbow || weaptype==WeapType_Thrown) { rangeMelee = 1000; // TODO: should depend on archer skill - rangeCloseUp = 0; //doesn't needed when attacking from distance + rangeCloseUp = 0; // not needed in ranged combat distantCombat = true; } else @@ -182,23 +215,27 @@ namespace MWMechanics rangeMelee = weapRange; rangeCloseUp = 300; } + + ESM::Position pos = actor.getRefData().getPosition(); + Ogre::Vector3 vActorPos(pos.pos); + Ogre::Vector3 vTargetPos(mTarget.getRefData().getPosition().pos); + Ogre::Vector3 vDirToTarget = vTargetPos - vActorPos; + float distToTarget = vDirToTarget.length(); - Ogre::Vector3 vStart(pos.pos[0], pos.pos[1], pos.pos[2]); - ESM::Position targetPos = mTarget.getRefData().getPosition(); - Ogre::Vector3 vDest(targetPos.pos[0], targetPos.pos[1], targetPos.pos[2]); - Ogre::Vector3 vDir = vDest - vStart; - float distBetween = vDir.length(); + bool isStuck = false; + float speed = 0.0f; + if(mMovement.mPosition[1] && (Ogre::Vector3(mLastPos.pos) - vActorPos).length() < (speed = cls.getSpeed(actor)) / 10.0f) + isStuck = true; - if(distBetween < rangeMelee || (distBetween <= rangeCloseUp && mFollowTarget) ) + mLastPos = pos; + + if(distToTarget < rangeMelee || (distToTarget <= rangeCloseUp && mFollowTarget && !isStuck) ) { //Melee and Close-up combat - vDir.z = 0; - float dirLen = vDir.length(); - mTargetAngle = Ogre::Radian( Ogre::Math::ACos(vDir.y / dirLen) * sgn(Ogre::Math::ASin(vDir.x / dirLen)) ).valueDegrees(); - mRotate = true; + vDirToTarget.z = 0; + mTargetAngle = getZAngleToDir(vDirToTarget, distToTarget); - //bool LOS = MWBase::Environment::get().getWorld()->getLOS(actor, mTarget); - if (mFollowTarget && distBetween > rangeMelee) + if (mFollowTarget && distToTarget > rangeMelee) { //Close-up combat: just run up on target mMovement.mPosition[1] = 1; @@ -228,7 +265,7 @@ namespace MWMechanics } } - if(distantCombat && distBetween < rangeMelee/4) + if(distantCombat && distToTarget < rangeMelee/4) { mMovement.mPosition[1] = -1; } @@ -238,32 +275,80 @@ namespace MWMechanics mFollowTarget = true; } } - else + else // remote pathfinding { - //target is at far distance: build path to target OR follow target (if previously actor had reached it once) - mFollowTarget = false; + bool preferShortcut = false; + bool inLOS; + + if(mReadyToAttack) isStuck = false; + + // check if shortcut is available + if(!isStuck + && (!mForceNoShortcut + || (Ogre::Vector3(mShortcutFailPos.pos) - vActorPos).length() >= PATHFIND_SHORTCUT_RETRY_DIST) + && (inLOS = MWBase::Environment::get().getWorld()->getLOS(actor, mTarget))) + { + if(speed == 0.0f) speed = cls.getSpeed(actor); + // maximum dist before pit/obstacle for actor to avoid them depending on his speed + float maxAvoidDist = tReaction * speed + speed / MAX_VEL_ANGULAR.valueRadians() * 2; + //if(actor.getRefData().getPosition().rot[2] != mTargetAngle) + preferShortcut = checkWayIsClear(vActorPos, vTargetPos, distToTarget > maxAvoidDist*1.5? maxAvoidDist : maxAvoidDist/2); + } + + if(preferShortcut) + { + mTargetAngle = getZAngleToDir(vDirToTarget, distToTarget); + mForceNoShortcut = false; + mShortcutFailPos.pos[0] = mShortcutFailPos.pos[1] = mShortcutFailPos.pos[2] = 0; + if(mPathFinder.isPathConstructed()) + mPathFinder.clearPath(); + } + else // if shortcut failed stick to path grid + { + if(!isStuck && mShortcutFailPos.pos[0] == 0.0f && mShortcutFailPos.pos[1] == 0.0f && mShortcutFailPos.pos[2] == 0.0f) + { + mForceNoShortcut = true; + mShortcutFailPos = pos; + } - buildNewPath(actor); //may fail to build a path, check before use + mFollowTarget = false; - //delete visited path node - mPathFinder.checkPathCompleted(pos.pos[0],pos.pos[1],pos.pos[2]); + buildNewPath(actor); //may fail to build a path, check before use - //if no new path leave mTargetAngle unchanged - if(!mPathFinder.getPath().empty()) - { - //try shortcut - if(vDir.length() < mPathFinder.getDistToNext(pos.pos[0],pos.pos[1],pos.pos[2]) && MWBase::Environment::get().getWorld()->getLOS(actor, mTarget)) - mTargetAngle = Ogre::Radian( Ogre::Math::ACos(vDir.y / vDir.length()) * sgn(Ogre::Math::ASin(vDir.x / vDir.length())) ).valueDegrees(); - else - mTargetAngle = mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]); - mRotate = true; + //delete visited path node + mPathFinder.checkWaypoint(pos.pos[0],pos.pos[1],pos.pos[2]); + + // This works on the borders between the path grid and areas with no waypoints. + if(inLOS && mPathFinder.getPath().size() > 1) + { + // get point just before target + std::list::iterator pntIter = --mPathFinder.getPath().end(); + --pntIter; + Ogre::Vector3 vBeforeTarget = Ogre::Vector3(pntIter->mX, pntIter->mY, pntIter->mZ); + + // if current actor pos is closer to target then last point of path (excluding target itself) then go straight on target + if(distToTarget <= (vTargetPos - vBeforeTarget).length()) + { + mTargetAngle = getZAngleToDir(vDirToTarget, distToTarget); + preferShortcut = true; + } + } + + // if there is no new path, then go straight on target + if(!preferShortcut) + { + if(!mPathFinder.getPath().empty()) + mTargetAngle = mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]); + else + mTargetAngle = getZAngleToDir(vDirToTarget, distToTarget); + } } mMovement.mPosition[1] = 1; mReadyToAttack = false; } - if(distBetween > rangeMelee) + if(distToTarget > rangeMelee) { //special run attack; it shouldn't affect melee combat tactics if(actor.getClass().getMovementSettings(actor).mPosition[1] == 1) @@ -277,7 +362,7 @@ namespace MWMechanics && mTarget.getClass().getMovementSettings(mTarget).mPosition[1] == 0) speed2 = 0; - float s1 = distBetween - weapRange; + float s1 = distToTarget - weapRange; float t = s1/speed1; float s2 = speed2 * t; float t_swing = 0.17f/weapSpeed;//instead of 0.17 should be the time of playing weapon anim from 'start' to 'hit' tags @@ -300,31 +385,23 @@ namespace MWMechanics void AiCombat::buildNewPath(const MWWorld::Ptr& actor) { - //Construct path to target - ESM::Pathgrid::Point dest; - dest.mX = mTarget.getRefData().getPosition().pos[0]; - dest.mY = mTarget.getRefData().getPosition().pos[1]; - dest.mZ = mTarget.getRefData().getPosition().pos[2]; - Ogre::Vector3 newPathTarget = Ogre::Vector3(dest.mX, dest.mY, dest.mZ); - - float dist = -1; //hack to indicate first time, to construct a new path + Ogre::Vector3 newPathTarget = Ogre::Vector3(mTarget.getRefData().getPosition().pos); + + float dist; + if(!mPathFinder.getPath().empty()) { ESM::Pathgrid::Point lastPt = mPathFinder.getPath().back(); Ogre::Vector3 currPathTarget(lastPt.mX, lastPt.mY, lastPt.mZ); - dist = Ogre::Math::Abs((newPathTarget - currPathTarget).length()); + dist = (newPathTarget - currPathTarget).length(); } + else dist = 1e+38F; // necessarily construct a new path - float targetPosThreshold; - bool isOutside = actor.getCell()->getCell()->isExterior(); - if (isOutside) - targetPosThreshold = 300; - else - targetPosThreshold = 100; + float targetPosThreshold = (actor.getCell()->getCell()->isExterior())? 300 : 100; - if((dist < 0) || (dist > targetPosThreshold)) + //construct new path only if target has moved away more than on [targetPosThreshold] + if(dist > targetPosThreshold) { - //construct new path only if target has moved away more than on ESM::Position pos = actor.getRefData().getPosition(); ESM::Pathgrid::Point start; @@ -332,17 +409,18 @@ namespace MWMechanics start.mY = pos.pos[1]; start.mZ = pos.pos[2]; + ESM::Pathgrid::Point dest; + dest.mX = newPathTarget.x; + dest.mY = newPathTarget.y; + dest.mZ = newPathTarget.z; + if(!mPathFinder.isPathConstructed()) - mPathFinder.buildPath(start, dest, actor.getCell(), isOutside); + mPathFinder.buildPath(start, dest, actor.getCell(), false); else { PathFinder newPathFinder; - newPathFinder.buildPath(start, dest, actor.getCell(), isOutside); + newPathFinder.buildPath(start, dest, actor.getCell(), false); - //TO EXPLORE: - //maybe here is a mistake (?): PathFinder::getPathSize() returns number of grid points in the path, - //not the actual path length. Here we should know if the new path is actually more effective. - //if(pathFinder2.getPathSize() < mPathFinder.getPathSize()) if(!mPathFinder.getPath().empty()) { newPathFinder.syncStart(mPathFinder.getPath()); diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index 767a36292..b71dd9cf0 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -43,8 +43,11 @@ namespace MWMechanics bool mReadyToAttack, mStrike; bool mFollowTarget; bool mCombatMove; - bool mRotate; + bool mForceNoShortcut; + ESM::Position mShortcutFailPos; + + ESM::Position mLastPos; MWMechanics::Movement mMovement; MWWorld::Ptr mTarget; diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 3ecd40743..4aee6f6e4 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -384,22 +384,21 @@ namespace MWMechanics return false; } - void PathFinder::syncStart(const std::list &path) + bool PathFinder::syncStart(const std::list &path) { if (mPath.size() < 2) - return; //nothing to pop + return false; //nothing to pop std::list::const_iterator oldStart = path.begin(); std::list::iterator iter = ++mPath.begin(); if( (*iter).mX == oldStart->mX && (*iter).mY == oldStart->mY - && (*iter).mZ == oldStart->mZ - && (*iter).mAutogenerated == oldStart->mAutogenerated - && (*iter).mConnectionNum == oldStart->mConnectionNum ) + && (*iter).mZ == oldStart->mZ) { mPath.pop_front(); + return true; } - + return false; } } diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index ecaaef568..86fa3b6bc 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -64,10 +64,15 @@ namespace MWMechanics return mPath; } - //When first point of newly created path is the nearest to actor point, then - //the cituation can occure when this point is undesirable (if the 2nd point of new path == the 1st point of old path) - //This functions deletes that point. - void syncStart(const std::list &path); + /** Synchronize new path with old one to avoid visiting 1 waypoint 2 times + @note + If the first point is chosen as the nearest one + the cituation can occure when the 1st point of the new path is undesirable + (i.e. the 2nd point of new path == the 1st point of old path). + @param path - old path + @return true if such point was found and deleted + */ + bool syncStart(const std::list &path); void addPointToPath(ESM::Pathgrid::Point &point) { diff --git a/apps/openmw/mwmechanics/steering.cpp b/apps/openmw/mwmechanics/steering.cpp index d911fd81b..7646e832d 100644 --- a/apps/openmw/mwmechanics/steering.cpp +++ b/apps/openmw/mwmechanics/steering.cpp @@ -31,8 +31,7 @@ bool zTurn(const MWWorld::Ptr& actor, Ogre::Radian targetAngle) if (absDiff < epsilon) return true; - // Max. speed of 10 radian per sec - Ogre::Radian limit = Ogre::Radian(10) * MWBase::Environment::get().getFrameDuration(); + Ogre::Radian limit = MAX_VEL_ANGULAR * MWBase::Environment::get().getFrameDuration(); if (absDiff > limit) diff = Ogre::Math::Sign(diff) * limit; diff --git a/apps/openmw/mwmechanics/steering.hpp b/apps/openmw/mwmechanics/steering.hpp index 504dc3ac3..9bdf7d4a3 100644 --- a/apps/openmw/mwmechanics/steering.hpp +++ b/apps/openmw/mwmechanics/steering.hpp @@ -10,6 +10,9 @@ class Ptr; namespace MWMechanics { +// Max rotating speed, radian/sec +const Ogre::Radian MAX_VEL_ANGULAR(10); + /// configure rotation settings for an actor to reach this target angle (eventually) /// @return have we reached the target angle? bool zTurn(const MWWorld::Ptr& actor, Ogre::Radian targetAngle); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 594a9f7f4..47536ab6c 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1875,7 +1875,7 @@ namespace MWWorld out.push_back(searchPtrViaHandle(*it)); } } - + bool World::getLOS(const MWWorld::Ptr& npc,const MWWorld::Ptr& targetNpc) { if (!targetNpc.getRefData().isEnabled() || !npc.getRefData().isEnabled()) @@ -1893,6 +1893,19 @@ namespace MWWorld return false; } + float World::getDistToNearestRayHit(const Ogre::Vector3& from, const Ogre::Vector3& dir, float maxDist) + { + btVector3 btFrom(from.x, from.y, from.z); + btVector3 btTo = btVector3(dir.x, dir.y, dir.z); + btTo.normalize(); + btTo = btFrom + btTo * maxDist; + + std::pair result = mPhysEngine->rayTest(btFrom, btTo, false); + + if(result.second == -1) return maxDist; + else return result.second*(btTo-btFrom).length(); + } + void World::enableActorCollision(const MWWorld::Ptr& actor, bool enable) { OEngine::Physic::PhysicActor *physicActor = mPhysEngine->getCharacter(actor.getRefData().getHandle()); diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index f1e89bf6b..0eea3c6a0 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -510,6 +510,8 @@ namespace MWWorld virtual bool getLOS(const MWWorld::Ptr& npc,const MWWorld::Ptr& targetNpc); ///< get Line of Sight (morrowind stupid implementation) + virtual float getDistToNearestRayHit(const Ogre::Vector3& from, const Ogre::Vector3& dir, float maxDist); + virtual void enableActorCollision(const MWWorld::Ptr& actor, bool enable); virtual int canRest();