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.

This commit is contained in:
cc9cii 2014-04-18 09:03:36 +10:00
parent 1ceeeb4a22
commit d3be725ee7
5 changed files with 488 additions and 198 deletions

View file

@ -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

View file

@ -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<int>& idle, bool repeat):
mDistance(distance), mDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle), mRepeat(repeat)
@ -29,16 +26,10 @@ namespace MWMechanics
, mCellY(std::numeric_limits<int>::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<ESM::GameSetting>().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<ESM::Pathgrid>().search(*actor.getCell()->getCell());
// infrequently used, therefore no benefit in caching it as a member
const ESM::Pathgrid *
pathgrid = world->getStore().get<ESM::Pathgrid>().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<ESM::GameSetting>().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,15 +384,26 @@ 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);
// 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;
@ -320,124 +416,97 @@ namespace MWMechanics
}
}
if(mWalking)
{
if(mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2]))
// Are we there yet?
if(mWalking &&
mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2]))
{
stopWalking(actor);
mMoveNow = false;
mWalking = false;
mChooseAction = true;
}
else
else if(mWalking) // have not yet reached the destination
{
/* 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);
// 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))
{
case State_Norm:
// first check if we're walking into a door
if(proximityToDoor(actor)) // NOTE: checks interior cells only
{
if(!samePosition)
break;
else
mWalkState = State_CheckStuck;
// remove allowed points then select another random destination
mTrimCurrentNode = true;
trimAllowedNodes(mAllowedNodes, mPathFinder);
mObstacleCheck.clear();
mPathFinder.clearPath();
mWalking = false;
mMoveNow = true;
}
/* FALL THROUGH */
case State_CheckStuck:
else // probably walking into another NPC
{
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 */
}
if(mWalkState == State_Evade)
{
//std::cout << "Stuck \""<<actor.getClass().getName(actor)<<"\"" << std::endl;
// diagonal should have same animation as walk forward
// TODO: diagonal should have same animation as walk forward
// but doesn't seem to do that?
actor.getClass().getMovementSettings(actor).mPosition[0] = 1;
actor.getClass().getMovementSettings(actor).mPosition[1] = 0.1f;
// change the angle a bit, too
zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0] + 1, pos.pos[1])));
}
else
{
// normal walk forward
actor.getClass().getMovementSettings(actor).mPosition[1] = 1;
// turn towards the next point in mPath
// TODO: possibly no need to check every frame, maybe every 30 should be ok?
zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1])));
mStuckCount++; // TODO: maybe no longer needed
}
//#if 0
// TODO: maybe no longer needed
if(mStuckCount >= COUNT_BEFORE_RESET) // something has gone wrong, reset
{
//std::cout << "Reset \""<<actor.getClass().getName(actor)<<"\"" << std::endl;
mWalkState = State_Norm;
mStuckCount = 0;
//std::cout << "Reset \""<< cls.getName(actor) << "\"" << std::endl;
mObstacleCheck.clear();
stopWalking(actor);
mMoveNow = false;
mWalking = false;
mChooseAction = true;
}
// update position
ESM::Position updatedPos = actor.getRefData().getPosition();
mPrevX = updatedPos.pos[0];
mPrevY = updatedPos.pos[1];
}
//#endif
}
return false;
return false; // AiWander package not yet completed
}
void AiWander::trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& 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<ESM::Pathgrid::Point> 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)
<<std::endl;
#endif
for(int j = 0; j < nodes.size(); j++)
{
// NOTE: doesn't hadle a door with the same X/Y
// coordinates but with a different Z
if(nodes[j].mX == pt.mX && nodes[j].mY == pt.mY)
{
nodes.erase(nodes.begin() + j);
//#if 0
std::cout << "deleted "<<
"pt "+std::to_string(pt.mX)+", "+std::to_string(pt.mY)
<<std::endl;
//#endif
break;
}
}
paths.pop_back();
}
}
int AiWander::getTypeId() const

View file

@ -5,6 +5,7 @@
#include <vector>
#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<int> 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<ESM::Pathgrid::Point> mAllowedNodes;
ESM::Pathgrid::Point mCurrentNode;
bool mTrimCurrentNode;
void trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes,
const PathFinder& pathfinder);
PathFinder mPathFinder;
const ESM::Pathgrid *mPathgrid;
//const ESM::Pathgrid *mPathgrid;
ObstacleCheck mObstacleCheck;
float mDoorCheckDuration;
int mStuckCount;
//float mReaction;
};
}

View file

@ -0,0 +1,176 @@
#include "obstacle.hpp"
#include <OgreVector3.h>
#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<ESM::Door>& doors = cell->get<ESM::Door>();
MWWorld::CellRefList<ESM::Door>::List& refList = doors.mList;
MWWorld::CellRefList<ESM::Door>::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<ESM::Door>& 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)
}
}

View file

@ -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