diff --git a/CHANGELOG.md b/CHANGELOG.md index 2287943ea..db30d2fd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ Bug #5557: Diagonal movement is noticeably slower with analogue stick Feature #390: 3rd person look "over the shoulder" Feature #2386: Distant Statics in the form of Object Paging + Feature #4894: Consider actors as obstacles for pathfinding Feature #5297: Add a search function to the "Datafiles" tab of the OpenMW launcher Feature #5362: Show the soul gems' trapped soul in count dialog Feature #5445: Handle NiLines @@ -59,6 +60,7 @@ Feature #5525: Search fields tweaks (utf-8) Feature #5545: Option to allow stealing from an unconscious NPC during combat Feature #5579: MCP SetAngle enhancement + Feature #5610: Actors movement should be smoother Task #5480: Drop Qt4 support Task #5520: Improve cell name autocompleter implementation diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index bc14a9269..2e929faf5 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -88,6 +88,7 @@ bool Launcher::AdvancedPage::loadSettings() loadSettingBool(uncappedDamageFatigueCheckBox, "uncapped damage fatigue", "Game"); loadSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game"); loadSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game"); + loadSettingBool(avoidCollisionsCheckBox, "NPCs avoid collisions", "Game"); int unarmedFactorsStrengthIndex = mEngineSettings.getInt("strength influences hand to hand", "Game"); if (unarmedFactorsStrengthIndex >= 0 && unarmedFactorsStrengthIndex <= 2) unarmedFactorsStrengthComboBox->setCurrentIndex(unarmedFactorsStrengthIndex); @@ -112,6 +113,7 @@ bool Launcher::AdvancedPage::loadSettings() loadSettingBool(shieldSheathingCheckBox, "shield sheathing", "Game"); } loadSettingBool(turnToMovementDirectionCheckBox, "turn to movement direction", "Game"); + loadSettingBool(smoothMovementCheckBox, "smooth movement", "Game"); const bool distantTerrain = mEngineSettings.getBool("distant terrain", "Terrain"); const bool objectPaging = mEngineSettings.getBool("object paging", "Terrain"); @@ -200,6 +202,7 @@ void Launcher::AdvancedPage::saveSettings() saveSettingBool(uncappedDamageFatigueCheckBox, "uncapped damage fatigue", "Game"); saveSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game"); saveSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game"); + saveSettingBool(avoidCollisionsCheckBox, "NPCs avoid collisions", "Game"); int unarmedFactorsStrengthIndex = unarmedFactorsStrengthComboBox->currentIndex(); if (unarmedFactorsStrengthIndex != mEngineSettings.getInt("strength influences hand to hand", "Game")) mEngineSettings.setInt("strength influences hand to hand", "Game", unarmedFactorsStrengthIndex); @@ -220,6 +223,7 @@ void Launcher::AdvancedPage::saveSettings() saveSettingBool(weaponSheathingCheckBox, "weapon sheathing", "Game"); saveSettingBool(shieldSheathingCheckBox, "shield sheathing", "Game"); saveSettingBool(turnToMovementDirectionCheckBox, "turn to movement direction", "Game"); + saveSettingBool(smoothMovementCheckBox, "smooth movement", "Game"); const bool distantTerrain = mEngineSettings.getBool("distant terrain", "Terrain"); const bool objectPaging = mEngineSettings.getBool("object paging", "Terrain"); diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 0dfa7a9e6..3de1a3acc 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" @@ -424,7 +426,7 @@ namespace MWMechanics const osg::Vec3f actor2Pos(targetActor.getRefData().getPosition().asVec3()); float sqrDist = (actor1Pos - actor2Pos).length2(); - if (sqrDist > maxDistance*maxDistance) + if (sqrDist > std::min(maxDistance * maxDistance, sqrHeadTrackDistance)) return; // stop tracking when target is behind the actor @@ -432,10 +434,7 @@ namespace MWMechanics osg::Vec3f targetDirection(actor2Pos - actor1Pos); actorDirection.z() = 0; targetDirection.z() = 0; - actorDirection.normalize(); - targetDirection.normalize(); - if (std::acos(actorDirection * targetDirection) < osg::DegreesToRadians(90.f) - && sqrDist <= sqrHeadTrackDistance + if (actorDirection * targetDirection > 0 && MWBase::Environment::get().getWorld()->getLOS(actor, targetActor) // check LOS and awareness last as it's the most expensive function && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(targetActor, actor)) { @@ -473,6 +472,9 @@ namespace MWMechanics void Actors::updateMovementSpeed(const MWWorld::Ptr& actor) { + if (mSmoothMovement) + return; + CreatureStats &stats = actor.getClass().getCreatureStats(actor); MWMechanics::AiSequence& seq = stats.getAiSequence(); @@ -481,9 +483,10 @@ namespace MWMechanics osg::Vec3f targetPos = seq.getActivePackage().getDestination(); osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); float distance = (targetPos - actorPos).length(); + if (distance < DECELERATE_DISTANCE) { - float speedCoef = std::max(0.7f, 0.1f * (distance/64.f + 2.f)); + float speedCoef = std::max(0.7f, 0.2f + 0.8f * distance / DECELERATE_DISTANCE); auto& movement = actor.getClass().getMovementSettings(actor); movement.mPosition[0] *= speedCoef; movement.mPosition[1] *= speedCoef; @@ -587,8 +590,11 @@ namespace MWMechanics if (!actorState.isTurningToPlayer()) { - actorState.setAngleToPlayer(std::atan2(dir.x(), dir.y())); - actorState.setTurningToPlayer(true); + float angle = std::atan2(dir.x(), dir.y()); + actorState.setAngleToPlayer(angle); + float deltaAngle = Misc::normalizeAngle(angle - actor.getRefData().getPosition().rot[2]); + if (!mSmoothMovement || std::abs(deltaAngle) > osg::DegreesToRadians(60.f)) + actorState.setTurningToPlayer(true); } } @@ -1460,7 +1466,7 @@ namespace MWMechanics } } - Actors::Actors() + Actors::Actors() : mSmoothMovement(Settings::Manager::getBool("smooth movement", "Game")) { mTimerDisposeSummonsCorpses = 0.2f; // We should add a delay between summoned creature death and its corpse despawning @@ -1659,6 +1665,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) @@ -1769,14 +1900,12 @@ namespace MWMechanics MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first); bool firstPersonPlayer = isPlayer && world->isFirstPerson(); + bool inCombatOrPursue = stats.getAiSequence().isInCombat() || stats.getAiSequence().hasPackage(AiPackageTypeId::Pursue); // 1. Unconsious actor can not track target // 2. Actors in combat and pursue mode do not bother to headtrack // 3. Player character does not use headtracking in the 1st-person view - if (!stats.getKnockedDown() && - !stats.getAiSequence().isInCombat() && - !stats.getAiSequence().hasPackage(AiPackageTypeId::Pursue) && - !firstPersonPlayer) + if (!stats.getKnockedDown() && !firstPersonPlayer && !inCombatOrPursue) { for(PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it) { @@ -1786,6 +1915,17 @@ namespace MWMechanics } } + if (!stats.getKnockedDown() && !isPlayer && inCombatOrPursue) + { + // Actors in combat and pursue mode always look at their target. + for (const auto& package : stats.getAiSequence()) + { + headTrackTarget = package->getTarget(); + if (!headTrackTarget.isEmpty()) + break; + } + } + ctrl->setHeadTrackTarget(headTrackTarget); } @@ -1824,6 +1964,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 bd5a14c0d..9299d468c 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(); @@ -209,6 +211,7 @@ namespace MWMechanics float mTimerDisposeSummonsCorpses; float mActorsProcessingRange; + bool mSmoothMovement; }; } diff --git a/apps/openmw/mwmechanics/aibreathe.cpp b/apps/openmw/mwmechanics/aibreathe.cpp index 15251e125..2740355b5 100644 --- a/apps/openmw/mwmechanics/aibreathe.cpp +++ b/apps/openmw/mwmechanics/aibreathe.cpp @@ -23,7 +23,7 @@ bool MWMechanics::AiBreathe::execute (const MWWorld::Ptr& actor, CharacterContro actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); actorClass.getMovementSettings(actor).mPosition[1] = 1; - smoothTurn(actor, -180, 0); + smoothTurn(actor, -osg::PI / 2, 0); return false; } diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 1ae27a9cb..cbe161af1 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -5,6 +5,8 @@ #include +#include + #include #include "../mwphysics/collisiontype.hpp" @@ -240,10 +242,6 @@ namespace MWMechanics if (storage.mReadyToAttack) { - storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target); - // start new attack - storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat); - if (isRangedCombat) { // rotate actor taking into account target movement direction and projectile speed @@ -259,6 +257,10 @@ namespace MWMechanics storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir); storage.mMovement.mRotation[2] = getZAngleToDir((vTargetPos-vActorPos)); // using vAimDir results in spastic movements since the head is animated } + + storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target); + // start new attack + storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat); } return false; } @@ -372,9 +374,13 @@ namespace MWMechanics void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage) { // apply combat movement + float deltaAngle = storage.mMovement.mRotation[2] - actor.getRefData().getPosition().rot[2]; + osg::Vec2f movement = Misc::rotateVec2f( + osg::Vec2f(storage.mMovement.mPosition[0], storage.mMovement.mPosition[1]), -deltaAngle); + MWMechanics::Movement& actorMovementSettings = actor.getClass().getMovementSettings(actor); - actorMovementSettings.mPosition[0] = storage.mMovement.mPosition[0]; - actorMovementSettings.mPosition[1] = storage.mMovement.mPosition[1]; + actorMovementSettings.mPosition[0] = movement.x(); + actorMovementSettings.mPosition[1] = movement.y(); actorMovementSettings.mPosition[2] = storage.mMovement.mPosition[2]; rotateActorOnAxis(actor, 2, actorMovementSettings, storage); @@ -385,26 +391,11 @@ namespace MWMechanics MWMechanics::Movement& actorMovementSettings, AiCombatStorage& storage) { actorMovementSettings.mRotation[axis] = 0; - float& targetAngleRadians = storage.mMovement.mRotation[axis]; - if (targetAngleRadians != 0) - { - // Some attack animations contain small amount of movement. - // Since we use cone shapes for melee, we can use a threshold to avoid jittering - std::shared_ptr& currentAction = storage.mCurrentAction; - bool isRangedCombat = false; - currentAction->getCombatRange(isRangedCombat); - // Check if the actor now facing desired direction, no need to turn any more - if (isRangedCombat) - { - if (smoothTurn(actor, targetAngleRadians, axis)) - targetAngleRadians = 0; - } - else - { - if (smoothTurn(actor, targetAngleRadians, axis, osg::DegreesToRadians(3.f))) - targetAngleRadians = 0; - } - } + bool isRangedCombat = false; + storage.mCurrentAction->getCombatRange(isRangedCombat); + float eps = isRangedCombat ? osg::DegreesToRadians(0.5) : osg::DegreesToRadians(3.f); + float targetAngleRadians = storage.mMovement.mRotation[axis]; + smoothTurn(actor, targetAngleRadians, axis, eps); } MWWorld::Ptr AiCombat::getTarget() const @@ -489,12 +480,19 @@ namespace MWMechanics // Note: do not use for ranged combat yet since in couple with back up behaviour can move actor out of cliff else if (actor.getClass().isBipedal(actor)) { - // apply sideway movement (kind of dodging) with some probability - // if actor is within range of target's weapon - if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25) + float moveDuration = 0; + float angleToTarget = Misc::normalizeAngle(mMovement.mRotation[2] - actor.getRefData().getPosition().rot[2]); + // Apply a big side step if enemy tries to get around and come from behind. + // Otherwise apply a random side step (kind of dodging) with some probability + // if actor is within range of target's weapon. + if (std::abs(angleToTarget) > osg::PI / 4) + moveDuration = 0.2; + else if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25) + moveDuration = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); + if (moveDuration > 0) { mMovement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f; // to the left/right - mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); + mTimerCombatMove = moveDuration; mCombatMove = true; } } diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 53e366579..4bffd28ba 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" @@ -87,6 +88,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& //... But AI processing distance may increase in the future. if (isNearInactiveCell(position)) { + actor.getClass().getMovementSettings(actor).mPosition[0] = 0; actor.getClass().getMovementSettings(actor).mPosition[1] = 0; world->updateActorPath(actor, mPathFinder.getPath(), halfExtents, position, dest); return false; @@ -169,12 +171,34 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& } // turn to next path point by X,Z axes - zTurn(actor, mPathFinder.getZAngleToNext(position.x(), position.y())); + float zAngleToNext = mPathFinder.getZAngleToNext(position.x(), position.y()); + zTurn(actor, zAngleToNext); smoothTurn(actor, mPathFinder.getXAngleToNext(position.x(), position.y(), position.z()), 0); const auto destination = mPathFinder.getPath().empty() ? dest : mPathFinder.getPath().front(); mObstacleCheck.update(actor, destination, duration); + static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game"); + if (smoothMovement) + { + const float smoothTurnReservedDist = 150; + auto& movement = actor.getClass().getMovementSettings(actor); + float distToNextSqr = osg::Vec2f(destination.x() - position.x(), destination.y() - position.y()).length2(); + float diffAngle = zAngleToNext - actor.getRefData().getPosition().rot[2]; + if (std::cos(diffAngle) < -0.1) + movement.mPosition[0] = movement.mPosition[1] = 0; + else if (distToNextSqr > smoothTurnReservedDist * smoothTurnReservedDist) + { // Go forward (and slowly turn towards the next path point) + movement.mPosition[0] = 0; + movement.mPosition[1] = 1; + } + else + { // Next path point is near, so use diagonal movement to follow the path precisely. + movement.mPosition[0] = std::sin(diffAngle); + movement.mPosition[1] = std::max(std::cos(diffAngle), 0.f); + } + } + // handle obstacles on the way evadeObstacles(actor); diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 9e179edeb..375209a25 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; @@ -96,6 +96,7 @@ namespace MWMechanics void stopMovement(const MWWorld::Ptr& actor) { + actor.getClass().getMovementSettings(actor).mPosition[0] = 0; actor.getClass().getMovementSettings(actor).mPosition[1] = 0; } @@ -424,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 @@ -451,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? @@ -468,6 +482,9 @@ namespace MWMechanics 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; diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 4138c3dea..4165cebbd 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/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index ad30c16bd..1dcccb888 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1963,6 +1963,50 @@ void CharacterController::update(float duration, bool animationOnly) if (isPlayer && !isrunning && !sneak && !flying && movementSettings.mSpeedFactor <= 0.5f) movementSettings.mSpeedFactor *= 2.f; + static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game"); + if (smoothMovement && !isFirstPersonPlayer) + { + float angle = mPtr.getRefData().getPosition().rot[2]; + osg::Vec2f targetSpeed = Misc::rotateVec2f(osg::Vec2f(vec.x(), vec.y()), -angle) * movementSettings.mSpeedFactor; + osg::Vec2f delta = targetSpeed - mSmoothedSpeed; + float speedDelta = movementSettings.mSpeedFactor - mSmoothedSpeed.length(); + float deltaLen = delta.length(); + + float maxDelta; + if (std::abs(speedDelta) < deltaLen / 2) + // Turning is smooth for player and less smooth for NPCs (otherwise NPC can miss a path point). + maxDelta = duration * (isPlayer ? 3.f : 6.f); + else if (isPlayer && speedDelta < -deltaLen / 2) + // As soon as controls are released, mwinput switches player from running to walking. + // So stopping should be instant for player, otherwise it causes a small twitch. + maxDelta = 1; + else // In all other cases speeding up and stopping are smooth. + maxDelta = duration * 3.f; + + if (deltaLen > maxDelta) + delta *= maxDelta / deltaLen; + mSmoothedSpeed += delta; + + osg::Vec2f newSpeed = Misc::rotateVec2f(mSmoothedSpeed, angle); + movementSettings.mSpeedFactor = newSpeed.normalize(); + vec.x() = newSpeed.x(); + vec.y() = newSpeed.y(); + + const float eps = 0.001f; + if (movementSettings.mSpeedFactor < eps) + { + movementSettings.mSpeedFactor = 0; + vec.x() = 0; + vec.y() = 1; + } + else if ((vec.y() < 0) != mIsMovingBackward) + { + if (targetSpeed.length() < eps || (movementSettings.mPosition[1] < 0) == mIsMovingBackward) + vec.y() = mIsMovingBackward ? -eps : eps; + } + vec.normalize(); + } + float effectiveRotation = rot.z(); bool canMove = cls.getMaxSpeed(mPtr) > 0; static const bool turnToMovementDirection = Settings::Manager::getBool("turn to movement direction", "Game"); @@ -1994,6 +2038,8 @@ void CharacterController::update(float duration, bool animationOnly) mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 2); else mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 4); + if (smoothMovement && !isPlayer && !inwater) + mAnimation->setUpperBodyYawRadians(mAnimation->getUpperBodyYawRadians() + mAnimation->getHeadYaw() / 2); speed = cls.getCurrentSpeed(mPtr); vec.x() *= speed; @@ -2185,13 +2231,11 @@ void CharacterController::update(float duration, bool animationOnly) : (sneak ? CharState_SneakBack : (isrunning ? CharState_RunBack : CharState_WalkBack))); } - else if (effectiveRotation != 0.0f) + else { // Do not play turning animation for player if rotation speed is very slow. // Actual threshold should take framerate in account. - float rotationThreshold = 0.f; - if (isPlayer) - rotationThreshold = 0.015 * 60 * duration; + float rotationThreshold = (isPlayer ? 0.015f : 0.001f) * 60 * duration; // It seems only bipedal actors use turning animations. // Also do not use turning animations in the first-person view and when sneaking. @@ -2695,10 +2739,9 @@ void CharacterController::setVisibility(float visibility) void CharacterController::setAttackTypeBasedOnMovement() { float *move = mPtr.getClass().getMovementSettings(mPtr).mPosition; - - if (move[1] && !move[0]) // forward-backward + if (std::abs(move[1]) > std::abs(move[0]) + 0.2f) // forward-backward mAttackType = "thrust"; - else if (move[0] && !move[1]) //sideway + else if (std::abs(move[0]) > std::abs(move[1]) + 0.2f) // sideway mAttackType = "slash"; else mAttackType = "chop"; @@ -2893,19 +2936,21 @@ void CharacterController::updateHeadTracking(float duration) return; const osg::Vec3f actorDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); - zAngleRadians = std::atan2(direction.x(), direction.y()) - std::atan2(actorDirection.x(), actorDirection.y()); - xAngleRadians = -std::asin(direction.z()); - - const double xLimit = osg::DegreesToRadians(40.0); - const double zLimit = osg::DegreesToRadians(30.0); - zAngleRadians = osg::clampBetween(Misc::normalizeAngle(zAngleRadians), -xLimit, xLimit); - xAngleRadians = osg::clampBetween(Misc::normalizeAngle(xAngleRadians), -zLimit, zLimit); + zAngleRadians = std::atan2(actorDirection.x(), actorDirection.y()) - std::atan2(direction.x(), direction.y()); + xAngleRadians = std::asin(direction.z()); } + const double xLimit = osg::DegreesToRadians(40.0); + const double zLimit = osg::DegreesToRadians(30.0); + double zLimitOffset = mAnimation->getUpperBodyYawRadians(); + xAngleRadians = osg::clampBetween(Misc::normalizeAngle(xAngleRadians), -xLimit, xLimit); + zAngleRadians = osg::clampBetween(Misc::normalizeAngle(zAngleRadians), + -zLimit + zLimitOffset, zLimit + zLimitOffset); + float factor = duration*5; factor = std::min(factor, 1.f); - xAngleRadians = (1.f-factor) * mAnimation->getHeadPitch() + factor * (-xAngleRadians); - zAngleRadians = (1.f-factor) * mAnimation->getHeadYaw() + factor * (-zAngleRadians); + xAngleRadians = (1.f-factor) * mAnimation->getHeadPitch() + factor * xAngleRadians; + zAngleRadians = (1.f-factor) * mAnimation->getHeadYaw() + factor * zAngleRadians; mAnimation->setHeadPitch(xAngleRadians); mAnimation->setHeadYaw(zAngleRadians); diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 6092ca724..0336c4e69 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -196,6 +196,7 @@ class CharacterController : public MWRender::Animation::TextKeyListener float mTimeUntilWake; bool mIsMovingBackward; + osg::Vec2f mSmoothedSpeed; void setAttackTypeBasedOnMovement(); diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 4d4b5be51..a82dcf717 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -88,6 +88,24 @@ namespace const auto halfExtents = world->getHalfExtents(actor); return 2.0 * halfExtents.z(); } + + // Returns true if turn in `p2` is less than 10 degrees and all the 3 points are almost on one line. + bool isAlmostStraight(const osg::Vec3f& p1, const osg::Vec3f& p2, const osg::Vec3f& p3, float pointTolerance) { + osg::Vec3f v1 = p1 - p2; + osg::Vec3f v3 = p3 - p2; + v1.z() = v3.z() = 0; + float dotProduct = v1.x() * v3.x() + v1.y() * v3.y(); + float crossProduct = v1.x() * v3.y() - v1.y() * v3.x(); + + // Check that the angle between v1 and v3 is less or equal than 10 degrees. + static const float cos170 = std::cos(osg::PI / 180 * 170); + bool checkAngle = dotProduct <= cos170 * v1.length() * v3.length(); + + // Check that distance from p2 to the line (p1, p3) is less or equal than `pointTolerance`. + bool checkDist = std::abs(crossProduct) <= pointTolerance * (p3 - p1).length() * 2; + + return checkAngle && checkDist; + } } namespace MWMechanics @@ -286,6 +304,11 @@ namespace MWMechanics while (mPath.size() > 1 && sqrDistanceIgnoreZ(mPath.front(), position) < pointTolerance * pointTolerance) mPath.pop_front(); + while (mPath.size() > 2 && isAlmostStraight(mPath[0], mPath[1], mPath[2], pointTolerance)) + mPath.erase(mPath.begin() + 1); + if (mPath.size() > 1 && isAlmostStraight(position, mPath[0], mPath[1], pointTolerance)) + mPath.pop_front(); + if (mPath.size() == 1 && sqrDistanceIgnoreZ(mPath.front(), position) < destinationTolerance * destinationTolerance) mPath.pop_front(); } diff --git a/apps/openmw/mwmechanics/steering.cpp b/apps/openmw/mwmechanics/steering.cpp index d442085ea..eaf37fbd2 100644 --- a/apps/openmw/mwmechanics/steering.cpp +++ b/apps/openmw/mwmechanics/steering.cpp @@ -1,5 +1,8 @@ #include "steering.hpp" +#include +#include + #include "../mwworld/class.hpp" #include "../mwworld/ptr.hpp" @@ -12,19 +15,8 @@ namespace MWMechanics bool smoothTurn(const MWWorld::Ptr& actor, float targetAngleRadians, int axis, float epsilonRadians) { - float currentAngle (actor.getRefData().getPosition().rot[axis]); - float diff (targetAngleRadians - currentAngle); - if (std::abs(diff) >= osg::DegreesToRadians(180.f)) - { - if (diff >= 0) - { - diff = diff - osg::DegreesToRadians(360.f); - } - else - { - diff = osg::DegreesToRadians(360.f) + diff; - } - } + MWMechanics::Movement& movement = actor.getClass().getMovementSettings(actor); + float diff = Misc::normalizeAngle(targetAngleRadians - actor.getRefData().getPosition().rot[axis]); float absDiff = std::abs(diff); // The turning animation actually moves you slightly, so the angle will be wrong again. @@ -33,10 +25,14 @@ bool smoothTurn(const MWWorld::Ptr& actor, float targetAngleRadians, int axis, f return true; float limit = getAngularVelocity(actor.getClass().getMaxSpeed(actor)) * MWBase::Environment::get().getFrameDuration(); + static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game"); + if (smoothMovement) + limit *= std::min(absDiff / osg::PI + 0.1, 0.5); + if (absDiff > limit) diff = osg::sign(diff) * limit; - actor.getClass().getMovementSettings(actor).mRotation[axis] = diff; + movement.mRotation[axis] = diff; return false; } diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index 24184f2c8..f9274cebf 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -1507,6 +1507,8 @@ namespace NifOsg texture2d->setWrap(osg::Texture::WRAP_S, wrapS ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); texture2d->setWrap(osg::Texture::WRAP_T, wrapT ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); + + uvSet = tex.uvSet; } else { diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index 46bd22e50..f942f6e7c 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -331,8 +331,43 @@ If enabled then the character turns lower body to the direction of movement. Upp This setting can be controlled in Advanced tab of the launcher. +smooth movement +--------------- + +:Type: boolean +:Range: True/False +:Default: False + +Makes NPCs and player movement more smooth. + +Recommended to use with "turn to movement direction" enabled. + +This setting can be controlled in Advanced tab of the launcher. + +NPCs avoid collisions +--------------------- + +:Type: boolean +:Range: True/False +:Default: False + +If enabled NPCs apply evasion maneuver to avoid collisions with others. + +This setting can be controlled in Advanced tab of the launcher. + +NPCs give way +------------- + +:Type: boolean +:Range: True/False +:Default: True + +Standing NPCs give way to moving ones. Works only if 'NPCs avoid collisions' is enabled. + +This setting can only be configured by editing the settings configuration file. + swim upward correction ----------------- +---------------------- :Type: boolean :Range: True/False diff --git a/files/settings-default.cfg b/files/settings-default.cfg index ac5433f30..fab5fe569 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -325,6 +325,15 @@ uncapped damage fatigue = false # Turn lower body to movement direction. 'true' makes diagonal movement more realistic. 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 diff --git a/files/ui/advancedpage.ui b/files/ui/advancedpage.ui index 9084a7aba..3a91db791 100644 --- a/files/ui/advancedpage.ui +++ b/files/ui/advancedpage.ui @@ -43,6 +43,16 @@ + + + + <html><head/><body><p>If enabled NPCs apply evasion maneuver to avoid collisions with others.</p></body></html> + + + NPCs avoid collisions + + + @@ -233,6 +243,16 @@ + + + + <html><head/><body><p>Makes NPCs and player movement more smooth. Recommended to use with "turn to movement direction" enabled.</p></body></html> + + + Smooth movement + + + @@ -283,7 +303,7 @@ - + <html><head/><body><p>Load per-group KF-files and skeleton files from Animations folder</p></body></html>