mirror of
https://github.com/TES3MP/openmw-tes3mp.git
synced 2025-02-28 07:09:42 +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/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…
Reference in a new issue