forked from mirror/openmw-tes3mp
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:
parent
1ceeeb4a22
commit
d3be725ee7
5 changed files with 488 additions and 198 deletions
|
@ -69,7 +69,7 @@ add_openmw_dir (mwmechanics
|
||||||
mechanicsmanagerimp stat character creaturestats magiceffects movement actors objects
|
mechanicsmanagerimp stat character creaturestats magiceffects movement actors objects
|
||||||
drawstate spells activespells npcstats aipackage aisequence aipersue alchemy aiwander aitravel aifollow
|
drawstate spells activespells npcstats aipackage aisequence aipersue alchemy aiwander aitravel aifollow
|
||||||
aiescort aiactivate aicombat repair enchanting pathfinding pathgrid security spellsuccess spellcasting
|
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
|
add_openmw_dir (mwstate
|
||||||
|
|
|
@ -17,11 +17,8 @@
|
||||||
|
|
||||||
namespace MWMechanics
|
namespace MWMechanics
|
||||||
{
|
{
|
||||||
// NOTE: determined empirically but probably need further tweaking
|
static const int COUNT_BEFORE_RESET = 200; // TODO: maybe no longer needed
|
||||||
static const int COUNT_BEFORE_RESET = 200;
|
static const float DOOR_CHECK_INTERVAL = 1.5f;
|
||||||
static const float DIST_SAME_SPOT = 1.8f;
|
|
||||||
static const float DURATION_SAME_SPOT = 1.0f;
|
|
||||||
static const float DURATION_TO_EVADE = 0.4f;
|
|
||||||
|
|
||||||
AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<int>& idle, bool repeat):
|
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)
|
mDistance(distance), mDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle), mRepeat(repeat)
|
||||||
|
@ -29,16 +26,10 @@ namespace MWMechanics
|
||||||
, mCellY(std::numeric_limits<int>::max())
|
, mCellY(std::numeric_limits<int>::max())
|
||||||
, mXCell(0)
|
, mXCell(0)
|
||||||
, mYCell(0)
|
, mYCell(0)
|
||||||
, mX(0)
|
, mCell(NULL)
|
||||||
, mY(0)
|
, mStuckCount(0) // TODO: maybe no longer needed
|
||||||
, mZ(0)
|
, mDoorCheckDuration(0)
|
||||||
, mPrevX(0)
|
, mTrimCurrentNode(false)
|
||||||
, mPrevY(0)
|
|
||||||
, mWalkState(State_Norm)
|
|
||||||
, mDistSameSpot(0)
|
|
||||||
, mStuckCount(0)
|
|
||||||
, mEvadeDuration(0)
|
|
||||||
, mStuckDuration(0)
|
|
||||||
, mSaidGreeting(false)
|
, mSaidGreeting(false)
|
||||||
{
|
{
|
||||||
for(unsigned short counter = 0; counter < mIdle.size(); counter++)
|
for(unsigned short counter = 0; counter < mIdle.size(); counter++)
|
||||||
|
@ -56,7 +47,7 @@ namespace MWMechanics
|
||||||
|
|
||||||
mStartTime = MWBase::Environment::get().getWorld()->getTimeStamp();
|
mStartTime = MWBase::Environment::get().getWorld()->getTimeStamp();
|
||||||
mPlayedIdle = 0;
|
mPlayedIdle = 0;
|
||||||
mPathgrid = NULL;
|
//mPathgrid = NULL;
|
||||||
mIdleChanceMultiplier =
|
mIdleChanceMultiplier =
|
||||||
MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fIdleChanceMultiplier")->getFloat();
|
MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fIdleChanceMultiplier")->getFloat();
|
||||||
|
|
||||||
|
@ -72,10 +63,71 @@ namespace MWMechanics
|
||||||
return new AiWander(*this);
|
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)
|
bool AiWander::execute (const MWWorld::Ptr& actor,float duration)
|
||||||
{
|
{
|
||||||
actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing);
|
bool cellChange = mCell && (actor.getCell() != mCell);
|
||||||
actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, false);
|
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();
|
MWBase::World *world = MWBase::Environment::get().getWorld();
|
||||||
if(mDuration)
|
if(mDuration)
|
||||||
{
|
{
|
||||||
|
@ -105,65 +157,76 @@ namespace MWMechanics
|
||||||
|
|
||||||
ESM::Position pos = actor.getRefData().getPosition();
|
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)
|
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;
|
// cache the current cell location
|
||||||
mCellY = actor.getCell()->getCell()->mData.mY;
|
mCellX = cell->mData.mX;
|
||||||
|
mCellY = cell->mData.mY;
|
||||||
|
|
||||||
// If there is no path this actor doesn't go anywhere. See:
|
// If there is no path this actor doesn't go anywhere. See:
|
||||||
// https://forum.openmw.org/viewtopic.php?t=1556
|
// https://forum.openmw.org/viewtopic.php?t=1556
|
||||||
// http://www.fliggerty.com/phpBB3/viewtopic.php?f=30&t=5833
|
// http://www.fliggerty.com/phpBB3/viewtopic.php?f=30&t=5833
|
||||||
if(!mPathgrid)
|
if(!pathgrid || pathgrid->mPoints.empty())
|
||||||
mDistance = 0;
|
|
||||||
else if(mPathgrid->mPoints.empty())
|
|
||||||
mDistance = 0;
|
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;
|
mXCell = 0;
|
||||||
mYCell = 0;
|
mYCell = 0;
|
||||||
if(actor.getCell()->getCell()->isExterior())
|
if(cell->isExterior())
|
||||||
{
|
{
|
||||||
mXCell = mCellX * ESM::Land::REAL_SIZE;
|
mXCell = mCellX * ESM::Land::REAL_SIZE;
|
||||||
mYCell = mCellY * ESM::Land::REAL_SIZE;
|
mYCell = mCellY * ESM::Land::REAL_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert npcPos to local (i.e. cell) co-ordinates
|
// convert actorPos to local (i.e. cell) co-ordinates
|
||||||
Ogre::Vector3 npcPos(actor.getRefData().getPosition().pos);
|
Ogre::Vector3 actorPos(pos.pos);
|
||||||
npcPos[0] = npcPos[0] - mXCell;
|
actorPos[0] = actorPos[0] - mXCell;
|
||||||
npcPos[1] = npcPos[1] - mYCell;
|
actorPos[1] = actorPos[1] - mYCell;
|
||||||
|
|
||||||
// populate mAllowedNodes for this actor with pathgrid point indexes based on mDistance
|
// mAllowedNodes for this actor with pathgrid point indexes
|
||||||
// NOTE: mPoints and mAllowedNodes contain points in local co-ordinates
|
// based on mDistance
|
||||||
for(unsigned int counter = 0; counter < mPathgrid->mPoints.size(); counter++)
|
// 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,
|
float sqrDist = actorPos.squaredDistance(Ogre::Vector3(
|
||||||
mPathgrid->mPoints[counter].mY,
|
pathgrid->mPoints[counter].mX,
|
||||||
mPathgrid->mPoints[counter].mZ);
|
pathgrid->mPoints[counter].mY,
|
||||||
if(npcPos.squaredDistance(nodePos) <= mDistance * mDistance)
|
pathgrid->mPoints[counter].mZ));
|
||||||
mAllowedNodes.push_back(mPathgrid->mPoints[counter]);
|
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())
|
if(!mAllowedNodes.empty())
|
||||||
{
|
{
|
||||||
Ogre::Vector3 firstNodePos(mAllowedNodes[0].mX, mAllowedNodes[0].mY, mAllowedNodes[0].mZ);
|
// Start with the closest node and remove it from the allowed set
|
||||||
float closestNode = npcPos.squaredDistance(firstNodePos);
|
// so that it does not get selected again. The removed node will
|
||||||
unsigned int index = 0;
|
// later be put in the back of the queue, unless it gets removed
|
||||||
for(unsigned int counterThree = 1; counterThree < mAllowedNodes.size(); counterThree++)
|
// due to inaccessibility (e.g. a closed door)
|
||||||
{
|
mCurrentNode = mAllowedNodes[closestIndex];
|
||||||
Ogre::Vector3 nodePos(mAllowedNodes[counterThree].mX,
|
mAllowedNodes.erase(mAllowedNodes.begin() + closestIndex);
|
||||||
mAllowedNodes[counterThree].mY,
|
// set only if successful in finding allowed nodes
|
||||||
mAllowedNodes[counterThree].mZ);
|
mStoredAvailableNodes = true;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,10 +236,10 @@ namespace MWMechanics
|
||||||
mDistance = 0;
|
mDistance = 0;
|
||||||
|
|
||||||
// Don't try to move if you are in a new cell (ie: positioncell command called) but still play idles.
|
// 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;
|
mDistance = 0;
|
||||||
|
|
||||||
if(mChooseAction) // Initially set true by the constructor.
|
if(mChooseAction)
|
||||||
{
|
{
|
||||||
mPlayedIdle = 0;
|
mPlayedIdle = 0;
|
||||||
unsigned short idleRoll = 0;
|
unsigned short idleRoll = 0;
|
||||||
|
@ -207,7 +270,7 @@ namespace MWMechanics
|
||||||
mIdleNow = true;
|
mIdleNow = true;
|
||||||
|
|
||||||
// Play idle voiced dialogue entries randomly
|
// 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)
|
if (hello > 0)
|
||||||
{
|
{
|
||||||
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
|
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
|
||||||
|
@ -216,19 +279,38 @@ namespace MWMechanics
|
||||||
MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr();
|
MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr();
|
||||||
|
|
||||||
// Don't bother if the player is out of hearing range
|
// 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");
|
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
|
// 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
|
// Play a random voice greeting if the player gets too close
|
||||||
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
|
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;
|
float helloDistance = hello;
|
||||||
int iGreetDistanceMultiplier = store.get<ESM::GameSetting>().find("iGreetDistanceMultiplier")->getInt();
|
int iGreetDistanceMultiplier = store.get<ESM::GameSetting>().find("iGreetDistanceMultiplier")->getInt();
|
||||||
helloDistance *= iGreetDistanceMultiplier;
|
helloDistance *= iGreetDistanceMultiplier;
|
||||||
|
@ -242,7 +324,7 @@ namespace MWMechanics
|
||||||
stopWalking(actor);
|
stopWalking(actor);
|
||||||
mMoveNow = false;
|
mMoveNow = false;
|
||||||
mWalking = false;
|
mWalking = false;
|
||||||
mWalkState = State_Norm;
|
mObstacleCheck.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mSaidGreeting)
|
if (!mSaidGreeting)
|
||||||
|
@ -275,12 +357,15 @@ namespace MWMechanics
|
||||||
|
|
||||||
if(mMoveNow && mDistance)
|
if(mMoveNow && mDistance)
|
||||||
{
|
{
|
||||||
|
// Construct a new path if there isn't one
|
||||||
if(!mPathFinder.isPathConstructed())
|
if(!mPathFinder.isPathConstructed())
|
||||||
{
|
{
|
||||||
assert(mAllowedNodes.size());
|
assert(mAllowedNodes.size());
|
||||||
unsigned int randNode = (int)(rand() / ((double)RAND_MAX + 1) * mAllowedNodes.size());
|
unsigned int randNode = (int)(rand() / ((double)RAND_MAX + 1) * mAllowedNodes.size());
|
||||||
// NOTE: destNodePos initially constructed with local (i.e. cell) co-ordinates
|
// NOTE: initially constructed with local (i.e. cell) co-ordinates
|
||||||
Ogre::Vector3 destNodePos(mAllowedNodes[randNode].mX, mAllowedNodes[randNode].mY, mAllowedNodes[randNode].mZ);
|
Ogre::Vector3 destNodePos(mAllowedNodes[randNode].mX,
|
||||||
|
mAllowedNodes[randNode].mY,
|
||||||
|
mAllowedNodes[randNode].mZ);
|
||||||
|
|
||||||
// convert dest to use world co-ordinates
|
// convert dest to use world co-ordinates
|
||||||
ESM::Pathgrid::Point dest;
|
ESM::Pathgrid::Point dest;
|
||||||
|
@ -299,15 +384,26 @@ namespace MWMechanics
|
||||||
|
|
||||||
if(mPathFinder.isPathConstructed())
|
if(mPathFinder.isPathConstructed())
|
||||||
{
|
{
|
||||||
// buildPath inserts dest in case it is not a pathgraph point index
|
// buildPath inserts dest in case it is not a pathgraph point
|
||||||
// which is a duplicate for AiWander
|
// 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)
|
//if(mPathFinder.getPathSize() > 1)
|
||||||
//mPathFinder.getPath().pop_back();
|
//mPathFinder.getPath().pop_back();
|
||||||
|
|
||||||
// Remove this node as an option and add back the previously used node
|
// Remove this node as an option and add back the previously used node (stops NPC from picking the same node):
|
||||||
// (stops NPC from picking the same node):
|
|
||||||
ESM::Pathgrid::Point temp = mAllowedNodes[randNode];
|
ESM::Pathgrid::Point temp = mAllowedNodes[randNode];
|
||||||
mAllowedNodes.erase(mAllowedNodes.begin() + 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);
|
mAllowedNodes.push_back(mCurrentNode);
|
||||||
mCurrentNode = temp;
|
mCurrentNode = temp;
|
||||||
|
|
||||||
|
@ -320,124 +416,97 @@ namespace MWMechanics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(mWalking)
|
// Are we there yet?
|
||||||
{
|
if(mWalking &&
|
||||||
if(mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2]))
|
mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2]))
|
||||||
{
|
{
|
||||||
stopWalking(actor);
|
stopWalking(actor);
|
||||||
mMoveNow = false;
|
mMoveNow = false;
|
||||||
mWalking = false;
|
mWalking = false;
|
||||||
mChooseAction = true;
|
mChooseAction = true;
|
||||||
}
|
}
|
||||||
else
|
else if(mWalking) // have not yet reached the destination
|
||||||
{
|
{
|
||||||
/* f t
|
// turn towards the next point in mPath
|
||||||
* State_Norm <---> State_CheckStuck --> State_Evade
|
zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1])));
|
||||||
* ^ ^ | ^ | ^ | |
|
actor.getClass().getMovementSettings(actor).mPosition[1] = 1;
|
||||||
* | | | | | | | |
|
|
||||||
* | +---+ +---+ +---+ | 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);
|
|
||||||
|
|
||||||
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)
|
// remove allowed points then select another random destination
|
||||||
break;
|
mTrimCurrentNode = true;
|
||||||
else
|
trimAllowedNodes(mAllowedNodes, mPathFinder);
|
||||||
mWalkState = State_CheckStuck;
|
mObstacleCheck.clear();
|
||||||
|
mPathFinder.clearPath();
|
||||||
|
mWalking = false;
|
||||||
|
mMoveNow = true;
|
||||||
}
|
}
|
||||||
/* FALL THROUGH */
|
else // probably walking into another NPC
|
||||||
case State_CheckStuck:
|
|
||||||
{
|
{
|
||||||
if(!samePosition)
|
// TODO: diagonal should have same animation as walk forward
|
||||||
{
|
// but doesn't seem to do that?
|
||||||
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
|
|
||||||
actor.getClass().getMovementSettings(actor).mPosition[0] = 1;
|
actor.getClass().getMovementSettings(actor).mPosition[0] = 1;
|
||||||
actor.getClass().getMovementSettings(actor).mPosition[1] = 0.1f;
|
actor.getClass().getMovementSettings(actor).mPosition[1] = 0.1f;
|
||||||
// change the angle a bit, too
|
// change the angle a bit, too
|
||||||
zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0] + 1, pos.pos[1])));
|
zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0] + 1, pos.pos[1])));
|
||||||
}
|
}
|
||||||
else
|
mStuckCount++; // TODO: maybe no longer needed
|
||||||
{
|
|
||||||
// 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])));
|
|
||||||
}
|
}
|
||||||
|
//#if 0
|
||||||
|
// TODO: maybe no longer needed
|
||||||
if(mStuckCount >= COUNT_BEFORE_RESET) // something has gone wrong, reset
|
if(mStuckCount >= COUNT_BEFORE_RESET) // something has gone wrong, reset
|
||||||
{
|
{
|
||||||
//std::cout << "Reset \""<<actor.getClass().getName(actor)<<"\"" << std::endl;
|
//std::cout << "Reset \""<< cls.getName(actor) << "\"" << std::endl;
|
||||||
mWalkState = State_Norm;
|
mObstacleCheck.clear();
|
||||||
mStuckCount = 0;
|
|
||||||
|
|
||||||
stopWalking(actor);
|
stopWalking(actor);
|
||||||
mMoveNow = false;
|
mMoveNow = false;
|
||||||
mWalking = false;
|
mWalking = false;
|
||||||
mChooseAction = true;
|
mChooseAction = true;
|
||||||
}
|
}
|
||||||
|
//#endif
|
||||||
// update position
|
|
||||||
ESM::Position updatedPos = actor.getRefData().getPosition();
|
|
||||||
mPrevX = updatedPos.pos[0];
|
|
||||||
mPrevY = updatedPos.pos[1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
int AiWander::getTypeId() const
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "pathfinding.hpp"
|
#include "pathfinding.hpp"
|
||||||
|
#include "obstacle.hpp"
|
||||||
|
|
||||||
#include "../mwworld/timestamp.hpp"
|
#include "../mwworld/timestamp.hpp"
|
||||||
|
|
||||||
|
@ -26,7 +27,7 @@ namespace MWMechanics
|
||||||
void playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect);
|
void playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect);
|
||||||
bool checkIdle(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 mDuration;
|
||||||
int mTimeOfDay;
|
int mTimeOfDay;
|
||||||
std::vector<int> mIdle;
|
std::vector<int> mIdle;
|
||||||
|
@ -34,35 +35,18 @@ namespace MWMechanics
|
||||||
|
|
||||||
bool mSaidGreeting;
|
bool mSaidGreeting;
|
||||||
|
|
||||||
float mX;
|
// Cached current cell location
|
||||||
float mY;
|
|
||||||
float mZ;
|
|
||||||
|
|
||||||
// Cell location
|
|
||||||
int mCellX;
|
int mCellX;
|
||||||
int mCellY;
|
int mCellY;
|
||||||
// Cell location multiplied by ESM::Land::REAL_SIZE
|
// Cell location multiplied by ESM::Land::REAL_SIZE
|
||||||
float mXCell;
|
float mXCell;
|
||||||
float mYCell;
|
float mYCell;
|
||||||
|
|
||||||
// for checking if we're stuck (but don't check Z axis)
|
const MWWorld::CellStore* mCell; // for detecting cell change
|
||||||
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
|
|
||||||
|
|
||||||
|
// if false triggers calculating allowed nodes based on mDistance
|
||||||
bool mStoredAvailableNodes;
|
bool mStoredAvailableNodes;
|
||||||
|
// AiWander states
|
||||||
bool mChooseAction;
|
bool mChooseAction;
|
||||||
bool mIdleNow;
|
bool mIdleNow;
|
||||||
bool mMoveNow;
|
bool mMoveNow;
|
||||||
|
@ -73,12 +57,21 @@ namespace MWMechanics
|
||||||
|
|
||||||
MWWorld::TimeStamp mStartTime;
|
MWWorld::TimeStamp mStartTime;
|
||||||
|
|
||||||
|
// allowed pathgrid nodes based on mDistance from the spawn point
|
||||||
std::vector<ESM::Pathgrid::Point> mAllowedNodes;
|
std::vector<ESM::Pathgrid::Point> mAllowedNodes;
|
||||||
ESM::Pathgrid::Point mCurrentNode;
|
ESM::Pathgrid::Point mCurrentNode;
|
||||||
|
bool mTrimCurrentNode;
|
||||||
|
void trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes,
|
||||||
|
const PathFinder& pathfinder);
|
||||||
|
|
||||||
PathFinder mPathFinder;
|
PathFinder mPathFinder;
|
||||||
const ESM::Pathgrid *mPathgrid;
|
//const ESM::Pathgrid *mPathgrid;
|
||||||
|
|
||||||
|
ObstacleCheck mObstacleCheck;
|
||||||
|
float mDoorCheckDuration;
|
||||||
|
int mStuckCount;
|
||||||
|
|
||||||
|
//float mReaction;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
176
apps/openmw/mwmechanics/obstacle.cpp
Normal file
176
apps/openmw/mwmechanics/obstacle.cpp
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
52
apps/openmw/mwmechanics/obstacle.hpp
Normal file
52
apps/openmw/mwmechanics/obstacle.hpp
Normal 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
|
Loading…
Reference in a new issue