mirror of
https://github.com/OpenMW/openmw.git
synced 2025-01-21 06:23:53 +00:00
Avoid collisions between actors.
This commit is contained in:
parent
79a72e4b44
commit
b838782557
5 changed files with 165 additions and 10 deletions
|
@ -6,6 +6,7 @@
|
||||||
#include <components/sceneutil/positionattitudetransform.hpp>
|
#include <components/sceneutil/positionattitudetransform.hpp>
|
||||||
#include <components/debug/debuglog.hpp>
|
#include <components/debug/debuglog.hpp>
|
||||||
#include <components/misc/rng.hpp>
|
#include <components/misc/rng.hpp>
|
||||||
|
#include <components/misc/mathutil.hpp>
|
||||||
#include <components/settings/settings.hpp>
|
#include <components/settings/settings.hpp>
|
||||||
|
|
||||||
#include "../mwworld/esmstore.hpp"
|
#include "../mwworld/esmstore.hpp"
|
||||||
|
@ -36,6 +37,7 @@
|
||||||
#include "aicombataction.hpp"
|
#include "aicombataction.hpp"
|
||||||
#include "aifollow.hpp"
|
#include "aifollow.hpp"
|
||||||
#include "aipursue.hpp"
|
#include "aipursue.hpp"
|
||||||
|
#include "aiwander.hpp"
|
||||||
#include "actor.hpp"
|
#include "actor.hpp"
|
||||||
#include "summoning.hpp"
|
#include "summoning.hpp"
|
||||||
#include "combat.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)
|
void Actors::update (float duration, bool paused)
|
||||||
{
|
{
|
||||||
if(!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;
|
timerUpdateAITargets += duration;
|
||||||
timerUpdateHeadTrack += duration;
|
timerUpdateHeadTrack += duration;
|
||||||
timerUpdateEquippedLight += duration;
|
timerUpdateEquippedLight += duration;
|
||||||
|
|
|
@ -63,6 +63,8 @@ namespace MWMechanics
|
||||||
|
|
||||||
void purgeSpellEffects (int casterActorId);
|
void purgeSpellEffects (int casterActorId);
|
||||||
|
|
||||||
|
void predictAndAvoidCollisions();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
Actors();
|
Actors();
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
namespace MWMechanics
|
namespace MWMechanics
|
||||||
{
|
{
|
||||||
static const int COUNT_BEFORE_RESET = 10;
|
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
|
// to prevent overcrowding
|
||||||
static const int DESTINATION_TOLERANCE = 64;
|
static const int DESTINATION_TOLERANCE = 64;
|
||||||
|
@ -425,15 +425,14 @@ namespace MWMechanics
|
||||||
|
|
||||||
void AiWander::onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage)
|
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
|
// Check if an idle actor is too far from all allowed nodes or too close to a door - if so start walking.
|
||||||
storage.mDoorCheckDuration += duration;
|
storage.mCheckIdlePositionTimer += duration;
|
||||||
|
|
||||||
if (storage.mDoorCheckDuration >= DOOR_CHECK_INTERVAL)
|
if (storage.mCheckIdlePositionTimer >= IDLE_POSITION_CHECK_INTERVAL && !isStationary())
|
||||||
{
|
{
|
||||||
storage.mDoorCheckDuration = 0; // restart timer
|
storage.mCheckIdlePositionTimer = 0; // restart timer
|
||||||
static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance();
|
static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f;
|
||||||
if (mDistance && // actor is not intended to be stationary
|
if (proximityToDoor(actor, distance) || !isNearAllowedNode(actor, storage, distance))
|
||||||
proximityToDoor(actor, distance*1.6f))
|
|
||||||
{
|
{
|
||||||
storage.setState(AiWanderStorage::Wander_MoveNow);
|
storage.setState(AiWanderStorage::Wander_MoveNow);
|
||||||
storage.mTrimCurrentNode = false; // just in case
|
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)
|
void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage)
|
||||||
{
|
{
|
||||||
// Is there no destination or are we there yet?
|
// Is there no destination or are we there yet?
|
||||||
|
|
|
@ -53,7 +53,7 @@ namespace MWMechanics
|
||||||
ESM::Pathgrid::Point mCurrentNode;
|
ESM::Pathgrid::Point mCurrentNode;
|
||||||
bool mTrimCurrentNode;
|
bool mTrimCurrentNode;
|
||||||
|
|
||||||
float mDoorCheckDuration;
|
float mCheckIdlePositionTimer;
|
||||||
int mStuckCount;
|
int mStuckCount;
|
||||||
|
|
||||||
AiWanderStorage():
|
AiWanderStorage():
|
||||||
|
@ -66,7 +66,7 @@ namespace MWMechanics
|
||||||
mPopulateAvailableNodes(true),
|
mPopulateAvailableNodes(true),
|
||||||
mAllowedNodes(),
|
mAllowedNodes(),
|
||||||
mTrimCurrentNode(false),
|
mTrimCurrentNode(false),
|
||||||
mDoorCheckDuration(0), // TODO: maybe no longer needed
|
mCheckIdlePositionTimer(0),
|
||||||
mStuckCount(0)
|
mStuckCount(0)
|
||||||
{};
|
{};
|
||||||
|
|
||||||
|
@ -117,6 +117,8 @@ namespace MWMechanics
|
||||||
return mDestination;
|
return mDestination;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isStationary() const { return mDistance == 0; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void stopWalking(const MWWorld::Ptr& actor);
|
void stopWalking(const MWWorld::Ptr& actor);
|
||||||
|
|
||||||
|
@ -137,6 +139,7 @@ namespace MWMechanics
|
||||||
void wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance);
|
void wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance);
|
||||||
bool destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination);
|
bool destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination);
|
||||||
void completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage);
|
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 mDistance; // how far the actor can wander from the spawn point
|
||||||
const int mDuration;
|
const int mDuration;
|
||||||
|
|
|
@ -328,6 +328,12 @@ turn to movement direction = false
|
||||||
# Makes all movements of NPCs and player more smooth.
|
# Makes all movements of NPCs and player more smooth.
|
||||||
smooth movement = false
|
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.
|
# Makes player swim a bit upward from the line of sight.
|
||||||
swim upward correction = false
|
swim upward correction = false
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue