From 3732979eecb095a16e71180f52143d545c2c8ca5 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sun, 14 Aug 2016 18:02:13 +0200 Subject: [PATCH] Revert "Merge pull request #993 from mrcheko/pathfinding" This reverts commit 5190275b3718a05cc20f0c9b6ccbf519a8e10379, reversing changes made to d7845012bf341878974e29c43c3cfe673d715e63. --- apps/openmw/mwmechanics/aiactivate.cpp | 22 +- apps/openmw/mwmechanics/aicombat.cpp | 364 +++++++++++++++++---- apps/openmw/mwmechanics/aicombat.hpp | 9 +- apps/openmw/mwmechanics/aicombataction.cpp | 67 +--- apps/openmw/mwmechanics/aicombataction.hpp | 13 +- apps/openmw/mwmechanics/aifollow.cpp | 35 +- apps/openmw/mwmechanics/aipackage.cpp | 199 +++-------- apps/openmw/mwmechanics/aipackage.hpp | 26 +- apps/openmw/mwmechanics/aipursue.cpp | 9 +- apps/openmw/mwmechanics/aisequence.cpp | 1 - apps/openmw/mwmechanics/aiwander.cpp | 76 +++-- apps/openmw/mwmechanics/aiwander.hpp | 4 +- apps/openmw/mwmechanics/obstacle.cpp | 5 - apps/openmw/mwmechanics/obstacle.hpp | 1 - apps/openmw/mwmechanics/pathfinding.cpp | 71 +--- apps/openmw/mwmechanics/pathfinding.hpp | 26 +- 16 files changed, 490 insertions(+), 438 deletions(-) diff --git a/apps/openmw/mwmechanics/aiactivate.cpp b/apps/openmw/mwmechanics/aiactivate.cpp index 745a01c8b..a79adbc8b 100644 --- a/apps/openmw/mwmechanics/aiactivate.cpp +++ b/apps/openmw/mwmechanics/aiactivate.cpp @@ -23,29 +23,33 @@ namespace MWMechanics return new AiActivate(*this); } - bool AiActivate::execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) + bool 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); - if (target == MWWorld::Ptr() || - !target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should check whether the target is currently registered + if(target == MWWorld::Ptr() || + !target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered // with the MechanicsManager - ) - return true; //Target doesn't exist + ) + return true; //Target doesn't exist - //Set the target destination for the actor + //Set the target desition from the actor ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; - if (pathTo(actor, dest, duration, MWBase::Environment::get().getWorld()->getMaxActivationDistance())) //Stop when you get in activation range - { - // activate when reached + if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < MWBase::Environment::get().getWorld()->getMaxActivationDistance()) { //Stop when you get in activation range + actor.getClass().getMovementSettings(actor).mPosition[1] = 0; 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 f5c64d4ab..ce66b045a 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -27,6 +27,46 @@ namespace osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const osg::Vec3f& vLastTargetPos, float duration, int weapType, float strength); + + float getZAngleToDir(const osg::Vec3f& dir) + { + return std::atan2(dir.x(), dir.y()); + } + + float getXAngleToDir(const osg::Vec3f& dir) + { + return -std::asin(dir.z() / dir.length()); + } + + const float REACTION_INTERVAL = 0.25f; + + 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 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::Vec3f(0,0,1) * verticalOffset; + + // cast up-down ray and find height in world space of hit + float h = _from.z() - MWBase::Environment::get().getWorld()->getDistToNearestRayHit(_from, osg::Vec3f(0,0,-1), verticalOffset + PATHFIND_Z_REACH + 1); + + if(std::abs(from.z() - h) <= PATHFIND_Z_REACH) + return true; + } + + return false; + } } namespace MWMechanics @@ -40,7 +80,7 @@ namespace MWMechanics float mTimerCombatMove; bool mReadyToAttack; bool mAttack; - float mAttackRange; + bool mFollowTarget; bool mCombatMove; osg::Vec3f mLastTargetPos; const MWWorld::CellStore* mCell; @@ -49,15 +89,16 @@ namespace MWMechanics float mStrength; bool mForceNoShortcut; ESM::Position mShortcutFailPos; + osg::Vec3f mLastActorPos; MWMechanics::Movement mMovement; AiCombatStorage(): mAttackCooldown(0), - mTimerReact(AI_REACTION_TIME), + mTimerReact(0), mTimerCombatMove(0), mReadyToAttack(false), mAttack(false), - mAttackRange(0), + mFollowTarget(false), mCombatMove(false), mLastTargetPos(0,0,0), mCell(NULL), @@ -66,8 +107,8 @@ namespace MWMechanics mStrength(), mForceNoShortcut(false), mShortcutFailPos(), - mMovement() - {} + mLastActorPos(0,0,0), + mMovement(){} void startCombatMove(bool isNpc, bool isDistantCombat, float distToTarget, float rangeAttack); void updateCombatMove(float duration); @@ -138,7 +179,6 @@ 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 @@ -157,38 +197,34 @@ namespace MWMechanics || target.getClass().getCreatureStats(target).isDead()) return true; - if (storage.mCurrentAction.get()) // need to wait to init action with it's attack range - { - //Update every frame - bool is_target_reached = pathTo(actor, target.getRefData().getPosition().pos, duration, storage.mAttackRange); - if (is_target_reached) storage.mReadyToAttack = true; - } - + //Update every frame storage.updateCombatMove(duration); - if (storage.mReadyToAttack) updateActorsMovement(actor, duration, storage); + updateActorsMovement(actor, duration, storage.mMovement); storage.updateAttack(characterController); storage.mActionCooldown -= duration; - + float& timerReact = storage.mTimerReact; - if (timerReact < AI_REACTION_TIME) + if(timerReact < REACTION_INTERVAL) { timerReact += duration; + return false; } else { timerReact = 0; - attack(actor, target, storage, characterController); + return reactionTimeActions(actor, characterController, storage, target); } - - return false; } - void AiCombat::attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController) + bool AiCombat::reactionTimeActions(const MWWorld::Ptr& actor, CharacterController& characterController, + AiCombatStorage& storage, MWWorld::Ptr target) { + MWMechanics::Movement& movement = storage.mMovement; + if (isTargetMagicallyHidden(target)) { storage.stopAttack(); - return; // TODO: run away instead of doing nothing + return false; // TODO: run away instead of doing nothing } const MWWorld::CellStore*& currentCell = storage.mCell; @@ -203,9 +239,10 @@ namespace MWMechanics float& actionCooldown = storage.mActionCooldown; if (actionCooldown > 0) - return; + return false; - float &rangeAttack = storage.mAttackRange; + float rangeAttack = 0; + float rangeFollow = 0; boost::shared_ptr& currentAction = storage.mCurrentAction; if (characterController.readyToPrepareAttack()) { @@ -213,14 +250,97 @@ namespace MWMechanics actionCooldown = currentAction->getActionCooldown(); } - const ESM::Weapon *weapon = NULL; - bool isRangedCombat = false; 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(); + static const float fCombatDistance = world->getStore().get().find("fCombatDistance")->getFloat(); + if (actorClass.hasInventoryStore(actor)) { - rangeAttack = currentAction->getCombatRange(isRangedCombat); - // Get weapon characteristics - weapon = currentAction->getWeapon(); + //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 *= fCombatDistance; } + else //is creature + { + weaptype = actorClass.getCreatureStats(actor).getDrawState() == DrawState_Spell ? WeapType_Spell : WeapType_HandToHand; + weapRange = fCombatDistance; + } + + bool distantCombat = false; + if (weaptype != WeapType_Spell) + { + // TODO: move to ActionWeapon + 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); + } + + + 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()); @@ -228,52 +348,155 @@ 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); + 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)) * REACTION_INTERVAL / 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 + if (distToTarget >= rangeAttack && !actorClass.isNpc() && !MWMechanics::isEnvironmentCompatible(actor, target)) { // TODO: start fleeing? storage.stopAttack(); - return; + return false; } - if (storage.mReadyToAttack) + // 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))) { - storage.startCombatMove(actorClass.isNpc(), isRangedCombat, distToTarget, rangeAttack); - // start new attack - storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat); - - if (isRangedCombat) + 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 + 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, (weapon ? weapon->mData.mType : 0), storage.mStrength); + vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, REACTION_INTERVAL, weaptype, + storage.mStrength); lastTargetPos = vTargetPos; - - storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir); - storage.mMovement.mRotation[2] = getZAngleToDir(vAimDir); + movement.mRotation[0] = getXAngleToDir(vAimDir); + movement.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 + 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 = REACTION_INTERVAL * 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; + movement.mRotation[0] = getXAngleToDir(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; + } + + return false; } - void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage) + void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, float duration, MWMechanics::Movement& desiredMovement) { - // apply combat movement MWMechanics::Movement& actorMovementSettings = actor.getClass().getMovementSettings(actor); - actorMovementSettings.mPosition[0] = storage.mMovement.mPosition[0]; - actorMovementSettings.mPosition[1] = storage.mMovement.mPosition[1]; - actorMovementSettings.mPosition[2] = storage.mMovement.mPosition[2]; - - rotateActorOnAxis(actor, 2, actorMovementSettings, storage.mMovement); - rotateActorOnAxis(actor, 0, actorMovementSettings, storage.mMovement); + 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 = desiredMovement; + rotateActorOnAxis(actor, 2, actorMovementSettings, desiredMovement); + rotateActorOnAxis(actor, 0, actorMovementSettings, desiredMovement); + } } void AiCombat::rotateActorOnAxis(const MWWorld::Ptr& actor, int axis, @@ -291,6 +514,35 @@ 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(), false); + } + } + int AiCombat::getTypeId() const { return TypeIdCombat; @@ -330,13 +582,13 @@ namespace MWMechanics mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); mCombatMove = true; } - // dodge movements (for NPCs only) + // only NPCs are smart enough to use dodge movements 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; // to the left/right + mMovement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f; mTimerCombatMove = 0.05f + 0.15f * Misc::Rng::rollClosedProbability(); mCombatMove = true; } @@ -399,7 +651,7 @@ namespace MWMechanics mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(), baseDelay + 0.9); } else - mAttackCooldown -= AI_REACTION_TIME; + mAttackCooldown -= REACTION_INTERVAL; } } diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index 4be2ac9da..1cfac5806 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -55,14 +55,19 @@ namespace MWMechanics virtual bool canCancel() const { return false; } virtual bool shouldCancelPreviousAi() const { return false; } + protected: + virtual bool doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell); + private: int mTargetActorId; - void attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController); + void buildNewPath(const MWWorld::Ptr& actor, const MWWorld::Ptr& target); + bool reactionTimeActions(const MWWorld::Ptr& actor, CharacterController& characterController, + AiCombatStorage& storage, MWWorld::Ptr target); /// Transfer desired movement (from AiCombatStorage) to Actor - void updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage); + void updateActorsMovement(const MWWorld::Ptr& actor, float duration, MWMechanics::Movement& movement); void rotateActorOnAxis(const MWWorld::Ptr& actor, int axis, MWMechanics::Movement& actorMovementSettings, MWMechanics::Movement& desiredMovement); }; diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index a70410035..39c11c678 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -40,21 +40,23 @@ int getRangeTypes (const ESM::EffectList& effects) return types; } -float suggestCombatRange(int rangeTypes) +void suggestCombatRange(int rangeTypes, float& rangeAttack, float& rangeFollow) { if (rangeTypes & Touch) { - static const float fCombatDistance = MWBase::Environment::get().getWorld()->getStore().get().find("fCombatDistance")->getFloat(); - return fCombatDistance; + rangeAttack = 100.f; + rangeFollow = 300.f; } else if (rangeTypes & Target) { - return 1000.f; + rangeAttack = 1000.f; + rangeFollow = 0.f; } else { // For Self spells, distance doesn't matter, so back away slightly to avoid enemy hits - return 600.f; + rangeAttack = 600.f; + rangeFollow = 0.f; } } @@ -425,13 +427,11 @@ namespace MWMechanics } } - float ActionSpell::getCombatRange (bool& isRanged) const + void ActionSpell::getCombatRange(float& rangeAttack, float& rangeFollow) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(mSpellId); int types = getRangeTypes(spell->mEffects); - - isRanged = (types & Target); - return suggestCombatRange(types); + suggestCombatRange(types, rangeAttack, rangeFollow); } void ActionEnchantedItem::prepare(const MWWorld::Ptr &actor) @@ -441,17 +441,18 @@ namespace MWMechanics actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Spell); } - float ActionEnchantedItem::getCombatRange(bool& isRanged) const + void ActionEnchantedItem::getCombatRange(float& rangeAttack, float& rangeFollow) { const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(mItem->getClass().getEnchantment(*mItem)); int types = getRangeTypes(enchantment->mEffects); - return suggestCombatRange(types); + suggestCombatRange(types, rangeAttack, rangeFollow); } - float ActionPotion::getCombatRange(bool& isRanged) const + void ActionPotion::getCombatRange(float& rangeAttack, float& rangeFollow) { // distance doesn't matter, so back away slightly to avoid enemy hits - return 600.f; + rangeAttack = 600.f; + rangeFollow = 0.f; } void ActionPotion::prepare(const MWWorld::Ptr &actor) @@ -462,8 +463,6 @@ namespace MWMechanics void ActionWeapon::prepare(const MWWorld::Ptr &actor) { - mIsNpc = actor.getClass().isNpc(); - if (actor.getClass().hasInventoryStore(actor)) { if (mWeapon.isEmpty()) @@ -483,43 +482,9 @@ namespace MWMechanics actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Weapon); } - float ActionWeapon::getCombatRange(bool& isRanged) const + void ActionWeapon::getCombatRange(float& rangeAttack, float& rangeFollow) { - isRanged = false; - - static const float fCombatDistance = MWBase::Environment::get().getWorld()->getStore().get().find("fCombatDistance")->getFloat(); - - if (mWeapon.isEmpty()) - { - if (!mIsNpc) - { - return fCombatDistance; - } - else - { - static float fHandToHandReach = - MWBase::Environment::get().getWorld()->getStore().get().find("fHandToHandReach")->getFloat(); - - return fHandToHandReach * fCombatDistance; - } - } - - const ESM::Weapon* weapon = mWeapon.get()->mBase; - - if (weapon->mData.mType >= ESM::Weapon::MarksmanBow) - { - isRanged = true; - return 1000.f; - } - else - return weapon->mData.mReach * fCombatDistance; - } - - const ESM::Weapon* ActionWeapon::getWeapon() const - { - if (mWeapon.isEmpty()) - return NULL; - return mWeapon.get()->mBase; + // Already done in AiCombat itself } boost::shared_ptr prepareNextAction(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy) diff --git a/apps/openmw/mwmechanics/aicombataction.hpp b/apps/openmw/mwmechanics/aicombataction.hpp index e4ce44346..bc635ceb2 100644 --- a/apps/openmw/mwmechanics/aicombataction.hpp +++ b/apps/openmw/mwmechanics/aicombataction.hpp @@ -16,9 +16,8 @@ namespace MWMechanics public: virtual ~Action() {} virtual void prepare(const MWWorld::Ptr& actor) = 0; - virtual float getCombatRange (bool& isRanged) const = 0; + virtual void getCombatRange (float& rangeAttack, float& rangeFollow) = 0; virtual float getActionCooldown() { return 0.f; } - virtual const ESM::Weapon* getWeapon() const { return NULL; }; }; class ActionSpell : public Action @@ -29,7 +28,7 @@ namespace MWMechanics /// Sets the given spell as selected on the actor's spell list. virtual void prepare(const MWWorld::Ptr& actor); - virtual float getCombatRange (bool& isRanged) const; + virtual void getCombatRange (float& rangeAttack, float& rangeFollow); }; class ActionEnchantedItem : public Action @@ -39,7 +38,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 float getCombatRange (bool& isRanged) const; + virtual void getCombatRange (float& rangeAttack, float& rangeFollow); /// Since this action has no animation, apply a small cool down for using it virtual float getActionCooldown() { return 1.f; } @@ -52,7 +51,7 @@ namespace MWMechanics MWWorld::Ptr mPotion; /// Drinks the given potion. virtual void prepare(const MWWorld::Ptr& actor); - virtual float getCombatRange (bool& isRanged) const; + virtual void getCombatRange (float& rangeAttack, float& rangeFollow); /// Since this action has no animation, apply a small cool down for using it virtual float getActionCooldown() { return 1.f; } @@ -63,7 +62,6 @@ namespace MWMechanics private: MWWorld::Ptr mAmmunition; MWWorld::Ptr mWeapon; - bool mIsNpc; public: /// \a weapon may be empty for hand-to-hand combat @@ -71,8 +69,7 @@ namespace MWMechanics : mAmmunition(ammo), mWeapon(weapon) {} /// Equips the given weapon. virtual void prepare(const MWWorld::Ptr& actor); - virtual float getCombatRange (bool& isRanged) const; - virtual const ESM::Weapon* getWeapon() const; + virtual void getCombatRange (float& rangeAttack, float& rangeFollow); }; float rateSpell (const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index 196498bad..1b843f850 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -137,24 +137,35 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte //Set the target destination from the actor ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; - if (!storage.mMoving) + float dist = distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]); + + if (storage.mMoving) //Stop when you get close + storage.mMoving = (dist > followDistance); + else { - const float threshold = 10; // to avoid constant switching between moving/stopping - followDistance += threshold; + const float threshold = 10; + storage.mMoving = (dist > followDistance + threshold); } - storage.mMoving = !pathTo(actor, dest, duration, followDistance); // Go to the destination - - if (storage.mMoving) + if(!storage.mMoving) { - //Check if you're far away - float dist = distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]); + actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - 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 + // 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, std::atan2(directionX,directionY), osg::DegreesToRadians(5.f)); } + else + { + pathTo(actor, dest, duration); //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 return false; } diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 1131b5e6f..34cf9b921 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -21,13 +21,6 @@ MWMechanics::AiPackage::~AiPackage() {} -MWMechanics::AiPackage::AiPackage() : - mTimer(AI_REACTION_TIME + 1.0f), // to force initial pathbuild - mIsShortcutting(false), - mShortcutProhibited(false), mShortcutFailPos() -{ -} - MWWorld::Ptr MWMechanics::AiPackage::getTarget() const { return MWWorld::Ptr(); @@ -58,20 +51,14 @@ 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(); +MWMechanics::AiPackage::AiPackage() : mTimer(0.26f) { //mTimer starts at .26 to force initial pathbuild - mPathFinder.clearPath(); - mObstacleCheck.clear(); } -bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest, float duration, float destTolerance) + +bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Point dest, float duration) { + //Update various Timers mTimer += duration; //Update timer ESM::Position pos = actor.getRefData().getPosition(); //position of the actor @@ -86,72 +73,42 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgr return false; } - // handle path building and shortcutting - ESM::Pathgrid::Point start = pos.pos; - - float distToTarget = distance(start, dest); - bool isDestReached = (distToTarget <= destTolerance); - - if (!isDestReached && mTimer > AI_REACTION_TIME) + //*********************** + /// 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 + //*********************** + if(mTimer > 0.25) { - bool wasShortcutting = mIsShortcutting; - bool destInLOS = false; - if (getTypeId() != TypeIdWander) // prohibit shortcuts for AiWander - mIsShortcutting = shortcutPath(start, dest, actor, &destInLOS); // try to shortcut first + const ESM::Cell *cell = actor.getCell()->getCell(); + if (doesPathNeedRecalc(dest, cell)) { //Only rebuild path if it's moved + mPathFinder.buildSyncedPath(pos.pos, dest, actor.getCell(), true); //Rebuild path, in case the target has moved + mPrevDest = dest; + } - if (!mIsShortcutting) + if(!mPathFinder.getPath().empty()) //Path has points in it { - if (wasShortcutting || doesPathNeedRecalc(dest)) // if need to rebuild path - { - mPathFinder.buildSyncedPath(start, dest, actor.getCell()); + ESM::Pathgrid::Point lastPos = mPathFinder.getPath().back(); //Get the end of the proposed path - // 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 - { - 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; } - if (isDestReached || mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1])) // if path is finished + //************************ + /// Checks if you aren't moving; attempts to unstick you + //************************ + if(mPathFinder.checkPathCompleted(pos.pos[0],pos.pos[1])) //Path finished? { - // turn to destination point - zTurn(actor, getZAngleToPoint(start, dest)); - smoothTurn(actor, getXAngleToPoint(start, dest), 0); + // Reset mTimer so that path will be built right away when a package is repeated + mTimer = 0.26f; return true; } else { - actor.getClass().getMovementSettings(actor).mPosition[1] = 1; // run to target - // handle obstacles on the way evadeObstacles(actor, duration, pos); } - - // turn to next path point by X,Z axes - zTurn(actor, mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1])); - smoothTurn(actor, mPathFinder.getXAngleToNext(pos.pos[0], pos.pos[1], pos.pos[2]), 0); - return false; } @@ -160,106 +117,30 @@ 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); - - // 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()) + if (mObstacleCheck.check(actor, duration)) { - // 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) + // first check if we're walking into a door + MWWorld::Ptr door = getNearbyDoor(actor); + if (door != MWWorld::Ptr()) // NOTE: checks interior cells only { - MWBase::Environment::get().getWorld()->activateDoor(door, 1); + 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); } } - else // any other obstacle (NPC, crate, etc.) - { - mObstacleCheck.takeEvasiveAction(movement); + else { //Not stuck, so reset things + movement.mPosition[1] = 1; //Just run forward } } -bool MWMechanics::AiPackage::shortcutPath(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor, bool *destInLOS) +bool MWMechanics::AiPackage::doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell) { - 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::MakeOsgVec3(mShortcutFailPos) - PathFinder::MakeOsgVec3(startPoint)).length() >= PATHFIND_SHORTCUT_RETRY_DIST)) - { - // 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)); - - 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_RADIANS * 2; // *2 - for reliability - osg::Vec3f::value_type distToTarget = osg::Vec3f(static_cast(endPoint.mX), static_cast(endPoint.mY), 0).length(); - - float offsetXY = distToTarget > maxAvoidDist*1.5? maxAvoidDist : maxAvoidDist/2; - - bool isClear = checkWayIsClear(PathFinder::MakeOsgVec3(startPoint), PathFinder::MakeOsgVec3(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(const ESM::Pathgrid::Point& newDest) -{ - return mPathFinder.getPath().empty() || (distance(mPathFinder.getPath().back(), newDest) > 10); + return mPathFinder.getPath().empty() || (distance(mPrevDest, dest) > 10); } bool MWMechanics::AiPackage::isTargetMagicallyHidden(const MWWorld::Ptr& target) diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 4feb13fe0..637d4f066 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -24,7 +24,6 @@ namespace ESM namespace MWMechanics { - const float AI_REACTION_TIME = 0.25f; class CharacterController; @@ -92,26 +91,14 @@ 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: - /// Handles path building and shortcutting with obstacles avoiding + /// 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, const ESM::Pathgrid::Point& dest, float duration, float destTolerance = 0.0f); + bool pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Point dest, float duration); - /// 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(const ESM::Pathgrid::Point& newDest); + virtual bool doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell); void evadeObstacles(const MWWorld::Ptr& actor, float duration, const ESM::Position& pos); @@ -121,14 +108,11 @@ namespace MWMechanics float mTimer; - 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 + ESM::Pathgrid::Point mPrevDest; private: bool isNearInactiveCell(const ESM::Position& actorPos); + }; } diff --git a/apps/openmw/mwmechanics/aipursue.cpp b/apps/openmw/mwmechanics/aipursue.cpp index 2b218de03..be16f49a2 100644 --- a/apps/openmw/mwmechanics/aipursue.cpp +++ b/apps/openmw/mwmechanics/aipursue.cpp @@ -33,6 +33,7 @@ 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 @@ -51,10 +52,14 @@ bool AiPursue::execute (const MWWorld::Ptr& actor, CharacterController& characte //Set the target desition from the actor ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; - if (pathTo(actor, dest, duration, 100)) { - target.getClass().activate(target,actor).get()->execute(actor); //Arrest player when reached + 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 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/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index b03586c3b..f05725cc2 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -234,7 +234,6 @@ 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 diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 1ec9444aa..87fb3b9b7 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -29,6 +29,7 @@ namespace MWMechanics { static const int COUNT_BEFORE_RESET = 10; static const float DOOR_CHECK_INTERVAL = 1.5f; + static const float REACTION_INTERVAL = 0.25f; static const int GREETING_SHOULD_START = 4; //how many reaction intervals should pass before NPC can greet player static const int GREETING_SHOULD_END = 10; @@ -73,6 +74,8 @@ namespace MWMechanics unsigned short mIdleAnimation; std::vector mBadIdles; // Idle animations that when called cause errors + PathFinder mPathFinder; + // do we need to calculate allowed nodes based on mDistance bool mPopulateAvailableNodes; @@ -83,6 +86,8 @@ namespace MWMechanics ESM::Pathgrid::Point mCurrentNode; bool mTrimCurrentNode; + ObstacleCheck mObstacleCheck; + float mDoorCheckDuration; int mStuckCount; @@ -113,7 +118,7 @@ namespace MWMechanics AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat): mDistance(distance), mDuration(duration), mRemainingDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle), - mRepeat(repeat), mStoredInitialActorPosition(false), mIsWanderDestReady(false) + mRepeat(repeat), mStoredInitialActorPosition(false) { mIdle.resize(8, 0); init(); @@ -218,7 +223,7 @@ namespace MWMechanics float& lastReaction = storage.mReaction; lastReaction += duration; - if (AI_REACTION_TIME <= lastReaction) + if (REACTION_INTERVAL <= lastReaction) { lastReaction = 0; return reactionTimeActions(actor, storage, currentCell, cellChange, pos, duration); @@ -268,7 +273,7 @@ namespace MWMechanics } // If Wandering manually and hit an obstacle, stop - if (storage.mIsWanderingManually && mObstacleCheck.check(actor, duration, 2.0f)) { + if (storage.mIsWanderingManually && storage.mObstacleCheck.check(actor, duration, 2.0f)) { completeManualWalking(actor, storage); } @@ -295,14 +300,14 @@ namespace MWMechanics if ((wanderState == Wander_MoveNow) && storage.mCanWanderAlongPathGrid) { // Construct a new path if there isn't one - if(!mPathFinder.isPathConstructed()) + if(!storage.mPathFinder.isPathConstructed()) { if (!storage.mAllowedNodes.empty()) { setPathToAnAllowedNode(actor, storage, pos); } } - } else if (storage.mIsWanderingManually && mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], DESTINATION_TOLERANCE)) { + } else if (storage.mIsWanderingManually && storage.mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], DESTINATION_TOLERANCE)) { completeManualWalking(actor, storage); } @@ -332,7 +337,7 @@ namespace MWMechanics void AiWander::returnToStartLocation(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos) { - if (!mPathFinder.isPathConstructed()) + if (!storage.mPathFinder.isPathConstructed()) { ESM::Pathgrid::Point dest(PathFinder::MakePathgridPoint(mReturnPosition)); @@ -340,11 +345,10 @@ namespace MWMechanics ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos)); // don't take shortcuts for wandering - mPathFinder.buildSyncedPath(start, dest, actor.getCell()); + storage.mPathFinder.buildSyncedPath(start, dest, actor.getCell(), false); - if (mPathFinder.isPathConstructed()) + if (storage.mPathFinder.isPathConstructed()) { - mIsWanderDestReady = true; storage.setState(Wander_Walking); } } @@ -375,14 +379,9 @@ namespace MWMechanics // Check if land creature will walk onto water or if water creature will swim onto land if ((!isWaterCreature && !destinationIsAtWater(actor, destination)) || (isWaterCreature && !destinationThroughGround(currentPositionVec3f, destination))) { - mPathFinder.buildSyncedPath(currentPosition, destinationPosition, actor.getCell()); - mPathFinder.addPointToPath(destinationPosition); - - if (mPathFinder.isPathConstructed()) - { - mIsWanderDestReady = true; - storage.setState(Wander_Walking, true); - } + storage.mPathFinder.buildSyncedPath(currentPosition, destinationPosition, actor.getCell(), true); + storage.mPathFinder.addPointToPath(destinationPosition); + storage.setState(Wander_Walking, true); return; } } while (--attempts); @@ -408,7 +407,7 @@ namespace MWMechanics void AiWander::completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage) { stopWalking(actor, storage); - mObstacleCheck.clear(); + storage.mObstacleCheck.clear(); storage.setState(Wander_IdleNow); } @@ -476,7 +475,7 @@ namespace MWMechanics float duration, AiWanderStorage& storage, ESM::Position& pos) { // Are we there yet? - if (mIsWanderDestReady && pathTo(actor, mPathFinder.getPath().back(), duration, DESTINATION_TOLERANCE)) + if (storage.mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], DESTINATION_TOLERANCE)) { stopWalking(actor, storage); storage.setState(Wander_ChooseAction); @@ -518,27 +517,40 @@ namespace MWMechanics void AiWander::evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage, float duration, ESM::Position& pos) { - if (mObstacleCheck.isEvading()) + // 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 (storage.mObstacleCheck.check(actor, duration)) { // 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 storage.mTrimCurrentNode = true; - trimAllowedNodes(storage.mAllowedNodes, mPathFinder); - mObstacleCheck.clear(); - mPathFinder.clearPath(); + trimAllowedNodes(storage.mAllowedNodes, storage.mPathFinder); + storage.mObstacleCheck.clear(); + storage.mPathFinder.clearPath(); storage.setState(Wander_MoveNow); } - - storage.mStuckCount++; // TODO: maybe no longer needed + else // probably walking into another NPC + { + // TODO: diagonal should have same animation as walk forward + // but doesn't seem to do that? + storage.mObstacleCheck.takeEvasiveAction(movement); + } + storage.mStuckCount++; // TODO: maybe no longer needed + } + else + { + movement.mPosition[1] = 1; } // if stuck for sufficiently long, act like current location was the destination if (storage.mStuckCount >= COUNT_BEFORE_RESET) // something has gone wrong, reset { //std::cout << "Reset \""<< cls.getName(actor) << "\"" << std::endl; - mObstacleCheck.clear(); + storage.mObstacleCheck.clear(); stopWalking(actor, storage); storage.setState(Wander_ChooseAction); @@ -615,7 +627,7 @@ namespace MWMechanics if (storage.mState == Wander_Walking) { stopWalking(actor, storage); - mObstacleCheck.clear(); + storage.mObstacleCheck.clear(); storage.setState(Wander_IdleNow); } @@ -655,12 +667,10 @@ namespace MWMechanics ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(actorPos)); // don't take shortcuts for wandering - mPathFinder.buildSyncedPath(start, dest, actor.getCell()); + storage.mPathFinder.buildSyncedPath(start, dest, actor.getCell(), false); - if (mPathFinder.isPathConstructed()) + if (storage.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 = storage.mAllowedNodes[randNode]; storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); @@ -716,8 +726,7 @@ namespace MWMechanics void AiWander::stopWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage) { - mPathFinder.clearPath(); - mIsWanderDestReady = false; + storage.mPathFinder.clearPath(); actor.getClass().getMovementSettings(actor).mPosition[1] = 0; } @@ -950,7 +959,6 @@ namespace MWMechanics , mTimeOfDay(wander->mData.mTimeOfDay) , mRepeat(wander->mData.mShouldRepeat != 0) , mStoredInitialActorPosition(wander->mStoredInitialActorPosition) - , mIsWanderDestReady(false) { if (mStoredInitialActorPosition) mInitialActorPosition = wander->mInitialActorPosition; diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index a46b3cbad..64a54a66b 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -108,10 +108,8 @@ namespace MWMechanics osg::Vec3f mInitialActorPosition; bool mStoredInitialActorPosition; - bool mIsWanderDestReady; - void getAllowedNodes(const MWWorld::Ptr& actor, const ESM::Cell* cell, AiWanderStorage& storage); - + void trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder); // constants for converting idleSelect values into groupNames diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp index 5d99fe723..4394168a5 100644 --- a/apps/openmw/mwmechanics/obstacle.cpp +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -93,11 +93,6 @@ 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 d2e1cfc2e..c8c83d68f 100644 --- a/apps/openmw/mwmechanics/obstacle.hpp +++ b/apps/openmw/mwmechanics/obstacle.hpp @@ -33,7 +33,6 @@ namespace MWMechanics void clear(); bool isNormalState() const; - bool isEvading() const; // Returns true if there is an obstacle and an evasive action // should be taken diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index ef60a85a4..51127de2a 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -82,42 +82,6 @@ namespace MWMechanics return sqrt(x * x + y * y + z * z); } - float getZAngleToDir(const osg::Vec3f& dir) - { - return std::atan2(dir.x(), dir.y()); - } - - float getXAngleToDir(const osg::Vec3f& dir) - { - return -std::asin(dir.z() / dir.length()); - } - - float getZAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest) - { - osg::Vec3f dir = PathFinder::MakeOsgVec3(dest) - PathFinder::MakeOsgVec3(origin); - return getZAngleToDir(dir); - } - - float getXAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest) - { - osg::Vec3f dir = PathFinder::MakeOsgVec3(dest) - PathFinder::MakeOsgVec3(origin); - return getXAngleToDir(dir); - } - - bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY) - { - 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 of hit in world space - float h = _from.z() - MWBase::Environment::get().getWorld()->getDistToNearestRayHit(_from, -osg::Z_AXIS, verticalOffset + PATHFIND_Z_REACH + 1); - - return (std::abs(from.z() - h) <= PATHFIND_Z_REACH); - } - PathFinder::PathFinder() : mPathgrid(NULL), mCell(NULL) @@ -168,10 +132,23 @@ namespace MWMechanics */ void PathFinder::buildPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint, - const MWWorld::CellStore* cell) + const MWWorld::CellStore* cell, + bool allowShortcuts) { 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; @@ -266,19 +243,6 @@ namespace MWMechanics return std::atan2(directionX, directionY); } - 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(); - osg::Vec3f dir = MakeOsgVec3(nextPoint) - osg::Vec3f(x,y,z); - - return -std::asin(dir.z() / dir.length()); - } - bool PathFinder::checkPathCompleted(float x, float y, float tolerance) { if(mPath.empty()) @@ -300,18 +264,19 @@ namespace MWMechanics // see header for the rationale void PathFinder::buildSyncedPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint, - const MWWorld::CellStore* cell) + const MWWorld::CellStore* cell, + bool allowShortcuts) { 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); + buildPath(startPoint, endPoint, cell, allowShortcuts); } else { const ESM::Pathgrid::Point oldStart(*getPath().begin()); - buildPath(startPoint, endPoint, cell); + buildPath(startPoint, endPoint, cell, allowShortcuts); 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 64608979b..00a8fe0e4 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -16,20 +16,6 @@ namespace MWMechanics { float distance(const ESM::Pathgrid::Point& point, float x, float y, float); float distance(const ESM::Pathgrid::Point& a, const ESM::Pathgrid::Point& b); - float getZAngleToDir(const osg::Vec3f& dir); - float getXAngleToDir(const osg::Vec3f& dir); - 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 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 osg::Vec3f& from, const osg::Vec3f& to, float offsetXY); - class PathFinder { public: @@ -53,17 +39,12 @@ namespace MWMechanics void clearPath(); - void buildPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint, - const MWWorld::CellStore* cell); - bool checkPathCompleted(float x, float y, float tolerance = PathTolerance); ///< \Returns true if we are within \a tolerance units of the last path point. /// In radians float getZAngleToNext(float x, float y) const; - float getXAngleToNext(float x, float y, float z) const; - bool isPathConstructed() const { return !mPath.empty(); @@ -87,9 +68,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); + const MWWorld::CellStore* cell, bool allowShortcuts = true); - void addPointToPath(const ESM::Pathgrid::Point &point) + void addPointToPath(ESM::Pathgrid::Point &point) { mPath.push_back(point); } @@ -149,6 +130,9 @@ namespace MWMechanics } private: + void buildPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint, + const MWWorld::CellStore* cell, bool allowShortcuts = true); + std::list mPath; const ESM::Pathgrid *mPathgrid;