diff --git a/apps/openmw/mwmechanics/aiactivate.cpp b/apps/openmw/mwmechanics/aiactivate.cpp index a79adbc8b..745a01c8b 100644 --- a/apps/openmw/mwmechanics/aiactivate.cpp +++ b/apps/openmw/mwmechanics/aiactivate.cpp @@ -23,33 +23,29 @@ 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 be checking whether the target is currently registered + if (target == MWWorld::Ptr() || + !target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should check whether the target is currently registered // with the MechanicsManager - ) - return true; //Target doesn't exist + ) + 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]) < MWBase::Environment::get().getWorld()->getMaxActivationDistance()) { //Stop when you get in activation range - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; + if (pathTo(actor, dest, duration, MWBase::Environment::get().getWorld()->getMaxActivationDistance())) //Stop when you get in activation range + { + // 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 ce66b045a..f5c64d4ab 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -27,46 +27,6 @@ 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 @@ -80,7 +40,7 @@ namespace MWMechanics float mTimerCombatMove; bool mReadyToAttack; bool mAttack; - bool mFollowTarget; + float mAttackRange; bool mCombatMove; osg::Vec3f mLastTargetPos; const MWWorld::CellStore* mCell; @@ -89,16 +49,15 @@ namespace MWMechanics float mStrength; bool mForceNoShortcut; ESM::Position mShortcutFailPos; - osg::Vec3f mLastActorPos; MWMechanics::Movement mMovement; AiCombatStorage(): mAttackCooldown(0), - mTimerReact(0), + mTimerReact(AI_REACTION_TIME), mTimerCombatMove(0), mReadyToAttack(false), mAttack(false), - mFollowTarget(false), + mAttackRange(0), mCombatMove(false), mLastTargetPos(0,0,0), mCell(NULL), @@ -107,8 +66,8 @@ namespace MWMechanics mStrength(), mForceNoShortcut(false), mShortcutFailPos(), - mLastActorPos(0,0,0), - mMovement(){} + mMovement() + {} void startCombatMove(bool isNpc, bool isDistantCombat, float distToTarget, float rangeAttack); void updateCombatMove(float duration); @@ -179,6 +138,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 @@ -197,34 +157,38 @@ namespace MWMechanics || target.getClass().getCreatureStats(target).isDead()) return true; - //Update every frame + 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; + } + 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 < REACTION_INTERVAL) + 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; @@ -239,10 +203,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()) { @@ -250,253 +213,67 @@ namespace MWMechanics actionCooldown = currentAction->getActionCooldown(); } - 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)) - { - //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 + bool isRangedCombat = false; + if (currentAction.get()) { - distantCombat = (rangeAttack > 500); + rangeAttack = currentAction->getCombatRange(isRangedCombat); + // Get weapon characteristics + weapon = currentAction->getWeapon(); } - - 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()); 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)) * 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); + 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? 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 - if (distantCombat) + storage.startCombatMove(actorClass.isNpc(), isRangedCombat, distToTarget, rangeAttack); + // start new attack + storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat); + + if (isRangedCombat) { + // rotate actor taking into account target movement direction and projectile speed osg::Vec3f& lastTargetPos = storage.mLastTargetPos; - vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, REACTION_INTERVAL, weaptype, - storage.mStrength); + vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, AI_REACTION_TIME, (weapon ? weapon->mData.mType : 0), 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; + storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir); + storage.mMovement.mRotation[2] = getZAngleToDir(vAimDir); } - 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) + else { - // to stop possible sideway moving after target moved out of attack range - storage.stopCombatMove(); - readyToAttack = false; + 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.mPosition[1] = 1; } - - 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 = desiredMovement; - rotateActorOnAxis(actor, 2, actorMovementSettings, desiredMovement); - rotateActorOnAxis(actor, 0, actorMovementSettings, desiredMovement); - } + 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); } void AiCombat::rotateActorOnAxis(const MWWorld::Ptr& actor, int axis, @@ -514,35 +291,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(), false); - } - } - int AiCombat::getTypeId() const { return TypeIdCombat; @@ -582,13 +330,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; } @@ -651,7 +399,7 @@ namespace MWMechanics mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(), baseDelay + 0.9); } else - mAttackCooldown -= REACTION_INTERVAL; + mAttackCooldown -= AI_REACTION_TIME; } } diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index 1cfac5806..4be2ac9da 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -55,19 +55,14 @@ 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 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/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index 39c11c678..a70410035 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -40,23 +40,21 @@ 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; + static const float fCombatDistance = MWBase::Environment::get().getWorld()->getStore().get().find("fCombatDistance")->getFloat(); + return fCombatDistance; } 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; } } @@ -427,11 +425,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 & Target); + return suggestCombatRange(types); } void ActionEnchantedItem::prepare(const MWWorld::Ptr &actor) @@ -441,18 +441,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) @@ -463,6 +462,8 @@ namespace MWMechanics void ActionWeapon::prepare(const MWWorld::Ptr &actor) { + mIsNpc = actor.getClass().isNpc(); + if (actor.getClass().hasInventoryStore(actor)) { if (mWeapon.isEmpty()) @@ -482,9 +483,43 @@ namespace MWMechanics actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Weapon); } - void ActionWeapon::getCombatRange(float& rangeAttack, float& rangeFollow) + float ActionWeapon::getCombatRange(bool& isRanged) const + { + 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 { - // 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 &enemy) diff --git a/apps/openmw/mwmechanics/aicombataction.hpp b/apps/openmw/mwmechanics/aicombataction.hpp index bc635ceb2..e4ce44346 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& enemy); diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index 1b843f850..196498bad 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -137,35 +137,24 @@ 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]); - - if (storage.mMoving) //Stop when you get close - storage.mMoving = (dist > followDistance); - else + if (!storage.mMoving) { - const float threshold = 10; - storage.mMoving = (dist > followDistance + threshold); + const float threshold = 10; // to avoid constant switching between moving/stopping + followDistance += threshold; } - if(!storage.mMoving) - { - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; + storage.mMoving = !pathTo(actor, dest, duration, followDistance); // Go to the destination - // 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 + if (storage.mMoving) { - pathTo(actor, dest, duration); //Go to the destination - } + //Check if you're far away + float dist = distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]); - //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 (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 34cf9b921..1131b5e6f 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -21,6 +21,13 @@ 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(); @@ -51,14 +58,20 @@ bool MWMechanics::AiPackage::getRepeat() const return false; } -MWMechanics::AiPackage::AiPackage() : mTimer(0.26f) { //mTimer starts at .26 to force initial pathbuild +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, 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 ESM::Position pos = actor.getRefData().getPosition(); //position of the actor @@ -73,42 +86,72 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Po return false; } - //*********************** - /// 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) + // 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) { - 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; - } + bool wasShortcutting = mIsShortcutting; + bool destInLOS = false; + if (getTypeId() != TypeIdWander) // prohibit shortcuts for AiWander + mIsShortcutting = shortcutPath(start, dest, actor, &destInLOS); // try to shortcut first - if(!mPathFinder.getPath().empty()) //Path has points in it + if (!mIsShortcutting) { - ESM::Pathgrid::Point lastPos = mPathFinder.getPath().back(); //Get the end of the proposed path + if (wasShortcutting || doesPathNeedRecalc(dest)) // if need to rebuild path + { + 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(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 (!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 + } } mTimer = 0; } - //************************ - /// 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])) // if path is finished { - // Reset mTimer so that path will be built right away when a package is repeated - mTimer = 0.26f; + // turn to destination point + zTurn(actor, getZAngleToPoint(start, dest)); + smoothTurn(actor, getXAngleToPoint(start, dest), 0); 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; } @@ -117,30 +160,106 @@ 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 + // 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) { - if (!door.getCellRef().getTeleport() && door.getCellRef().getTrap().empty() - && door.getCellRef().getLockLevel() <= 0 && door.getClass().getDoorState(door) == 0) { - MWBase::Environment::get().getWorld()->activateDoor(door, 1); - } + MWBase::Environment::get().getWorld()->activateDoor(door, 1); } - else // probably walking into another NPC + } + else // any other obstacle (NPC, crate, etc.) + { + mObstacleCheck.takeEvasiveAction(movement); + } +} + +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::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) { - mObstacleCheck.takeEvasiveAction(movement); + mShortcutProhibited = false; + mShortcutFailPos = ESM::Pathgrid::Point(); } } - else { //Not stuck, so reset things - movement.mPosition[1] = 1; //Just run forward + 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) +bool MWMechanics::AiPackage::doesPathNeedRecalc(const ESM::Pathgrid::Point& newDest) { - return mPathFinder.getPath().empty() || (distance(mPrevDest, dest) > 10); + return mPathFinder.getPath().empty() || (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 637d4f066..4feb13fe0 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; class CharacterController; @@ -91,14 +92,26 @@ 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: - /// 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, 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); + virtual bool doesPathNeedRecalc(const ESM::Pathgrid::Point& newDest); void evadeObstacles(const MWWorld::Ptr& actor, float duration, const ESM::Position& pos); @@ -108,11 +121,14 @@ namespace MWMechanics float mTimer; - 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 private: bool isNearInactiveCell(const ESM::Position& actorPos); - }; } diff --git a/apps/openmw/mwmechanics/aipursue.cpp b/apps/openmw/mwmechanics/aipursue.cpp index be16f49a2..2b218de03 100644 --- a/apps/openmw/mwmechanics/aipursue.cpp +++ b/apps/openmw/mwmechanics/aipursue.cpp @@ -33,7 +33,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 @@ -52,14 +51,10 @@ 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(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/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 diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index a6694c314..7afbedb12 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -29,7 +29,6 @@ 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; @@ -74,8 +73,6 @@ 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; @@ -86,8 +83,6 @@ namespace MWMechanics ESM::Pathgrid::Point mCurrentNode; bool mTrimCurrentNode; - ObstacleCheck mObstacleCheck; - float mDoorCheckDuration; int mStuckCount; @@ -118,7 +113,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) + mRepeat(repeat), mStoredInitialActorPosition(false), mIsWanderDestReady(false) { mIdle.resize(8, 0); init(); @@ -223,7 +218,7 @@ namespace MWMechanics float& lastReaction = storage.mReaction; lastReaction += duration; - if (REACTION_INTERVAL <= lastReaction) + if (AI_REACTION_TIME <= lastReaction) { lastReaction = 0; return reactionTimeActions(actor, storage, currentCell, cellChange, pos, duration); @@ -273,7 +268,7 @@ namespace MWMechanics } // If Wandering manually and hit an obstacle, stop - if (storage.mIsWanderingManually && storage.mObstacleCheck.check(actor, duration, 2.0f)) { + if (storage.mIsWanderingManually && mObstacleCheck.check(actor, duration, 2.0f)) { completeManualWalking(actor, storage); } @@ -300,14 +295,14 @@ namespace MWMechanics if ((wanderState == Wander_MoveNow) && storage.mCanWanderAlongPathGrid) { // Construct a new path if there isn't one - if(!storage.mPathFinder.isPathConstructed()) + if(!mPathFinder.isPathConstructed()) { if (!storage.mAllowedNodes.empty()) { setPathToAnAllowedNode(actor, storage, pos); } } - } else if (storage.mIsWanderingManually && storage.mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], DESTINATION_TOLERANCE)) { + } else if (storage.mIsWanderingManually && mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], DESTINATION_TOLERANCE)) { completeManualWalking(actor, storage); } @@ -337,7 +332,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)); @@ -345,10 +340,11 @@ namespace MWMechanics ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos)); // don't take shortcuts for wandering - storage.mPathFinder.buildSyncedPath(start, dest, actor.getCell(), false); + mPathFinder.buildSyncedPath(start, dest, actor.getCell()); - if (storage.mPathFinder.isPathConstructed()) + if (mPathFinder.isPathConstructed()) { + mIsWanderDestReady = true; storage.setState(Wander_Walking); } } @@ -379,9 +375,14 @@ 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))) { - storage.mPathFinder.buildSyncedPath(currentPosition, destinationPosition, actor.getCell(), true); - storage.mPathFinder.addPointToPath(destinationPosition); - storage.setState(Wander_Walking, true); + mPathFinder.buildSyncedPath(currentPosition, destinationPosition, actor.getCell()); + mPathFinder.addPointToPath(destinationPosition); + + if (mPathFinder.isPathConstructed()) + { + mIsWanderDestReady = true; + storage.setState(Wander_Walking, true); + } return; } } while (--attempts); @@ -407,7 +408,7 @@ namespace MWMechanics void AiWander::completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage) { stopWalking(actor, storage); - storage.mObstacleCheck.clear(); + mObstacleCheck.clear(); storage.setState(Wander_IdleNow); } @@ -475,7 +476,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.setState(Wander_ChooseAction); @@ -517,40 +518,27 @@ 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 (storage.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 storage.mTrimCurrentNode = true; - trimAllowedNodes(storage.mAllowedNodes, storage.mPathFinder); - storage.mObstacleCheck.clear(); - storage.mPathFinder.clearPath(); + trimAllowedNodes(storage.mAllowedNodes, mPathFinder); + mObstacleCheck.clear(); + mPathFinder.clearPath(); storage.setState(Wander_MoveNow); } - 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; + + storage.mStuckCount++; // TODO: maybe no longer needed } // 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; - storage.mObstacleCheck.clear(); + mObstacleCheck.clear(); stopWalking(actor, storage); storage.setState(Wander_ChooseAction); @@ -627,7 +615,7 @@ namespace MWMechanics if (storage.mState == Wander_Walking) { stopWalking(actor, storage); - storage.mObstacleCheck.clear(); + mObstacleCheck.clear(); storage.setState(Wander_IdleNow); } @@ -667,10 +655,12 @@ namespace MWMechanics ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(actorPos)); // don't take shortcuts for wandering - storage.mPathFinder.buildSyncedPath(start, dest, actor.getCell(), false); + 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 = storage.mAllowedNodes[randNode]; storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); @@ -726,7 +716,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; } @@ -962,6 +953,7 @@ 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 64a54a66b..a46b3cbad 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -108,8 +108,10 @@ 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 4394168a5..5d99fe723 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 c8c83d68f..d2e1cfc2e 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 diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 51127de2a..ef60a85a4 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -82,6 +82,42 @@ 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) @@ -132,23 +168,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; @@ -243,6 +266,19 @@ 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()) @@ -264,19 +300,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 00a8fe0e4..64608979b 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -16,6 +16,20 @@ 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: @@ -39,12 +53,17 @@ 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(); @@ -68,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); } @@ -130,9 +149,6 @@ 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;