From b8387825575a2cf78c619de7697187e98e5960cb Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Mon, 31 Aug 2020 01:12:52 +0200 Subject: [PATCH] Avoid collisions between actors. --- apps/openmw/mwmechanics/actors.cpp | 131 +++++++++++++++++++++++++++ apps/openmw/mwmechanics/actors.hpp | 2 + apps/openmw/mwmechanics/aiwander.cpp | 29 ++++-- apps/openmw/mwmechanics/aiwander.hpp | 7 +- files/settings-default.cfg | 6 ++ 5 files changed, 165 insertions(+), 10 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 0b268214b5..02b71e3d58 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #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(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; diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index bd5a14c0d7..2668a71316 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -63,6 +63,8 @@ namespace MWMechanics void purgeSpellEffects (int casterActorId); + void predictAndAvoidCollisions(); + public: Actors(); diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index cef97fa9be..375209a250 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -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? diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 4138c3dea5..4165cebbd6 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -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; diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 63df884e56..fab5fe5692 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -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