From d3be725ee72f157afe14524e7955b885b46a4499 Mon Sep 17 00:00:00 2001 From: cc9cii Date: Fri, 18 Apr 2014 09:03:36 +1000 Subject: [PATCH] Actors are moved on if idling near a closed interior door. Unreachable pathgrid points due to a closed door are removed from the allowed set of points. --- apps/openmw/CMakeLists.txt | 2 +- apps/openmw/mwmechanics/aiwander.cpp | 417 ++++++++++++++++----------- apps/openmw/mwmechanics/aiwander.hpp | 39 +-- apps/openmw/mwmechanics/obstacle.cpp | 176 +++++++++++ apps/openmw/mwmechanics/obstacle.hpp | 52 ++++ 5 files changed, 488 insertions(+), 198 deletions(-) create mode 100644 apps/openmw/mwmechanics/obstacle.cpp create mode 100644 apps/openmw/mwmechanics/obstacle.hpp diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 9fabb2080..e83ae2d8d 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -69,7 +69,7 @@ add_openmw_dir (mwmechanics mechanicsmanagerimp stat character creaturestats magiceffects movement actors objects drawstate spells activespells npcstats aipackage aisequence aipersue alchemy aiwander aitravel aifollow aiescort aiactivate aicombat repair enchanting pathfinding pathgrid security spellsuccess spellcasting - disease pickpocket levelledlist combat steering + disease pickpocket levelledlist combat steering obstacle ) add_openmw_dir (mwstate diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index c50506c75..029be2fb8 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -17,11 +17,8 @@ namespace MWMechanics { - // NOTE: determined empirically but probably need further tweaking - static const int COUNT_BEFORE_RESET = 200; - static const float DIST_SAME_SPOT = 1.8f; - static const float DURATION_SAME_SPOT = 1.0f; - static const float DURATION_TO_EVADE = 0.4f; + static const int COUNT_BEFORE_RESET = 200; // TODO: maybe no longer needed + static const float DOOR_CHECK_INTERVAL = 1.5f; AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat): mDistance(distance), mDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle), mRepeat(repeat) @@ -29,16 +26,10 @@ namespace MWMechanics , mCellY(std::numeric_limits::max()) , mXCell(0) , mYCell(0) - , mX(0) - , mY(0) - , mZ(0) - , mPrevX(0) - , mPrevY(0) - , mWalkState(State_Norm) - , mDistSameSpot(0) - , mStuckCount(0) - , mEvadeDuration(0) - , mStuckDuration(0) + , mCell(NULL) + , mStuckCount(0) // TODO: maybe no longer needed + , mDoorCheckDuration(0) + , mTrimCurrentNode(false) , mSaidGreeting(false) { for(unsigned short counter = 0; counter < mIdle.size(); counter++) @@ -56,7 +47,7 @@ namespace MWMechanics mStartTime = MWBase::Environment::get().getWorld()->getTimeStamp(); mPlayedIdle = 0; - mPathgrid = NULL; + //mPathgrid = NULL; mIdleChanceMultiplier = MWBase::Environment::get().getWorld()->getStore().get().find("fIdleChanceMultiplier")->getFloat(); @@ -72,10 +63,71 @@ namespace MWMechanics return new AiWander(*this); } + /* + * AiWander high level states (0.29.0). Not entirely accurate in some cases + * e.g. non-NPC actors do not greet and some creatures may be moving even in + * the IdleNow state. + * + * [select node, + * build path] + * +---------->MoveNow----------->Walking + * | | + * [allowed | | + * nodes] | [hello if near] | + * start--->ChooseAction----->IdleNow | + * ^ ^ | | + * | | | | + * | +-----------+ | + * | | + * +----------------------------------+ + * + * + * New high level states. Not exactly as per vanilla (e.g. door stuff) + * but the differences are required because our physics does not work like + * vanilla and therefore have to compensate/work around. Note also many of + * the actions now have reaction times. + * + * [select node, [if stuck evade + * build path] or remove nodes if near door] + * +---------->MoveNow<---------->Walking + * | ^ | | + * | |(near door) | | + * [allowed | | | | + * nodes] | [hello if near] | | + * start--->ChooseAction----->IdleNow | | + * ^ ^ | ^ | | + * | | | | (stuck near | | + * | +-----------+ +---------------+ | + * | player) | + * +----------------------------------+ + * + * TODO: non-time critical operations should be run once every 250ms or so. + * + * TODO: It would be great if door opening/closing can be detected and pathgrid + * links dynamically updated. Currently (0.29.0) AiWander allows destination + * beyond closed doors which sometimes makes the actors stuck at the door and + * impossible for the player to open the door. + * + * For now detect being stuck at the door and simply delete the nodes from the + * allowed set. The issue is when the door opens the allowed set is not + * re-calculated. Normally this would not be an issue since hostile actors will + * enter combat (i.e. no longer wandering) + * + * FIXME: Sometimes allowed nodes that shouldn't be deleted are deleted. + */ bool AiWander::execute (const MWWorld::Ptr& actor,float duration) { - actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); - actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, false); + bool cellChange = mCell && (actor.getCell() != mCell); + if(!mCell || cellChange) + { + mCell = actor.getCell(); + mStoredAvailableNodes = false; // prob. not needed since mDistance = 0 + } + const ESM::Cell *cell = mCell->getCell(); + + MWMechanics::CreatureStats& cStats = actor.getClass().getCreatureStats(actor); + cStats.setDrawState(DrawState_Nothing); + cStats.setMovementFlag(CreatureStats::Flag_Run, false); MWBase::World *world = MWBase::Environment::get().getWorld(); if(mDuration) { @@ -105,65 +157,76 @@ namespace MWMechanics ESM::Position pos = actor.getRefData().getPosition(); - // Once off initialization to discover & store allowed node points for this actor. + // Initialization to discover & store allowed node points for this actor. if(!mStoredAvailableNodes) { - mPathgrid = world->getStore().get().search(*actor.getCell()->getCell()); + // infrequently used, therefore no benefit in caching it as a member + const ESM::Pathgrid * + pathgrid = world->getStore().get().search(*cell); - mCellX = actor.getCell()->getCell()->mData.mX; - mCellY = actor.getCell()->getCell()->mData.mY; + // cache the current cell location + mCellX = cell->mData.mX; + mCellY = cell->mData.mY; // If there is no path this actor doesn't go anywhere. See: // https://forum.openmw.org/viewtopic.php?t=1556 // http://www.fliggerty.com/phpBB3/viewtopic.php?f=30&t=5833 - if(!mPathgrid) - mDistance = 0; - else if(mPathgrid->mPoints.empty()) + if(!pathgrid || pathgrid->mPoints.empty()) mDistance = 0; - if(mDistance) // A distance value is initially passed into the constructor. + // A distance value passed into the constructor indicates how far the + // actor can wander from the spawn position. AiWander assumes that + // pathgrid points are available, and uses them to randomly select wander + // destinations within the allowed set of pathgrid points (nodes). + if(mDistance) { mXCell = 0; mYCell = 0; - if(actor.getCell()->getCell()->isExterior()) + if(cell->isExterior()) { mXCell = mCellX * ESM::Land::REAL_SIZE; mYCell = mCellY * ESM::Land::REAL_SIZE; } - // convert npcPos to local (i.e. cell) co-ordinates - Ogre::Vector3 npcPos(actor.getRefData().getPosition().pos); - npcPos[0] = npcPos[0] - mXCell; - npcPos[1] = npcPos[1] - mYCell; + // convert actorPos to local (i.e. cell) co-ordinates + Ogre::Vector3 actorPos(pos.pos); + actorPos[0] = actorPos[0] - mXCell; + actorPos[1] = actorPos[1] - mYCell; - // populate mAllowedNodes for this actor with pathgrid point indexes based on mDistance - // NOTE: mPoints and mAllowedNodes contain points in local co-ordinates - for(unsigned int counter = 0; counter < mPathgrid->mPoints.size(); counter++) + // mAllowedNodes for this actor with pathgrid point indexes + // based on mDistance + // NOTE: mPoints and mAllowedNodes are in local co-ordinates + float closestNodeDist = -1; + unsigned int closestIndex = 0; + unsigned int indexAllowedNodes = 0; + for(unsigned int counter = 0; counter < pathgrid->mPoints.size(); counter++) { - Ogre::Vector3 nodePos(mPathgrid->mPoints[counter].mX, - mPathgrid->mPoints[counter].mY, - mPathgrid->mPoints[counter].mZ); - if(npcPos.squaredDistance(nodePos) <= mDistance * mDistance) - mAllowedNodes.push_back(mPathgrid->mPoints[counter]); + float sqrDist = actorPos.squaredDistance(Ogre::Vector3( + pathgrid->mPoints[counter].mX, + pathgrid->mPoints[counter].mY, + pathgrid->mPoints[counter].mZ)); + if(sqrDist <= (mDistance * mDistance)) + { + mAllowedNodes.push_back(pathgrid->mPoints[counter]); + // keep track of the closest node + if(closestNodeDist == -1 || sqrDist < closestNodeDist) + { + closestNodeDist = sqrDist; + closestIndex = indexAllowedNodes; + } + indexAllowedNodes++; + } } if(!mAllowedNodes.empty()) { - Ogre::Vector3 firstNodePos(mAllowedNodes[0].mX, mAllowedNodes[0].mY, mAllowedNodes[0].mZ); - float closestNode = npcPos.squaredDistance(firstNodePos); - unsigned int index = 0; - for(unsigned int counterThree = 1; counterThree < mAllowedNodes.size(); counterThree++) - { - Ogre::Vector3 nodePos(mAllowedNodes[counterThree].mX, - mAllowedNodes[counterThree].mY, - mAllowedNodes[counterThree].mZ); - float tempDist = npcPos.squaredDistance(nodePos); - if(tempDist < closestNode) - index = counterThree; - } - mCurrentNode = mAllowedNodes[index]; - mAllowedNodes.erase(mAllowedNodes.begin() + index); - - mStoredAvailableNodes = true; // set only if successful in finding allowed nodes + // Start with the closest node and remove it from the allowed set + // so that it does not get selected again. The removed node will + // later be put in the back of the queue, unless it gets removed + // due to inaccessibility (e.g. a closed door) + mCurrentNode = mAllowedNodes[closestIndex]; + mAllowedNodes.erase(mAllowedNodes.begin() + closestIndex); + // set only if successful in finding allowed nodes + mStoredAvailableNodes = true; } } } @@ -173,10 +236,10 @@ namespace MWMechanics mDistance = 0; // Don't try to move if you are in a new cell (ie: positioncell command called) but still play idles. - if(mDistance && (mCellX != actor.getCell()->getCell()->mData.mX || mCellY != actor.getCell()->getCell()->mData.mY)) + if(mDistance && cellChange) mDistance = 0; - if(mChooseAction) // Initially set true by the constructor. + if(mChooseAction) { mPlayedIdle = 0; unsigned short idleRoll = 0; @@ -207,7 +270,7 @@ namespace MWMechanics mIdleNow = true; // Play idle voiced dialogue entries randomly - int hello = actor.getClass().getCreatureStats(actor).getAiSetting(CreatureStats::AI_Hello).getModified(); + int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified(); if (hello > 0) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); @@ -216,19 +279,38 @@ namespace MWMechanics MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); // Don't bother if the player is out of hearing range - if (roll < chance && Ogre::Vector3(player.getRefData().getPosition().pos).distance(Ogre::Vector3(actor.getRefData().getPosition().pos)) < 1500) + if (roll < chance && Ogre::Vector3(player.getRefData().getPosition().pos).distance(Ogre::Vector3(pos.pos)) < 1500) MWBase::Environment::get().getDialogueManager()->say(actor, "idle"); } } } + // Check if an idle actor is too close to a door - if so start walking + mDoorCheckDuration += duration; + if(mDoorCheckDuration >= DOOR_CHECK_INTERVAL) + { + mDoorCheckDuration = 0; // restart timer + if(mDistance && // actor is not intended to be stationary + mIdleNow && // but is in idle + !mWalking && // FIXME: some actors are idle while walking + proximityToDoor(actor)) // NOTE: checks interior cells only + { + mIdleNow = false; + mMoveNow = true; + mTrimCurrentNode = false; // just in case +//#if 0 + std::cout << "idle door \""+actor.getClass().getName(actor)+"\" "<< std::endl; +//#endif + } + } + // Allow interrupting a walking actor to trigger a greeting - if(mIdleNow || (mWalking && (mWalkState != State_Norm))) + if(mIdleNow || (mWalking && !mObstacleCheck.isNormalState())) { // Play a random voice greeting if the player gets too close const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - int hello = actor.getClass().getCreatureStats(actor).getAiSetting(CreatureStats::AI_Hello).getModified(); + int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified(); float helloDistance = hello; int iGreetDistanceMultiplier = store.get().find("iGreetDistanceMultiplier")->getInt(); helloDistance *= iGreetDistanceMultiplier; @@ -242,7 +324,7 @@ namespace MWMechanics stopWalking(actor); mMoveNow = false; mWalking = false; - mWalkState = State_Norm; + mObstacleCheck.clear(); } if (!mSaidGreeting) @@ -275,12 +357,15 @@ namespace MWMechanics if(mMoveNow && mDistance) { + // Construct a new path if there isn't one if(!mPathFinder.isPathConstructed()) { assert(mAllowedNodes.size()); unsigned int randNode = (int)(rand() / ((double)RAND_MAX + 1) * mAllowedNodes.size()); - // NOTE: destNodePos initially constructed with local (i.e. cell) co-ordinates - Ogre::Vector3 destNodePos(mAllowedNodes[randNode].mX, mAllowedNodes[randNode].mY, mAllowedNodes[randNode].mZ); + // NOTE: initially constructed with local (i.e. cell) co-ordinates + Ogre::Vector3 destNodePos(mAllowedNodes[randNode].mX, + mAllowedNodes[randNode].mY, + mAllowedNodes[randNode].mZ); // convert dest to use world co-ordinates ESM::Pathgrid::Point dest; @@ -299,16 +384,27 @@ namespace MWMechanics if(mPathFinder.isPathConstructed()) { - // buildPath inserts dest in case it is not a pathgraph point index - // which is a duplicate for AiWander + // buildPath inserts dest in case it is not a pathgraph point + // index which is a duplicate for AiWander. However below code + // does not work since getPath() returns a copy of path not a + // reference //if(mPathFinder.getPathSize() > 1) //mPathFinder.getPath().pop_back(); - // Remove this node as an option and add back the previously used node - // (stops NPC from picking the same node): + // Remove this node as an option and add back the previously used node (stops NPC from picking the same node): ESM::Pathgrid::Point temp = mAllowedNodes[randNode]; mAllowedNodes.erase(mAllowedNodes.begin() + randNode); - mAllowedNodes.push_back(mCurrentNode); + // check if mCurrentNode was taken out of mAllowedNodes + if(mTrimCurrentNode && mAllowedNodes.size() > 1) + { + mTrimCurrentNode = false; +#if 0 + std::cout << "deleted "<< std::to_string(mCurrentNode.mX) + +", "+std::to_string(mCurrentNode.mY) << std::endl; +#endif + } + else + mAllowedNodes.push_back(mCurrentNode); mCurrentNode = temp; mMoveNow = false; @@ -320,124 +416,97 @@ namespace MWMechanics } } - if(mWalking) + // Are we there yet? + if(mWalking && + mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2])) { - if(mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2])) - { - stopWalking(actor); - mMoveNow = false; - mWalking = false; - mChooseAction = true; - } - else - { - /* f t - * State_Norm <---> State_CheckStuck --> State_Evade - * ^ ^ | ^ | ^ | | - * | | | | | | | | - * | +---+ +---+ +---+ | u - * | any < t < u | - * +--------------------------------------------+ - * - * f = one frame - * t = how long before considered stuck - * u = how long to move sideways - * - * DIST_SAME_SPOT is calibrated for movement speed of around 150. - * A rat has walking speed of around 30, so we need to adjust for - * that. - */ - if(!mDistSameSpot) - mDistSameSpot = DIST_SAME_SPOT * (actor.getClass().getSpeed(actor) / 150); - bool samePosition = (abs(pos.pos[0] - mPrevX) < mDistSameSpot) && - (abs(pos.pos[1] - mPrevY) < mDistSameSpot); + stopWalking(actor); + mMoveNow = false; + mWalking = false; + mChooseAction = true; + } + else if(mWalking) // have not yet reached the destination + { + // turn towards the next point in mPath + zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]))); + actor.getClass().getMovementSettings(actor).mPosition[1] = 1; - switch(mWalkState) + // Returns true if evasive action needs to be taken + if(mObstacleCheck.check(actor, duration)) + { + // first check if we're walking into a door + if(proximityToDoor(actor)) // NOTE: checks interior cells only { - case State_Norm: - { - if(!samePosition) - break; - else - mWalkState = State_CheckStuck; - } - /* FALL THROUGH */ - case State_CheckStuck: - { - if(!samePosition) - { - mWalkState = State_Norm; - mStuckDuration = 0; - break; - } - else - { - mStuckDuration += duration; - // consider stuck only if position unchanges for a period - if(mStuckDuration < DURATION_SAME_SPOT) - break; // still checking, note duration added to timer - else - { - mStuckDuration = 0; - mStuckCount++; - mWalkState = State_Evade; - } - } - } - /* FALL THROUGH */ - case State_Evade: - { - mEvadeDuration += duration; - if(mEvadeDuration < DURATION_TO_EVADE) - break; - else - { - mWalkState = State_Norm; // tried to evade, assume all is ok and start again - mEvadeDuration = 0; - } - } - /* NO DEFAULT CASE */ + // remove allowed points then select another random destination + mTrimCurrentNode = true; + trimAllowedNodes(mAllowedNodes, mPathFinder); + mObstacleCheck.clear(); + mPathFinder.clearPath(); + mWalking = false; + mMoveNow = true; } - - if(mWalkState == State_Evade) + else // probably walking into another NPC { - //std::cout << "Stuck \""<= COUNT_BEFORE_RESET) // something has gone wrong, reset - { - //std::cout << "Reset \""<= COUNT_BEFORE_RESET) // something has gone wrong, reset + { + //std::cout << "Reset \""<< cls.getName(actor) << "\"" << std::endl; + mObstacleCheck.clear(); + + stopWalking(actor); + mMoveNow = false; + mWalking = false; + mChooseAction = true; + } +//#endif } - return false; + return false; // AiWander package not yet completed + } + + void AiWander::trimAllowedNodes(std::vector& nodes, + const PathFinder& pathfinder) + { +//#if 0 + std::cout << "allowed size "<< std::to_string(nodes.size()) << std::endl; +//#endif + // TODO: how to add these back in once the door opens? + std::list paths = pathfinder.getPath(); + while(paths.size() >= 2) + { + ESM::Pathgrid::Point pt = paths.back(); +#if 0 + std::cout << "looking for "<< + "pt "+std::to_string(pt.mX)+", "+std::to_string(pt.mY) + < #include "pathfinding.hpp" +#include "obstacle.hpp" #include "../mwworld/timestamp.hpp" @@ -26,7 +27,7 @@ namespace MWMechanics void playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); - int mDistance; + int mDistance; // how far the actor can wander from the spawn point int mDuration; int mTimeOfDay; std::vector mIdle; @@ -34,35 +35,18 @@ namespace MWMechanics bool mSaidGreeting; - float mX; - float mY; - float mZ; - - // Cell location + // Cached current cell location int mCellX; int mCellY; // Cell location multiplied by ESM::Land::REAL_SIZE float mXCell; float mYCell; - // for checking if we're stuck (but don't check Z axis) - float mPrevX; - float mPrevY; - - enum WalkState - { - State_Norm, - State_CheckStuck, - State_Evade - }; - WalkState mWalkState; - - int mStuckCount; - float mStuckDuration; // accumulate time here while in same spot - float mEvadeDuration; - float mDistSameSpot; // take account of actor's speed + const MWWorld::CellStore* mCell; // for detecting cell change + // if false triggers calculating allowed nodes based on mDistance bool mStoredAvailableNodes; + // AiWander states bool mChooseAction; bool mIdleNow; bool mMoveNow; @@ -73,12 +57,21 @@ namespace MWMechanics MWWorld::TimeStamp mStartTime; + // allowed pathgrid nodes based on mDistance from the spawn point std::vector mAllowedNodes; ESM::Pathgrid::Point mCurrentNode; + bool mTrimCurrentNode; + void trimAllowedNodes(std::vector& nodes, + const PathFinder& pathfinder); PathFinder mPathFinder; - const ESM::Pathgrid *mPathgrid; + //const ESM::Pathgrid *mPathgrid; + ObstacleCheck mObstacleCheck; + float mDoorCheckDuration; + int mStuckCount; + + //float mReaction; }; } diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp new file mode 100644 index 000000000..00f97ae01 --- /dev/null +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -0,0 +1,176 @@ +#include "obstacle.hpp" + +#include + +#include "../mwbase/world.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/cellstore.hpp" + +namespace MWMechanics +{ + // NOTE: determined empirically but probably need further tweaking + static const float DIST_SAME_SPOT = 1.8f; + static const float DURATION_SAME_SPOT = 1.0f; + static const float DURATION_TO_EVADE = 0.4f; + + // Proximity check function for interior doors. Given that most interior cells + // do not have many doors performance shouldn't be too much of an issue. + // + // Limitation: there can be false detections, and does not test whether the + // actor is facing the door. + bool proximityToDoor(const MWWorld::Ptr& actor, float minSqr, bool closed) + { + MWWorld::CellStore *cell = actor.getCell(); + + if(cell->getCell()->isExterior()) + return false; // check interior cells only + + // Check all the doors in this cell + MWWorld::CellRefList& doors = cell->get(); + MWWorld::CellRefList::List& refList = doors.mList; + MWWorld::CellRefList::List::iterator it = refList.begin(); + Ogre::Vector3 pos(actor.getRefData().getPosition().pos); + + // TODO: How to check whether the actor is facing a door? Below code is for + // the player, perhaps it can be adapted. + //MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->getFacedObject(); + //if(!ptr.isEmpty()) + //std::cout << "faced door " << ptr.getClass().getName(ptr) << std::endl; + + // TODO: The in-game observation of rot[2] value seems to be the + // opposite of the code in World::activateDoor() ::confused:: + for (; it != refList.end(); ++it) + { + MWWorld::LiveCellRef& ref = *it; + if(pos.squaredDistance(Ogre::Vector3(ref.mRef.mPos.pos)) < minSqr && + ref.mData.getLocalRotation().rot[2] == (closed ? 0 : 1)) + { +//#if 0 + std::cout << "\""+actor.getClass().getName(actor)+"\" " + <<"next to door "+ref.mRef.mRefID + //+", enabled? "+std::to_string(ref.mData.isEnabled()) + +", dist "+std::to_string(sqrt(pos.squaredDistance(Ogre::Vector3(ref.mRef.mPos.pos)))) + << std::endl; +//#endif + return true; // found, stop searching + } + } + return false; // none found + } + + ObstacleCheck::ObstacleCheck(): + mPrevX(0) // to see if the moved since last time + , mPrevY(0) + , mDistSameSpot(-1) // avoid calculating it each time + , mWalkState(State_Norm) + , mStuckDuration(0) + , mEvadeDuration(0) + { + } + + void ObstacleCheck::clear() + { + mWalkState = State_Norm; + mStuckDuration = 0; + mEvadeDuration = 0; + } + + bool ObstacleCheck::isNormalState() const + { + return mWalkState == State_Norm; + } + + /* + * input - actor, duration (time since last check) + * output - true if evasive action needs to be taken + * + * Walking state transitions (player greeting check not shown): + * + * MoveNow <------------------------------------+ + * | d| + * | | + * +-> State_Norm <---> State_CheckStuck --> State_Evade + * ^ ^ | f ^ | t ^ | | + * | | | | | | | | + * | +---+ +---+ +---+ | u + * | any < t < u | + * +--------------------------------------------+ + * + * f = one reaction time + * d = proximity to a closed door + * t = how long before considered stuck + * u = how long to move sideways + * + * DIST_SAME_SPOT is calibrated for movement speed of around 150. + * A rat has walking speed of around 30, so we need to adjust for + * that. + */ + bool ObstacleCheck::check(const MWWorld::Ptr& actor, float duration) + { + const MWWorld::Class& cls = actor.getClass(); + ESM::Position pos = actor.getRefData().getPosition(); + + if(mDistSameSpot == -1) + mDistSameSpot = DIST_SAME_SPOT * (cls.getSpeed(actor) / 150); + + bool samePosition = (abs(pos.pos[0] - mPrevX) < mDistSameSpot) && + (abs(pos.pos[1] - mPrevY) < mDistSameSpot); + // update position + mPrevX = pos.pos[0]; + mPrevY = pos.pos[1]; + + switch(mWalkState) + { + case State_Norm: + { + if(!samePosition) + break; + else + mWalkState = State_CheckStuck; + } + /* FALL THROUGH */ + case State_CheckStuck: + { + if(!samePosition) + { + mWalkState = State_Norm; + mStuckDuration = 0; + break; + } + else + { + mStuckDuration += duration; + // consider stuck only if position unchanges for a period + if(mStuckDuration < DURATION_SAME_SPOT) + break; // still checking, note duration added to timer + else + { + mStuckDuration = 0; + mWalkState = State_Evade; + } + } + } + /* FALL THROUGH */ + case State_Evade: + { + mEvadeDuration += duration; + if(mEvadeDuration < DURATION_TO_EVADE) + return true; + else + { +//#if 0 + std::cout << "evade \""+actor.getClass().getName(actor)+"\" " + //<<"dist spot "+std::to_string(mDistSameSpot) + //+", speed "+std::to_string(cls.getSpeed(actor)) + << std::endl; +//#endif + // tried to evade, assume all is ok and start again + mWalkState = State_Norm; + mEvadeDuration = 0; + } + } + /* NO DEFAULT CASE */ + } + return false; // no obstacles to evade (yet) + } +} diff --git a/apps/openmw/mwmechanics/obstacle.hpp b/apps/openmw/mwmechanics/obstacle.hpp new file mode 100644 index 000000000..920e2e794 --- /dev/null +++ b/apps/openmw/mwmechanics/obstacle.hpp @@ -0,0 +1,52 @@ +#ifndef OPENMW_MECHANICS_OBSTACLE_H + +namespace MWWorld +{ + class Ptr; +} + +namespace MWMechanics +{ + // NOTE: determined empirically based on in-game behaviour + static const float MIN_DIST_TO_DOOR_SQUARED = 128*128; + + // tests actor's proximity to a closed door by default + bool proximityToDoor(const MWWorld::Ptr& actor, + float minSqr = MIN_DIST_TO_DOOR_SQUARED, + bool closed = true); + + class ObstacleCheck + { + public: + ObstacleCheck(); + + // Clear the timers and set the state machine to default + void clear(); + + bool isNormalState() const; + + // Returns true if there is an obstacle and an evasive action + // should be taken + bool check(const MWWorld::Ptr& actor, float duration); + + private: + + // for checking if we're stuck (ignoring Z axis) + float mPrevX; + float mPrevY; + + enum WalkState + { + State_Norm, + State_CheckStuck, + State_Evade + }; + WalkState mWalkState; + + float mStuckDuration; // accumulate time here while in same spot + float mEvadeDuration; + float mDistSameSpot; // take account of actor's speed + }; +} + +#endif