mirror of
https://github.com/TES3MP/openmw-tes3mp.git
synced 2025-01-15 21:49:56 +00:00
675c0ab72f
This allows to distribute AI reaction calls over time. Before this change actors appearing at the same frame will react in the same frame over and over because AI reaction period is constant. It creates a non-uniform CPU usage over frames. If a single frame has too many AI reactions it may cause stuttering when there are too many actors on a scene for current system. Another concern is a synchronization of actions between creatures and NPC. They start to go or hit at the same frame that is unnatural.
910 lines
38 KiB
C++
910 lines
38 KiB
C++
#include "aiwander.hpp"
|
|
|
|
#include <algorithm>
|
|
|
|
#include <components/debug/debuglog.hpp>
|
|
#include <components/misc/rng.hpp>
|
|
#include <components/esm/aisequence.hpp>
|
|
#include <components/detournavigator/navigator.hpp>
|
|
#include <components/misc/coordinateconverter.hpp>
|
|
|
|
#include "../mwbase/world.hpp"
|
|
#include "../mwbase/environment.hpp"
|
|
#include "../mwbase/mechanicsmanager.hpp"
|
|
#include "../mwbase/dialoguemanager.hpp"
|
|
|
|
#include "../mwworld/class.hpp"
|
|
#include "../mwworld/esmstore.hpp"
|
|
#include "../mwworld/cellstore.hpp"
|
|
|
|
#include "../mwphysics/collisiontype.hpp"
|
|
|
|
#include "pathgrid.hpp"
|
|
#include "creaturestats.hpp"
|
|
#include "movement.hpp"
|
|
#include "actorutil.hpp"
|
|
|
|
namespace MWMechanics
|
|
{
|
|
static const int COUNT_BEFORE_RESET = 10;
|
|
static const float IDLE_POSITION_CHECK_INTERVAL = 1.5f;
|
|
|
|
// to prevent overcrowding
|
|
static const int DESTINATION_TOLERANCE = 64;
|
|
|
|
// distance must be long enough that NPC will need to move to get there.
|
|
static const int MINIMUM_WANDER_DISTANCE = DESTINATION_TOLERANCE * 2;
|
|
|
|
static const std::size_t MAX_IDLE_SIZE = 8;
|
|
|
|
const std::string AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] =
|
|
{
|
|
std::string("idle2"),
|
|
std::string("idle3"),
|
|
std::string("idle4"),
|
|
std::string("idle5"),
|
|
std::string("idle6"),
|
|
std::string("idle7"),
|
|
std::string("idle8"),
|
|
std::string("idle9"),
|
|
};
|
|
|
|
namespace
|
|
{
|
|
inline int getCountBeforeReset(const MWWorld::ConstPtr& actor)
|
|
{
|
|
if (actor.getClass().isPureWaterCreature(actor) || actor.getClass().isPureFlyingCreature(actor))
|
|
return 1;
|
|
return COUNT_BEFORE_RESET;
|
|
}
|
|
|
|
osg::Vec3f getRandomPointAround(const osg::Vec3f& position, const float distance)
|
|
{
|
|
const float randomDirection = Misc::Rng::rollClosedProbability() * 2.0f * osg::PI;
|
|
osg::Matrixf rotation;
|
|
rotation.makeRotate(randomDirection, osg::Vec3f(0.0, 0.0, 1.0));
|
|
return position + osg::Vec3f(distance, 0.0, 0.0) * rotation;
|
|
}
|
|
|
|
bool isDestinationHidden(const MWWorld::ConstPtr &actor, const osg::Vec3f& destination)
|
|
{
|
|
const auto position = actor.getRefData().getPosition().asVec3();
|
|
const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor);
|
|
const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor);
|
|
const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(actor);
|
|
osg::Vec3f direction = destination - position;
|
|
direction.normalize();
|
|
const auto visibleDestination = (
|
|
isWaterCreature || isFlyingCreature
|
|
? destination
|
|
: destination + osg::Vec3f(0, 0, halfExtents.z())
|
|
) + direction * std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z()));
|
|
const int mask = MWPhysics::CollisionType_World
|
|
| MWPhysics::CollisionType_HeightMap
|
|
| MWPhysics::CollisionType_Door
|
|
| MWPhysics::CollisionType_Actor;
|
|
return MWBase::Environment::get().getWorld()->castRay(position, visibleDestination, mask, actor);
|
|
}
|
|
|
|
bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr &actor, const osg::Vec3f& destination)
|
|
{
|
|
const auto world = MWBase::Environment::get().getWorld();
|
|
const osg::Vec3f halfExtents = world->getPathfindingHalfExtents(actor);
|
|
const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z()));
|
|
return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, actor);
|
|
}
|
|
|
|
void stopMovement(const MWWorld::Ptr& actor)
|
|
{
|
|
actor.getClass().getMovementSettings(actor).mPosition[0] = 0;
|
|
actor.getClass().getMovementSettings(actor).mPosition[1] = 0;
|
|
}
|
|
|
|
std::vector<unsigned char> getInitialIdle(const std::vector<unsigned char>& idle)
|
|
{
|
|
std::vector<unsigned char> result(MAX_IDLE_SIZE, 0);
|
|
std::copy_n(idle.begin(), std::min(MAX_IDLE_SIZE, idle.size()), result.begin());
|
|
return result;
|
|
}
|
|
|
|
std::vector<unsigned char> getInitialIdle(const unsigned char (&idle)[MAX_IDLE_SIZE])
|
|
{
|
|
return std::vector<unsigned char>(std::begin(idle), std::end(idle));
|
|
}
|
|
}
|
|
|
|
AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<unsigned char>& idle, bool repeat):
|
|
TypedAiPackage<AiWander>(makeDefaultOptions().withRepeat(repeat)),
|
|
mDistance(std::max(0, distance)),
|
|
mDuration(std::max(0, duration)),
|
|
mRemainingDuration(duration), mTimeOfDay(timeOfDay),
|
|
mIdle(getInitialIdle(idle)),
|
|
mStoredInitialActorPosition(false), mInitialActorPosition(osg::Vec3f(0, 0, 0)),
|
|
mHasDestination(false), mDestination(osg::Vec3f(0, 0, 0)), mUsePathgrid(false)
|
|
{
|
|
}
|
|
|
|
/*
|
|
* 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.
|
|
*
|
|
* [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) |
|
|
* +----------------------------------+
|
|
*
|
|
* NOTE: non-time critical operations are 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 choosing a
|
|
* 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. However this would not be an issue in most cases since hostile
|
|
* actors will enter combat (i.e. no longer wandering) and different pathfinding
|
|
* will kick in.
|
|
*/
|
|
bool AiWander::execute (const MWWorld::Ptr& actor, CharacterController& /*characterController*/, AiState& state, float duration)
|
|
{
|
|
MWMechanics::CreatureStats& cStats = actor.getClass().getCreatureStats(actor);
|
|
if (cStats.isDead() || cStats.getHealth().getCurrent() <= 0)
|
|
return true; // Don't bother with dead actors
|
|
|
|
// get or create temporary storage
|
|
AiWanderStorage& storage = state.get<AiWanderStorage>();
|
|
|
|
mRemainingDuration -= ((duration*MWBase::Environment::get().getWorld()->getTimeScaleFactor()) / 3600);
|
|
|
|
cStats.setDrawState(DrawState_Nothing);
|
|
cStats.setMovementFlag(CreatureStats::Flag_Run, false);
|
|
|
|
ESM::Position pos = actor.getRefData().getPosition();
|
|
|
|
// If there is already a destination due to the package having been interrupted by a combat or pursue package,
|
|
// rebuild a path to it
|
|
if (!mPathFinder.isPathConstructed() && mHasDestination)
|
|
{
|
|
if (mUsePathgrid)
|
|
{
|
|
mPathFinder.buildPathByPathgrid(pos.asVec3(), mDestination, actor.getCell(),
|
|
getPathGridGraph(actor.getCell()));
|
|
}
|
|
else
|
|
{
|
|
const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(actor);
|
|
mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(),
|
|
getPathGridGraph(actor.getCell()), halfExtents, getNavigatorFlags(actor), getAreaCosts(actor));
|
|
}
|
|
|
|
if (mPathFinder.isPathConstructed())
|
|
storage.setState(AiWanderStorage::Wander_Walking);
|
|
}
|
|
|
|
GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor);
|
|
if (greetingState == Greet_InProgress)
|
|
{
|
|
if (storage.mState == AiWanderStorage::Wander_Walking)
|
|
{
|
|
stopMovement(actor);
|
|
mObstacleCheck.clear();
|
|
storage.setState(AiWanderStorage::Wander_IdleNow);
|
|
}
|
|
}
|
|
|
|
doPerFrameActionsForState(actor, duration, storage);
|
|
|
|
if (storage.mReaction.update(duration) == Misc::TimerStatus::Waiting)
|
|
return false;
|
|
|
|
return reactionTimeActions(actor, storage, pos);
|
|
}
|
|
|
|
bool AiWander::reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos)
|
|
{
|
|
if (mDistance <= 0)
|
|
storage.mCanWanderAlongPathGrid = false;
|
|
|
|
if (isPackageCompleted())
|
|
{
|
|
stopWalking(actor);
|
|
// Reset package so it can be used again
|
|
mRemainingDuration=mDuration;
|
|
return true;
|
|
}
|
|
|
|
if (!mStoredInitialActorPosition)
|
|
{
|
|
mInitialActorPosition = actor.getRefData().getPosition().asVec3();
|
|
mStoredInitialActorPosition = true;
|
|
}
|
|
|
|
// Initialization to discover & store allowed node points for this actor.
|
|
if (storage.mPopulateAvailableNodes)
|
|
{
|
|
getAllowedNodes(actor, actor.getCell()->getCell(), storage);
|
|
}
|
|
|
|
if (canActorMoveByZAxis(actor) && mDistance > 0) {
|
|
// Typically want to idle for a short time before the next wander
|
|
if (Misc::Rng::rollDice(100) >= 92 && storage.mState != AiWanderStorage::Wander_Walking) {
|
|
wanderNearStart(actor, storage, mDistance);
|
|
}
|
|
|
|
storage.mCanWanderAlongPathGrid = false;
|
|
}
|
|
// If the package has a wander distance but no pathgrid is available,
|
|
// randomly idle or wander near spawn point
|
|
else if(storage.mAllowedNodes.empty() && mDistance > 0 && !storage.mIsWanderingManually) {
|
|
// Typically want to idle for a short time before the next wander
|
|
if (Misc::Rng::rollDice(100) >= 96) {
|
|
wanderNearStart(actor, storage, mDistance);
|
|
} else {
|
|
storage.setState(AiWanderStorage::Wander_IdleNow);
|
|
}
|
|
} else if (storage.mAllowedNodes.empty() && !storage.mIsWanderingManually) {
|
|
storage.mCanWanderAlongPathGrid = false;
|
|
}
|
|
|
|
// If Wandering manually and hit an obstacle, stop
|
|
if (storage.mIsWanderingManually && mObstacleCheck.isEvading()) {
|
|
completeManualWalking(actor, storage);
|
|
}
|
|
|
|
if (storage.mState == AiWanderStorage::Wander_MoveNow && storage.mCanWanderAlongPathGrid)
|
|
{
|
|
// Construct a new path if there isn't one
|
|
if(!mPathFinder.isPathConstructed())
|
|
{
|
|
if (!storage.mAllowedNodes.empty())
|
|
{
|
|
setPathToAnAllowedNode(actor, storage, pos);
|
|
}
|
|
}
|
|
}
|
|
else if (storage.mIsWanderingManually && mPathFinder.checkPathCompleted())
|
|
{
|
|
completeManualWalking(actor, storage);
|
|
}
|
|
|
|
if (storage.mIsWanderingManually
|
|
&& storage.mState == AiWanderStorage::Wander_Walking
|
|
&& (mPathFinder.getPathSize() == 0
|
|
|| isDestinationHidden(actor, mPathFinder.getPath().back())
|
|
|| isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back())))
|
|
completeManualWalking(actor, storage);
|
|
|
|
return false; // AiWander package not yet completed
|
|
}
|
|
|
|
osg::Vec3f AiWander::getDestination(const MWWorld::Ptr& actor) const
|
|
{
|
|
if (mHasDestination)
|
|
return mDestination;
|
|
|
|
return actor.getRefData().getPosition().asVec3();
|
|
}
|
|
|
|
bool AiWander::isPackageCompleted() const
|
|
{
|
|
// End package if duration is complete
|
|
return mDuration && mRemainingDuration <= 0;
|
|
}
|
|
|
|
/*
|
|
* Commands actor to walk to a random location near original spawn location.
|
|
*/
|
|
void AiWander::wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance) {
|
|
const auto currentPosition = actor.getRefData().getPosition().asVec3();
|
|
|
|
std::size_t attempts = 10; // If a unit can't wander out of water, don't want to hang here
|
|
const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor);
|
|
const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor);
|
|
const auto world = MWBase::Environment::get().getWorld();
|
|
const auto halfExtents = world->getPathfindingHalfExtents(actor);
|
|
const auto navigator = world->getNavigator();
|
|
const auto navigatorFlags = getNavigatorFlags(actor);
|
|
const auto areaCosts = getAreaCosts(actor);
|
|
|
|
do {
|
|
// Determine a random location within radius of original position
|
|
const float wanderRadius = (0.2f + Misc::Rng::rollClosedProbability() * 0.8f) * wanderDistance;
|
|
if (!isWaterCreature && !isFlyingCreature)
|
|
{
|
|
// findRandomPointAroundCircle uses wanderDistance as limit for random and not as exact distance
|
|
if (const auto destination = navigator->findRandomPointAroundCircle(halfExtents, mInitialActorPosition, wanderDistance, navigatorFlags))
|
|
mDestination = *destination;
|
|
else
|
|
mDestination = getRandomPointAround(mInitialActorPosition, wanderRadius);
|
|
}
|
|
else
|
|
mDestination = getRandomPointAround(mInitialActorPosition, wanderRadius);
|
|
|
|
// Check if land creature will walk onto water or if water creature will swim onto land
|
|
if (!isWaterCreature && destinationIsAtWater(actor, mDestination))
|
|
continue;
|
|
|
|
if (isDestinationHidden(actor, mDestination))
|
|
continue;
|
|
|
|
if (isAreaOccupiedByOtherActor(actor, mDestination))
|
|
continue;
|
|
|
|
if (isWaterCreature || isFlyingCreature)
|
|
mPathFinder.buildStraightPath(mDestination);
|
|
else
|
|
mPathFinder.buildPathByNavMesh(actor, currentPosition, mDestination, halfExtents, navigatorFlags,
|
|
areaCosts);
|
|
|
|
if (mPathFinder.isPathConstructed())
|
|
{
|
|
storage.setState(AiWanderStorage::Wander_Walking, true);
|
|
mHasDestination = true;
|
|
mUsePathgrid = false;
|
|
}
|
|
|
|
break;
|
|
} while (--attempts);
|
|
}
|
|
|
|
/*
|
|
* Returns true if the position provided is above water.
|
|
*/
|
|
bool AiWander::destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination) {
|
|
float heightToGroundOrWater = MWBase::Environment::get().getWorld()->getDistToNearestRayHit(destination, osg::Vec3f(0,0,-1), 1000.0, true);
|
|
osg::Vec3f positionBelowSurface = destination;
|
|
positionBelowSurface[2] = positionBelowSurface[2] - heightToGroundOrWater - 1.0f;
|
|
return MWBase::Environment::get().getWorld()->isUnderwater(actor.getCell(), positionBelowSurface);
|
|
}
|
|
|
|
void AiWander::completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage) {
|
|
stopWalking(actor);
|
|
mObstacleCheck.clear();
|
|
storage.setState(AiWanderStorage::Wander_IdleNow);
|
|
}
|
|
|
|
void AiWander::doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage)
|
|
{
|
|
switch (storage.mState)
|
|
{
|
|
case AiWanderStorage::Wander_IdleNow:
|
|
onIdleStatePerFrameActions(actor, duration, storage);
|
|
break;
|
|
|
|
case AiWanderStorage::Wander_Walking:
|
|
onWalkingStatePerFrameActions(actor, duration, storage);
|
|
break;
|
|
|
|
case AiWanderStorage::Wander_ChooseAction:
|
|
onChooseActionStatePerFrameActions(actor, storage);
|
|
break;
|
|
|
|
case AiWanderStorage::Wander_MoveNow:
|
|
break; // nothing to do
|
|
|
|
default:
|
|
// should never get here
|
|
assert(false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void AiWander::onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage)
|
|
{
|
|
// Check if an idle actor is too far from all allowed nodes or too close to a door - if so start walking.
|
|
storage.mCheckIdlePositionTimer += duration;
|
|
|
|
if (storage.mCheckIdlePositionTimer >= IDLE_POSITION_CHECK_INTERVAL && !isStationary())
|
|
{
|
|
storage.mCheckIdlePositionTimer = 0; // restart timer
|
|
static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f;
|
|
if (proximityToDoor(actor, distance) || !isNearAllowedNode(actor, storage, distance))
|
|
{
|
|
storage.setState(AiWanderStorage::Wander_MoveNow);
|
|
storage.mTrimCurrentNode = false; // just in case
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check if idle animation finished
|
|
GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor);
|
|
if (!checkIdle(actor, storage.mIdleAnimation) && (greetingState == Greet_Done || greetingState == Greet_None))
|
|
{
|
|
if (mPathFinder.isPathConstructed())
|
|
storage.setState(AiWanderStorage::Wander_Walking);
|
|
else
|
|
storage.setState(AiWanderStorage::Wander_ChooseAction);
|
|
}
|
|
}
|
|
|
|
bool AiWander::isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const
|
|
{
|
|
const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3();
|
|
auto cell = actor.getCell()->getCell();
|
|
for (const ESM::Pathgrid::Point& node : storage.mAllowedNodes)
|
|
{
|
|
osg::Vec3f point(node.mX, node.mY, node.mZ);
|
|
Misc::CoordinateConverter(cell).toWorld(point);
|
|
if ((actorPos - point).length2() < distance * distance)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage)
|
|
{
|
|
// Is there no destination or are we there yet?
|
|
if ((!mPathFinder.isPathConstructed()) || pathTo(actor, osg::Vec3f(mPathFinder.getPath().back()), duration, DESTINATION_TOLERANCE))
|
|
{
|
|
stopWalking(actor);
|
|
storage.setState(AiWanderStorage::Wander_ChooseAction);
|
|
}
|
|
else
|
|
{
|
|
// have not yet reached the destination
|
|
evadeObstacles(actor, storage);
|
|
}
|
|
}
|
|
|
|
void AiWander::onChooseActionStatePerFrameActions(const MWWorld::Ptr& actor, AiWanderStorage& storage)
|
|
{
|
|
// Wait while fully stop before starting idle animation (important if "smooth movement" is enabled).
|
|
if (actor.getClass().getCurrentSpeed(actor) > 0)
|
|
return;
|
|
|
|
unsigned short idleAnimation = getRandomIdle();
|
|
storage.mIdleAnimation = idleAnimation;
|
|
|
|
if (!idleAnimation && mDistance)
|
|
{
|
|
storage.setState(AiWanderStorage::Wander_MoveNow);
|
|
return;
|
|
}
|
|
if(idleAnimation)
|
|
{
|
|
if(std::find(storage.mBadIdles.begin(), storage.mBadIdles.end(), idleAnimation)==storage.mBadIdles.end())
|
|
{
|
|
if(!playIdle(actor, idleAnimation))
|
|
{
|
|
storage.mBadIdles.push_back(idleAnimation);
|
|
storage.setState(AiWanderStorage::Wander_ChooseAction);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
storage.setState(AiWanderStorage::Wander_IdleNow);
|
|
}
|
|
|
|
void AiWander::evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage)
|
|
{
|
|
if (mUsePathgrid)
|
|
{
|
|
const auto halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor);
|
|
mPathFinder.buildPathByNavMeshToNextPoint(actor, halfExtents, getNavigatorFlags(actor),
|
|
getAreaCosts(actor));
|
|
}
|
|
|
|
if (mObstacleCheck.isEvading())
|
|
{
|
|
// first check if we're walking into a door
|
|
static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance();
|
|
if (proximityToDoor(actor, distance))
|
|
{
|
|
// remove allowed points then select another random destination
|
|
storage.mTrimCurrentNode = true;
|
|
trimAllowedNodes(storage.mAllowedNodes, mPathFinder);
|
|
mObstacleCheck.clear();
|
|
stopWalking(actor);
|
|
storage.setState(AiWanderStorage::Wander_MoveNow);
|
|
}
|
|
|
|
storage.mStuckCount++; // TODO: maybe no longer needed
|
|
}
|
|
|
|
// if stuck for sufficiently long, act like current location was the destination
|
|
if (storage.mStuckCount >= getCountBeforeReset(actor)) // something has gone wrong, reset
|
|
{
|
|
mObstacleCheck.clear();
|
|
stopWalking(actor);
|
|
storage.setState(AiWanderStorage::Wander_ChooseAction);
|
|
storage.mStuckCount = 0;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void AiWander::setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos)
|
|
{
|
|
unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size());
|
|
ESM::Pathgrid::Point dest(storage.mAllowedNodes[randNode]);
|
|
|
|
ToWorldCoordinates(dest, actor.getCell()->getCell());
|
|
|
|
// actor position is already in world coordinates
|
|
const osg::Vec3f start = actorPos.asVec3();
|
|
|
|
// don't take shortcuts for wandering
|
|
const osg::Vec3f destVec3f = PathFinder::makeOsgVec3(dest);
|
|
mPathFinder.buildPathByPathgrid(start, destVec3f, actor.getCell(), getPathGridGraph(actor.getCell()));
|
|
|
|
if (mPathFinder.isPathConstructed())
|
|
{
|
|
mDestination = destVec3f;
|
|
mHasDestination = true;
|
|
mUsePathgrid = 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);
|
|
// check if mCurrentNode was taken out of mAllowedNodes
|
|
if (storage.mTrimCurrentNode && storage.mAllowedNodes.size() > 1)
|
|
storage.mTrimCurrentNode = false;
|
|
else
|
|
storage.mAllowedNodes.push_back(storage.mCurrentNode);
|
|
storage.mCurrentNode = temp;
|
|
|
|
storage.setState(AiWanderStorage::Wander_Walking);
|
|
}
|
|
// Choose a different node and delete this one from possible nodes because it is uncreachable:
|
|
else
|
|
storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode);
|
|
}
|
|
|
|
void AiWander::ToWorldCoordinates(ESM::Pathgrid::Point& point, const ESM::Cell * cell)
|
|
{
|
|
Misc::CoordinateConverter(cell).toWorld(point);
|
|
}
|
|
|
|
void AiWander::trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes,
|
|
const PathFinder& pathfinder)
|
|
{
|
|
// TODO: how to add these back in once the door opens?
|
|
// Idea: keep a list of detected closed doors (see aicombat.cpp)
|
|
// Every now and then check whether one of the doors is opened. (maybe
|
|
// at the end of playing idle?) If the door is opened then re-calculate
|
|
// allowed nodes starting from the spawn point.
|
|
auto paths = pathfinder.getPath();
|
|
while(paths.size() >= 2)
|
|
{
|
|
const auto pt = paths.back();
|
|
for(unsigned int j = 0; j < nodes.size(); j++)
|
|
{
|
|
// FIXME: doesn't handle a door with the same X/Y
|
|
// coordinates but with a different Z
|
|
if (std::abs(nodes[j].mX - pt.x()) <= 0.5 && std::abs(nodes[j].mY - pt.y()) <= 0.5)
|
|
{
|
|
nodes.erase(nodes.begin() + j);
|
|
break;
|
|
}
|
|
}
|
|
paths.pop_back();
|
|
}
|
|
}
|
|
|
|
void AiWander::stopWalking(const MWWorld::Ptr& actor)
|
|
{
|
|
mPathFinder.clearPath();
|
|
mHasDestination = false;
|
|
stopMovement(actor);
|
|
}
|
|
|
|
bool AiWander::playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect)
|
|
{
|
|
if ((GroupIndex_MinIdle <= idleSelect) && (idleSelect <= GroupIndex_MaxIdle))
|
|
{
|
|
const std::string& groupName = sIdleSelectToGroupName[idleSelect - GroupIndex_MinIdle];
|
|
return MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, groupName, 0, 1);
|
|
}
|
|
else
|
|
{
|
|
Log(Debug::Verbose) << "Attempted to play out of range idle animation \"" << idleSelect << "\" for " << actor.getCellRef().getRefId();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool AiWander::checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect)
|
|
{
|
|
if ((GroupIndex_MinIdle <= idleSelect) && (idleSelect <= GroupIndex_MaxIdle))
|
|
{
|
|
const std::string& groupName = sIdleSelectToGroupName[idleSelect - GroupIndex_MinIdle];
|
|
return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, groupName);
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
short unsigned AiWander::getRandomIdle()
|
|
{
|
|
unsigned short idleRoll = 0;
|
|
short unsigned selectedAnimation = 0;
|
|
|
|
for(unsigned int counter = 0; counter < mIdle.size(); counter++)
|
|
{
|
|
static float fIdleChanceMultiplier = MWBase::Environment::get().getWorld()->getStore()
|
|
.get<ESM::GameSetting>().find("fIdleChanceMultiplier")->mValue.getFloat();
|
|
|
|
unsigned short idleChance = static_cast<unsigned short>(fIdleChanceMultiplier * mIdle[counter]);
|
|
unsigned short randSelect = (int)(Misc::Rng::rollProbability() * int(100 / fIdleChanceMultiplier));
|
|
if(randSelect < idleChance && randSelect > idleRoll)
|
|
{
|
|
selectedAnimation = counter + GroupIndex_MinIdle;
|
|
idleRoll = randSelect;
|
|
}
|
|
}
|
|
return selectedAnimation;
|
|
}
|
|
|
|
void AiWander::fastForward(const MWWorld::Ptr& actor, AiState &state)
|
|
{
|
|
// Update duration counter
|
|
mRemainingDuration--;
|
|
if (mDistance == 0)
|
|
return;
|
|
|
|
AiWanderStorage& storage = state.get<AiWanderStorage>();
|
|
if (storage.mPopulateAvailableNodes)
|
|
getAllowedNodes(actor, actor.getCell()->getCell(), storage);
|
|
|
|
if (storage.mAllowedNodes.empty())
|
|
return;
|
|
|
|
int index = Misc::Rng::rollDice(storage.mAllowedNodes.size());
|
|
ESM::Pathgrid::Point dest = storage.mAllowedNodes[index];
|
|
ESM::Pathgrid::Point worldDest = dest;
|
|
ToWorldCoordinates(worldDest, actor.getCell()->getCell());
|
|
|
|
bool isPathGridOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(PathFinder::makeOsgVec3(worldDest), 60);
|
|
|
|
// add offset only if the selected pathgrid is occupied by another actor
|
|
if (isPathGridOccupied)
|
|
{
|
|
ESM::Pathgrid::PointList points;
|
|
getNeighbouringNodes(dest, actor.getCell(), points);
|
|
|
|
// there are no neighbouring nodes, nowhere to move
|
|
if (points.empty())
|
|
return;
|
|
|
|
int initialSize = points.size();
|
|
bool isOccupied = false;
|
|
// AI will try to move the NPC towards every neighboring node until suitable place will be found
|
|
for (int i = 0; i < initialSize; i++)
|
|
{
|
|
int randomIndex = Misc::Rng::rollDice(points.size());
|
|
ESM::Pathgrid::Point connDest = points[randomIndex];
|
|
|
|
// add an offset towards random neighboring node
|
|
osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - PathFinder::makeOsgVec3(dest);
|
|
float length = dir.length();
|
|
dir.normalize();
|
|
|
|
for (int j = 1; j <= 3; j++)
|
|
{
|
|
// move for 5-15% towards random neighboring node
|
|
dest = PathFinder::makePathgridPoint(PathFinder::makeOsgVec3(dest) + dir * (j * 5 * length / 100.f));
|
|
worldDest = dest;
|
|
ToWorldCoordinates(worldDest, actor.getCell()->getCell());
|
|
|
|
isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(PathFinder::makeOsgVec3(worldDest), 60);
|
|
|
|
if (!isOccupied)
|
|
break;
|
|
}
|
|
|
|
if (!isOccupied)
|
|
break;
|
|
|
|
// Will try an another neighboring node
|
|
points.erase(points.begin()+randomIndex);
|
|
}
|
|
|
|
// there is no free space, nowhere to move
|
|
if (isOccupied)
|
|
return;
|
|
}
|
|
|
|
// place above to prevent moving inside objects, e.g. stairs, because a vector between pathgrids can be underground.
|
|
// Adding 20 in adjustPosition() is not enough.
|
|
dest.mZ += 60;
|
|
|
|
ToWorldCoordinates(dest, actor.getCell()->getCell());
|
|
|
|
state.moveIn(new AiWanderStorage());
|
|
|
|
MWBase::Environment::get().getWorld()->moveObject(actor, static_cast<float>(dest.mX),
|
|
static_cast<float>(dest.mY), static_cast<float>(dest.mZ));
|
|
actor.getClass().adjustPosition(actor, false);
|
|
}
|
|
|
|
void AiWander::getNeighbouringNodes(ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points)
|
|
{
|
|
const ESM::Pathgrid *pathgrid =
|
|
MWBase::Environment::get().getWorld()->getStore().get<ESM::Pathgrid>().search(*currentCell->getCell());
|
|
|
|
int index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest));
|
|
|
|
getPathGridGraph(currentCell).getNeighbouringPoints(index, points);
|
|
}
|
|
|
|
void AiWander::getAllowedNodes(const MWWorld::Ptr& actor, const ESM::Cell* cell, AiWanderStorage& storage)
|
|
{
|
|
// infrequently used, therefore no benefit in caching it as a member
|
|
const ESM::Pathgrid *
|
|
pathgrid = MWBase::Environment::get().getWorld()->getStore().get<ESM::Pathgrid>().search(*cell);
|
|
const MWWorld::CellStore* cellStore = actor.getCell();
|
|
|
|
storage.mAllowedNodes.clear();
|
|
|
|
// 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
|
|
// Note: In order to wander, need at least two points.
|
|
if(!pathgrid || (pathgrid->mPoints.size() < 2))
|
|
storage.mCanWanderAlongPathGrid = false;
|
|
|
|
// 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).
|
|
// ... pathgrids don't usually include water, so swimmers ignore them
|
|
if (mDistance && storage.mCanWanderAlongPathGrid && !actor.getClass().isPureWaterCreature(actor))
|
|
{
|
|
// get NPC's position in local (i.e. cell) coordinates
|
|
osg::Vec3f npcPos(mInitialActorPosition);
|
|
Misc::CoordinateConverter(cell).toLocal(npcPos);
|
|
|
|
// Find closest pathgrid point
|
|
int closestPointIndex = PathFinder::getClosestPoint(pathgrid, npcPos);
|
|
|
|
// mAllowedNodes for this actor with pathgrid point indexes based on mDistance
|
|
// and if the point is connected to the closest current point
|
|
// NOTE: mPoints and mAllowedNodes are in local coordinates
|
|
int pointIndex = 0;
|
|
for(unsigned int counter = 0; counter < pathgrid->mPoints.size(); counter++)
|
|
{
|
|
osg::Vec3f nodePos(PathFinder::makeOsgVec3(pathgrid->mPoints[counter]));
|
|
if((npcPos - nodePos).length2() <= mDistance * mDistance &&
|
|
getPathGridGraph(cellStore).isPointConnected(closestPointIndex, counter))
|
|
{
|
|
storage.mAllowedNodes.push_back(pathgrid->mPoints[counter]);
|
|
pointIndex = counter;
|
|
}
|
|
}
|
|
if (storage.mAllowedNodes.size() == 1)
|
|
{
|
|
AddNonPathGridAllowedPoints(npcPos, pathgrid, pointIndex, storage);
|
|
}
|
|
if(!storage.mAllowedNodes.empty())
|
|
{
|
|
SetCurrentNodeToClosestAllowedNode(npcPos, storage);
|
|
}
|
|
}
|
|
|
|
storage.mPopulateAvailableNodes = false;
|
|
}
|
|
|
|
// When only one path grid point in wander distance,
|
|
// additional points for NPC to wander to are:
|
|
// 1. NPC's initial location
|
|
// 2. Partway along the path between the point and its connected points.
|
|
void AiWander::AddNonPathGridAllowedPoints(osg::Vec3f npcPos, const ESM::Pathgrid * pathGrid, int pointIndex, AiWanderStorage& storage)
|
|
{
|
|
storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(npcPos));
|
|
for (auto& edge : pathGrid->mEdges)
|
|
{
|
|
if (edge.mV0 == pointIndex)
|
|
{
|
|
AddPointBetweenPathGridPoints(pathGrid->mPoints[edge.mV0], pathGrid->mPoints[edge.mV1], storage);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AiWander::AddPointBetweenPathGridPoints(const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage)
|
|
{
|
|
osg::Vec3f vectorStart = PathFinder::makeOsgVec3(start);
|
|
osg::Vec3f delta = PathFinder::makeOsgVec3(end) - vectorStart;
|
|
float length = delta.length();
|
|
delta.normalize();
|
|
|
|
int distance = std::max(mDistance / 2, MINIMUM_WANDER_DISTANCE);
|
|
|
|
// must not travel longer than distance between waypoints or NPC goes past waypoint
|
|
distance = std::min(distance, static_cast<int>(length));
|
|
delta *= distance;
|
|
storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(vectorStart + delta));
|
|
}
|
|
|
|
void AiWander::SetCurrentNodeToClosestAllowedNode(const osg::Vec3f& npcPos, AiWanderStorage& storage)
|
|
{
|
|
float distanceToClosestNode = std::numeric_limits<float>::max();
|
|
unsigned int index = 0;
|
|
for (unsigned int counterThree = 0; counterThree < storage.mAllowedNodes.size(); counterThree++)
|
|
{
|
|
osg::Vec3f nodePos(PathFinder::makeOsgVec3(storage.mAllowedNodes[counterThree]));
|
|
float tempDist = (npcPos - nodePos).length2();
|
|
if (tempDist < distanceToClosestNode)
|
|
{
|
|
index = counterThree;
|
|
distanceToClosestNode = tempDist;
|
|
}
|
|
}
|
|
storage.mCurrentNode = storage.mAllowedNodes[index];
|
|
storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + index);
|
|
}
|
|
|
|
void AiWander::writeState(ESM::AiSequence::AiSequence &sequence) const
|
|
{
|
|
float remainingDuration;
|
|
if (mRemainingDuration > 0 && mRemainingDuration < 24)
|
|
remainingDuration = mRemainingDuration;
|
|
else
|
|
remainingDuration = mDuration;
|
|
|
|
std::unique_ptr<ESM::AiSequence::AiWander> wander(new ESM::AiSequence::AiWander());
|
|
wander->mData.mDistance = mDistance;
|
|
wander->mData.mDuration = mDuration;
|
|
wander->mData.mTimeOfDay = mTimeOfDay;
|
|
wander->mDurationData.mRemainingDuration = remainingDuration;
|
|
assert (mIdle.size() == 8);
|
|
for (int i=0; i<8; ++i)
|
|
wander->mData.mIdle[i] = mIdle[i];
|
|
wander->mData.mShouldRepeat = mOptions.mRepeat;
|
|
wander->mStoredInitialActorPosition = mStoredInitialActorPosition;
|
|
if (mStoredInitialActorPosition)
|
|
wander->mInitialActorPosition = mInitialActorPosition;
|
|
|
|
ESM::AiSequence::AiPackageContainer package;
|
|
package.mType = ESM::AiSequence::Ai_Wander;
|
|
package.mPackage = wander.release();
|
|
sequence.mPackages.push_back(package);
|
|
}
|
|
|
|
AiWander::AiWander (const ESM::AiSequence::AiWander* wander)
|
|
: TypedAiPackage<AiWander>(makeDefaultOptions().withRepeat(wander->mData.mShouldRepeat != 0))
|
|
, mDistance(std::max(static_cast<short>(0), wander->mData.mDistance))
|
|
, mDuration(std::max(static_cast<short>(0), wander->mData.mDuration))
|
|
, mRemainingDuration(wander->mDurationData.mRemainingDuration)
|
|
, mTimeOfDay(wander->mData.mTimeOfDay)
|
|
, mIdle(getInitialIdle(wander->mData.mIdle))
|
|
, mStoredInitialActorPosition(wander->mStoredInitialActorPosition)
|
|
, mHasDestination(false)
|
|
, mDestination(osg::Vec3f(0, 0, 0))
|
|
, mUsePathgrid(false)
|
|
{
|
|
if (mStoredInitialActorPosition)
|
|
mInitialActorPosition = wander->mInitialActorPosition;
|
|
if (mRemainingDuration <= 0 || mRemainingDuration >= 24)
|
|
mRemainingDuration = mDuration;
|
|
}
|
|
}
|