Avoid collisions between actors.

pull/593/head
Petr Mikheev 4 years ago
parent 79a72e4b44
commit b838782557

@ -6,6 +6,7 @@
#include <components/sceneutil/positionattitudetransform.hpp>
#include <components/debug/debuglog.hpp>
#include <components/misc/rng.hpp>
#include <components/misc/mathutil.hpp>
#include <components/settings/settings.hpp>
#include "../mwworld/esmstore.hpp"
@ -36,6 +37,7 @@
#include "aicombataction.hpp"
#include "aifollow.hpp"
#include "aipursue.hpp"
#include "aiwander.hpp"
#include "actor.hpp"
#include "summoning.hpp"
#include "combat.hpp"
@ -1664,6 +1666,131 @@ namespace MWMechanics
}
void Actors::predictAndAvoidCollisions()
{
const float minGap = 10.f;
const float maxDistToCheck = 100.f;
const float maxTimeToCheck = 1.f;
static const bool giveWayWhenIdle = Settings::Manager::getBool("NPCs give way", "Game");
MWWorld::Ptr player = getPlayer();
MWBase::World* world = MWBase::Environment::get().getWorld();
for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter)
{
const MWWorld::Ptr& ptr = iter->first;
if (ptr == player)
continue; // Don't interfere with player controls.
Movement& movement = ptr.getClass().getMovementSettings(ptr);
osg::Vec2f origMovement(movement.mPosition[0], movement.mPosition[1]);
bool isMoving = origMovement.length2() > 0.01;
// Moving NPCs always should avoid collisions.
// Standing NPCs give way to moving ones if they are not in combat (or pursue) mode and either
// follow player or have a AIWander package with non-empty wander area.
bool shouldAvoidCollision = isMoving;
bool shouldTurnToApproachingActor = !isMoving;
MWWorld::Ptr currentTarget; // Combat or pursue target (NPCs should not avoid collision with their targets).
for (const auto& package : ptr.getClass().getCreatureStats(ptr).getAiSequence())
{
if (package->getTypeId() == AiPackageTypeId::Follow)
shouldAvoidCollision = true;
else if (package->getTypeId() == AiPackageTypeId::Wander && giveWayWhenIdle)
{
if (!dynamic_cast<const AiWander*>(package.get())->isStationary())
shouldAvoidCollision = true;
}
else if (package->getTypeId() == AiPackageTypeId::Combat || package->getTypeId() == AiPackageTypeId::Pursue)
{
currentTarget = package->getTarget();
shouldAvoidCollision = isMoving;
shouldTurnToApproachingActor = false;
break;
}
}
if (!shouldAvoidCollision)
continue;
float maxSpeed = ptr.getClass().getMaxSpeed(ptr);
osg::Vec2f baseSpeed = origMovement * maxSpeed;
osg::Vec3f basePos = ptr.getRefData().getPosition().asVec3();
float baseRotZ = ptr.getRefData().getPosition().rot[2];
osg::Vec3f halfExtents = world->getHalfExtents(ptr);
float timeToCollision = maxTimeToCheck;
osg::Vec2f movementCorrection(0, 0);
float angleToApproachingActor = 0;
// Iterate through all other actors and predict collisions.
for(PtrActorMap::iterator otherIter(mActors.begin()); otherIter != mActors.end(); ++otherIter)
{
const MWWorld::Ptr& otherPtr = otherIter->first;
if (otherPtr == ptr || otherPtr == currentTarget)
continue;
osg::Vec3f otherHalfExtents = world->getHalfExtents(otherPtr);
osg::Vec3f deltaPos = otherPtr.getRefData().getPosition().asVec3() - basePos;
osg::Vec2f relPos = Misc::rotateVec2f(osg::Vec2f(deltaPos.x(), deltaPos.y()), baseRotZ);
// Ignore actors which are not close enough or come from behind.
if (deltaPos.length2() > maxDistToCheck * maxDistToCheck || relPos.y() < 0)
continue;
// Don't check for a collision if vertical distance is greater then the actor's height.
if (deltaPos.z() > halfExtents.z() * 2 || deltaPos.z() < -otherHalfExtents.z() * 2)
continue;
osg::Vec3f speed = otherPtr.getClass().getMovementSettings(otherPtr).asVec3() *
otherPtr.getClass().getMaxSpeed(otherPtr);
float rotZ = otherPtr.getRefData().getPosition().rot[2];
osg::Vec2f relSpeed = Misc::rotateVec2f(osg::Vec2f(speed.x(), speed.y()), baseRotZ - rotZ) - baseSpeed;
float collisionDist = minGap + world->getHalfExtents(ptr).x() + world->getHalfExtents(otherPtr).x();
collisionDist = std::min(collisionDist, relPos.length());
// Find the earliest `t` when |relPos + relSpeed * t| == collisionDist.
float vr = relPos.x() * relSpeed.x() + relPos.y() * relSpeed.y();
float v2 = relSpeed.length2();
float Dh = vr * vr - v2 * (relPos.length2() - collisionDist * collisionDist);
if (Dh <= 0 || v2 == 0)
continue; // No solution; distance is always >= collisionDist.
float t = (-vr - std::sqrt(Dh)) / v2;
if (t < 0 || t > timeToCollision)
continue;
// Check visibility and awareness last as it's expensive.
if (!MWBase::Environment::get().getWorld()->getLOS(otherPtr, ptr))
continue;
if (!MWBase::Environment::get().getMechanicsManager()->awarenessCheck(otherPtr, ptr))
continue;
timeToCollision = t;
angleToApproachingActor = std::atan2(deltaPos.x(), deltaPos.y());
osg::Vec2f posAtT = relPos + relSpeed * t;
float coef = (posAtT.x() * relSpeed.x() + posAtT.y() * relSpeed.y()) / (collisionDist * maxSpeed);
movementCorrection = posAtT * coef;
// Step to the side rather than backward. Otherwise player will be able to push the NPC far away from it's original location.
movementCorrection.y() = std::max(0.f, movementCorrection.y());
}
if (timeToCollision < maxTimeToCheck)
{
// Try to evade the nearest collision.
osg::Vec2f newMovement = origMovement + movementCorrection;
if (isMoving)
{ // Keep the original speed.
newMovement.normalize();
newMovement *= origMovement.length();
}
movement.mPosition[0] = newMovement.x();
movement.mPosition[1] = newMovement.y();
if (shouldTurnToApproachingActor)
zTurn(ptr, angleToApproachingActor);
}
}
}
void Actors::update (float duration, bool paused)
{
if(!paused)
@ -1838,6 +1965,10 @@ namespace MWMechanics
}
}
static const bool avoidCollisions = Settings::Manager::getBool("NPCs avoid collisions", "Game");
if (avoidCollisions)
predictAndAvoidCollisions();
timerUpdateAITargets += duration;
timerUpdateHeadTrack += duration;
timerUpdateEquippedLight += duration;

@ -63,6 +63,8 @@ namespace MWMechanics
void purgeSpellEffects (int casterActorId);
void predictAndAvoidCollisions();
public:
Actors();

@ -27,7 +27,7 @@
namespace MWMechanics
{
static const int COUNT_BEFORE_RESET = 10;
static const float DOOR_CHECK_INTERVAL = 1.5f;
static const float IDLE_POSITION_CHECK_INTERVAL = 1.5f;
// to prevent overcrowding
static const int DESTINATION_TOLERANCE = 64;
@ -425,15 +425,14 @@ namespace MWMechanics
void AiWander::onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage)
{
// Check if an idle actor is too close to a door - if so start walking
storage.mDoorCheckDuration += duration;
// 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.mDoorCheckDuration >= DOOR_CHECK_INTERVAL)
if (storage.mCheckIdlePositionTimer >= IDLE_POSITION_CHECK_INTERVAL && !isStationary())
{
storage.mDoorCheckDuration = 0; // restart timer
static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance();
if (mDistance && // actor is not intended to be stationary
proximityToDoor(actor, distance*1.6f))
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
@ -452,6 +451,20 @@ namespace MWMechanics
}
}
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?

@ -53,7 +53,7 @@ namespace MWMechanics
ESM::Pathgrid::Point mCurrentNode;
bool mTrimCurrentNode;
float mDoorCheckDuration;
float mCheckIdlePositionTimer;
int mStuckCount;
AiWanderStorage():
@ -66,7 +66,7 @@ namespace MWMechanics
mPopulateAvailableNodes(true),
mAllowedNodes(),
mTrimCurrentNode(false),
mDoorCheckDuration(0), // TODO: maybe no longer needed
mCheckIdlePositionTimer(0),
mStuckCount(0)
{};
@ -117,6 +117,8 @@ namespace MWMechanics
return mDestination;
}
bool isStationary() const { return mDistance == 0; }
private:
void stopWalking(const MWWorld::Ptr& actor);
@ -137,6 +139,7 @@ namespace MWMechanics
void wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance);
bool destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination);
void completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage);
bool isNearAllowedNode(const MWWorld::Ptr &actor, const AiWanderStorage& storage, float distance) const;
const int mDistance; // how far the actor can wander from the spawn point
const int mDuration;

@ -328,6 +328,12 @@ turn to movement direction = false
# Makes all movements of NPCs and player more smooth.
smooth movement = false
# All actors avoid collisions with other actors.
NPCs avoid collisions = false
# Give way to moving actors when idle. Requires 'NPCs avoid collisions' to be enabled.
NPCs give way = true
# Makes player swim a bit upward from the line of sight.
swim upward correction = false

Loading…
Cancel
Save