Merge branch 'master' into 'rotation'

# Conflicts:
#   CHANGELOG.md
pull/593/head
psi29a 4 years ago
commit ad1021721f

@ -49,6 +49,7 @@
Bug #5557: Diagonal movement is noticeably slower with analogue stick Bug #5557: Diagonal movement is noticeably slower with analogue stick
Feature #390: 3rd person look "over the shoulder" Feature #390: 3rd person look "over the shoulder"
Feature #2386: Distant Statics in the form of Object Paging 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 #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 #5362: Show the soul gems' trapped soul in count dialog
Feature #5445: Handle NiLines Feature #5445: Handle NiLines
@ -59,6 +60,7 @@
Feature #5525: Search fields tweaks (utf-8) Feature #5525: Search fields tweaks (utf-8)
Feature #5545: Option to allow stealing from an unconscious NPC during combat Feature #5545: Option to allow stealing from an unconscious NPC during combat
Feature #5579: MCP SetAngle enhancement Feature #5579: MCP SetAngle enhancement
Feature #5610: Actors movement should be smoother
Task #5480: Drop Qt4 support Task #5480: Drop Qt4 support
Task #5520: Improve cell name autocompleter implementation Task #5520: Improve cell name autocompleter implementation

@ -88,6 +88,7 @@ bool Launcher::AdvancedPage::loadSettings()
loadSettingBool(uncappedDamageFatigueCheckBox, "uncapped damage fatigue", "Game"); loadSettingBool(uncappedDamageFatigueCheckBox, "uncapped damage fatigue", "Game");
loadSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game"); loadSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game");
loadSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game"); loadSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game");
loadSettingBool(avoidCollisionsCheckBox, "NPCs avoid collisions", "Game");
int unarmedFactorsStrengthIndex = mEngineSettings.getInt("strength influences hand to hand", "Game"); int unarmedFactorsStrengthIndex = mEngineSettings.getInt("strength influences hand to hand", "Game");
if (unarmedFactorsStrengthIndex >= 0 && unarmedFactorsStrengthIndex <= 2) if (unarmedFactorsStrengthIndex >= 0 && unarmedFactorsStrengthIndex <= 2)
unarmedFactorsStrengthComboBox->setCurrentIndex(unarmedFactorsStrengthIndex); unarmedFactorsStrengthComboBox->setCurrentIndex(unarmedFactorsStrengthIndex);
@ -112,6 +113,7 @@ bool Launcher::AdvancedPage::loadSettings()
loadSettingBool(shieldSheathingCheckBox, "shield sheathing", "Game"); loadSettingBool(shieldSheathingCheckBox, "shield sheathing", "Game");
} }
loadSettingBool(turnToMovementDirectionCheckBox, "turn to movement direction", "Game"); loadSettingBool(turnToMovementDirectionCheckBox, "turn to movement direction", "Game");
loadSettingBool(smoothMovementCheckBox, "smooth movement", "Game");
const bool distantTerrain = mEngineSettings.getBool("distant terrain", "Terrain"); const bool distantTerrain = mEngineSettings.getBool("distant terrain", "Terrain");
const bool objectPaging = mEngineSettings.getBool("object paging", "Terrain"); const bool objectPaging = mEngineSettings.getBool("object paging", "Terrain");
@ -200,6 +202,7 @@ void Launcher::AdvancedPage::saveSettings()
saveSettingBool(uncappedDamageFatigueCheckBox, "uncapped damage fatigue", "Game"); saveSettingBool(uncappedDamageFatigueCheckBox, "uncapped damage fatigue", "Game");
saveSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game"); saveSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game");
saveSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game"); saveSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game");
saveSettingBool(avoidCollisionsCheckBox, "NPCs avoid collisions", "Game");
int unarmedFactorsStrengthIndex = unarmedFactorsStrengthComboBox->currentIndex(); int unarmedFactorsStrengthIndex = unarmedFactorsStrengthComboBox->currentIndex();
if (unarmedFactorsStrengthIndex != mEngineSettings.getInt("strength influences hand to hand", "Game")) if (unarmedFactorsStrengthIndex != mEngineSettings.getInt("strength influences hand to hand", "Game"))
mEngineSettings.setInt("strength influences hand to hand", "Game", unarmedFactorsStrengthIndex); mEngineSettings.setInt("strength influences hand to hand", "Game", unarmedFactorsStrengthIndex);
@ -220,6 +223,7 @@ void Launcher::AdvancedPage::saveSettings()
saveSettingBool(weaponSheathingCheckBox, "weapon sheathing", "Game"); saveSettingBool(weaponSheathingCheckBox, "weapon sheathing", "Game");
saveSettingBool(shieldSheathingCheckBox, "shield sheathing", "Game"); saveSettingBool(shieldSheathingCheckBox, "shield sheathing", "Game");
saveSettingBool(turnToMovementDirectionCheckBox, "turn to movement direction", "Game"); saveSettingBool(turnToMovementDirectionCheckBox, "turn to movement direction", "Game");
saveSettingBool(smoothMovementCheckBox, "smooth movement", "Game");
const bool distantTerrain = mEngineSettings.getBool("distant terrain", "Terrain"); const bool distantTerrain = mEngineSettings.getBool("distant terrain", "Terrain");
const bool objectPaging = mEngineSettings.getBool("object paging", "Terrain"); const bool objectPaging = mEngineSettings.getBool("object paging", "Terrain");

@ -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"
@ -424,7 +426,7 @@ namespace MWMechanics
const osg::Vec3f actor2Pos(targetActor.getRefData().getPosition().asVec3()); const osg::Vec3f actor2Pos(targetActor.getRefData().getPosition().asVec3());
float sqrDist = (actor1Pos - actor2Pos).length2(); float sqrDist = (actor1Pos - actor2Pos).length2();
if (sqrDist > maxDistance*maxDistance) if (sqrDist > std::min(maxDistance * maxDistance, sqrHeadTrackDistance))
return; return;
// stop tracking when target is behind the actor // stop tracking when target is behind the actor
@ -432,10 +434,7 @@ namespace MWMechanics
osg::Vec3f targetDirection(actor2Pos - actor1Pos); osg::Vec3f targetDirection(actor2Pos - actor1Pos);
actorDirection.z() = 0; actorDirection.z() = 0;
targetDirection.z() = 0; targetDirection.z() = 0;
actorDirection.normalize(); if (actorDirection * targetDirection > 0
targetDirection.normalize();
if (std::acos(actorDirection * targetDirection) < osg::DegreesToRadians(90.f)
&& sqrDist <= sqrHeadTrackDistance
&& MWBase::Environment::get().getWorld()->getLOS(actor, targetActor) // check LOS and awareness last as it's the most expensive function && 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)) && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(targetActor, actor))
{ {
@ -473,6 +472,9 @@ namespace MWMechanics
void Actors::updateMovementSpeed(const MWWorld::Ptr& actor) void Actors::updateMovementSpeed(const MWWorld::Ptr& actor)
{ {
if (mSmoothMovement)
return;
CreatureStats &stats = actor.getClass().getCreatureStats(actor); CreatureStats &stats = actor.getClass().getCreatureStats(actor);
MWMechanics::AiSequence& seq = stats.getAiSequence(); MWMechanics::AiSequence& seq = stats.getAiSequence();
@ -481,9 +483,10 @@ namespace MWMechanics
osg::Vec3f targetPos = seq.getActivePackage().getDestination(); osg::Vec3f targetPos = seq.getActivePackage().getDestination();
osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3();
float distance = (targetPos - actorPos).length(); float distance = (targetPos - actorPos).length();
if (distance < DECELERATE_DISTANCE) 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); auto& movement = actor.getClass().getMovementSettings(actor);
movement.mPosition[0] *= speedCoef; movement.mPosition[0] *= speedCoef;
movement.mPosition[1] *= speedCoef; movement.mPosition[1] *= speedCoef;
@ -587,8 +590,11 @@ namespace MWMechanics
if (!actorState.isTurningToPlayer()) if (!actorState.isTurningToPlayer())
{ {
actorState.setAngleToPlayer(std::atan2(dir.x(), dir.y())); float angle = std::atan2(dir.x(), dir.y());
actorState.setTurningToPlayer(true); 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 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<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)
@ -1769,14 +1900,12 @@ namespace MWMechanics
MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first); MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first);
bool firstPersonPlayer = isPlayer && world->isFirstPerson(); bool firstPersonPlayer = isPlayer && world->isFirstPerson();
bool inCombatOrPursue = stats.getAiSequence().isInCombat() || stats.getAiSequence().hasPackage(AiPackageTypeId::Pursue);
// 1. Unconsious actor can not track target // 1. Unconsious actor can not track target
// 2. Actors in combat and pursue mode do not bother to headtrack // 2. Actors in combat and pursue mode do not bother to headtrack
// 3. Player character does not use headtracking in the 1st-person view // 3. Player character does not use headtracking in the 1st-person view
if (!stats.getKnockedDown() && if (!stats.getKnockedDown() && !firstPersonPlayer && !inCombatOrPursue)
!stats.getAiSequence().isInCombat() &&
!stats.getAiSequence().hasPackage(AiPackageTypeId::Pursue) &&
!firstPersonPlayer)
{ {
for(PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it) 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); 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; 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();
@ -209,6 +211,7 @@ namespace MWMechanics
float mTimerDisposeSummonsCorpses; float mTimerDisposeSummonsCorpses;
float mActorsProcessingRange; float mActorsProcessingRange;
bool mSmoothMovement;
}; };
} }

@ -23,7 +23,7 @@ bool MWMechanics::AiBreathe::execute (const MWWorld::Ptr& actor, CharacterContro
actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true);
actorClass.getMovementSettings(actor).mPosition[1] = 1; actorClass.getMovementSettings(actor).mPosition[1] = 1;
smoothTurn(actor, -180, 0); smoothTurn(actor, -osg::PI / 2, 0);
return false; return false;
} }

@ -5,6 +5,8 @@
#include <components/esm/aisequence.hpp> #include <components/esm/aisequence.hpp>
#include <components/misc/mathutil.hpp>
#include <components/sceneutil/positionattitudetransform.hpp> #include <components/sceneutil/positionattitudetransform.hpp>
#include "../mwphysics/collisiontype.hpp" #include "../mwphysics/collisiontype.hpp"
@ -240,10 +242,6 @@ namespace MWMechanics
if (storage.mReadyToAttack) if (storage.mReadyToAttack)
{ {
storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target);
// start new attack
storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat);
if (isRangedCombat) if (isRangedCombat)
{ {
// rotate actor taking into account target movement direction and projectile speed // 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[0] = getXAngleToDir(vAimDir);
storage.mMovement.mRotation[2] = getZAngleToDir((vTargetPos-vActorPos)); // using vAimDir results in spastic movements since the head is animated 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; return false;
} }
@ -372,9 +374,13 @@ namespace MWMechanics
void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage) void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage)
{ {
// apply combat movement // 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); MWMechanics::Movement& actorMovementSettings = actor.getClass().getMovementSettings(actor);
actorMovementSettings.mPosition[0] = storage.mMovement.mPosition[0]; actorMovementSettings.mPosition[0] = movement.x();
actorMovementSettings.mPosition[1] = storage.mMovement.mPosition[1]; actorMovementSettings.mPosition[1] = movement.y();
actorMovementSettings.mPosition[2] = storage.mMovement.mPosition[2]; actorMovementSettings.mPosition[2] = storage.mMovement.mPosition[2];
rotateActorOnAxis(actor, 2, actorMovementSettings, storage); rotateActorOnAxis(actor, 2, actorMovementSettings, storage);
@ -385,26 +391,11 @@ namespace MWMechanics
MWMechanics::Movement& actorMovementSettings, AiCombatStorage& storage) MWMechanics::Movement& actorMovementSettings, AiCombatStorage& storage)
{ {
actorMovementSettings.mRotation[axis] = 0; actorMovementSettings.mRotation[axis] = 0;
float& targetAngleRadians = storage.mMovement.mRotation[axis]; bool isRangedCombat = false;
if (targetAngleRadians != 0) storage.mCurrentAction->getCombatRange(isRangedCombat);
{ float eps = isRangedCombat ? osg::DegreesToRadians(0.5) : osg::DegreesToRadians(3.f);
// Some attack animations contain small amount of movement. float targetAngleRadians = storage.mMovement.mRotation[axis];
// Since we use cone shapes for melee, we can use a threshold to avoid jittering smoothTurn(actor, targetAngleRadians, axis, eps);
std::shared_ptr<Action>& 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;
}
}
} }
MWWorld::Ptr AiCombat::getTarget() const 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 // 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)) else if (actor.getClass().isBipedal(actor))
{ {
// apply sideway movement (kind of dodging) with some probability float moveDuration = 0;
// if actor is within range of target's weapon float angleToTarget = Misc::normalizeAngle(mMovement.mRotation[2] - actor.getRefData().getPosition().rot[2]);
if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25) // 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 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; mCombatMove = true;
} }
} }

@ -5,6 +5,7 @@
#include <components/esm/loadmgef.hpp> #include <components/esm/loadmgef.hpp>
#include <components/detournavigator/navigator.hpp> #include <components/detournavigator/navigator.hpp>
#include <components/misc/coordinateconverter.hpp> #include <components/misc/coordinateconverter.hpp>
#include <components/settings/settings.hpp>
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwbase/environment.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. //... But AI processing distance may increase in the future.
if (isNearInactiveCell(position)) if (isNearInactiveCell(position))
{ {
actor.getClass().getMovementSettings(actor).mPosition[0] = 0;
actor.getClass().getMovementSettings(actor).mPosition[1] = 0; actor.getClass().getMovementSettings(actor).mPosition[1] = 0;
world->updateActorPath(actor, mPathFinder.getPath(), halfExtents, position, dest); world->updateActorPath(actor, mPathFinder.getPath(), halfExtents, position, dest);
return false; 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 // 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); smoothTurn(actor, mPathFinder.getXAngleToNext(position.x(), position.y(), position.z()), 0);
const auto destination = mPathFinder.getPath().empty() ? dest : mPathFinder.getPath().front(); const auto destination = mPathFinder.getPath().empty() ? dest : mPathFinder.getPath().front();
mObstacleCheck.update(actor, destination, duration); 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 // handle obstacles on the way
evadeObstacles(actor); evadeObstacles(actor);

@ -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;
@ -96,6 +96,7 @@ namespace MWMechanics
void stopMovement(const MWWorld::Ptr& actor) void stopMovement(const MWWorld::Ptr& actor)
{ {
actor.getClass().getMovementSettings(actor).mPosition[0] = 0;
actor.getClass().getMovementSettings(actor).mPosition[1] = 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) 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
@ -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) 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?
@ -468,6 +482,9 @@ namespace MWMechanics
void AiWander::onChooseActionStatePerFrameActions(const MWWorld::Ptr& actor, AiWanderStorage& storage) 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(); unsigned short idleAnimation = getRandomIdle();
storage.mIdleAnimation = idleAnimation; storage.mIdleAnimation = idleAnimation;

@ -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;

@ -1963,6 +1963,50 @@ void CharacterController::update(float duration, bool animationOnly)
if (isPlayer && !isrunning && !sneak && !flying && movementSettings.mSpeedFactor <= 0.5f) if (isPlayer && !isrunning && !sneak && !flying && movementSettings.mSpeedFactor <= 0.5f)
movementSettings.mSpeedFactor *= 2.f; 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(); float effectiveRotation = rot.z();
bool canMove = cls.getMaxSpeed(mPtr) > 0; bool canMove = cls.getMaxSpeed(mPtr) > 0;
static const bool turnToMovementDirection = Settings::Manager::getBool("turn to movement direction", "Game"); 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); mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 2);
else else
mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 4); mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 4);
if (smoothMovement && !isPlayer && !inwater)
mAnimation->setUpperBodyYawRadians(mAnimation->getUpperBodyYawRadians() + mAnimation->getHeadYaw() / 2);
speed = cls.getCurrentSpeed(mPtr); speed = cls.getCurrentSpeed(mPtr);
vec.x() *= speed; vec.x() *= speed;
@ -2185,13 +2231,11 @@ void CharacterController::update(float duration, bool animationOnly)
: (sneak ? CharState_SneakBack : (sneak ? CharState_SneakBack
: (isrunning ? CharState_RunBack : CharState_WalkBack))); : (isrunning ? CharState_RunBack : CharState_WalkBack)));
} }
else if (effectiveRotation != 0.0f) else
{ {
// Do not play turning animation for player if rotation speed is very slow. // Do not play turning animation for player if rotation speed is very slow.
// Actual threshold should take framerate in account. // Actual threshold should take framerate in account.
float rotationThreshold = 0.f; float rotationThreshold = (isPlayer ? 0.015f : 0.001f) * 60 * duration;
if (isPlayer)
rotationThreshold = 0.015 * 60 * duration;
// It seems only bipedal actors use turning animations. // It seems only bipedal actors use turning animations.
// Also do not use turning animations in the first-person view and when sneaking. // 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() void CharacterController::setAttackTypeBasedOnMovement()
{ {
float *move = mPtr.getClass().getMovementSettings(mPtr).mPosition; float *move = mPtr.getClass().getMovementSettings(mPtr).mPosition;
if (std::abs(move[1]) > std::abs(move[0]) + 0.2f) // forward-backward
if (move[1] && !move[0]) // forward-backward
mAttackType = "thrust"; mAttackType = "thrust";
else if (move[0] && !move[1]) //sideway else if (std::abs(move[0]) > std::abs(move[1]) + 0.2f) // sideway
mAttackType = "slash"; mAttackType = "slash";
else else
mAttackType = "chop"; mAttackType = "chop";
@ -2893,19 +2936,21 @@ void CharacterController::updateHeadTracking(float duration)
return; return;
const osg::Vec3f actorDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); 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()); zAngleRadians = std::atan2(actorDirection.x(), actorDirection.y()) - std::atan2(direction.x(), direction.y());
xAngleRadians = -std::asin(direction.z()); 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);
} }
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; float factor = duration*5;
factor = std::min(factor, 1.f); factor = std::min(factor, 1.f);
xAngleRadians = (1.f-factor) * mAnimation->getHeadPitch() + factor * (-xAngleRadians); xAngleRadians = (1.f-factor) * mAnimation->getHeadPitch() + factor * xAngleRadians;
zAngleRadians = (1.f-factor) * mAnimation->getHeadYaw() + factor * (-zAngleRadians); zAngleRadians = (1.f-factor) * mAnimation->getHeadYaw() + factor * zAngleRadians;
mAnimation->setHeadPitch(xAngleRadians); mAnimation->setHeadPitch(xAngleRadians);
mAnimation->setHeadYaw(zAngleRadians); mAnimation->setHeadYaw(zAngleRadians);

@ -196,6 +196,7 @@ class CharacterController : public MWRender::Animation::TextKeyListener
float mTimeUntilWake; float mTimeUntilWake;
bool mIsMovingBackward; bool mIsMovingBackward;
osg::Vec2f mSmoothedSpeed;
void setAttackTypeBasedOnMovement(); void setAttackTypeBasedOnMovement();

@ -88,6 +88,24 @@ namespace
const auto halfExtents = world->getHalfExtents(actor); const auto halfExtents = world->getHalfExtents(actor);
return 2.0 * halfExtents.z(); 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 namespace MWMechanics
@ -286,6 +304,11 @@ namespace MWMechanics
while (mPath.size() > 1 && sqrDistanceIgnoreZ(mPath.front(), position) < pointTolerance * pointTolerance) while (mPath.size() > 1 && sqrDistanceIgnoreZ(mPath.front(), position) < pointTolerance * pointTolerance)
mPath.pop_front(); 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) if (mPath.size() == 1 && sqrDistanceIgnoreZ(mPath.front(), position) < destinationTolerance * destinationTolerance)
mPath.pop_front(); mPath.pop_front();
} }

@ -1,5 +1,8 @@
#include "steering.hpp" #include "steering.hpp"
#include <components/misc/mathutil.hpp>
#include <components/settings/settings.hpp>
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwworld/ptr.hpp" #include "../mwworld/ptr.hpp"
@ -12,19 +15,8 @@ namespace MWMechanics
bool smoothTurn(const MWWorld::Ptr& actor, float targetAngleRadians, int axis, float epsilonRadians) bool smoothTurn(const MWWorld::Ptr& actor, float targetAngleRadians, int axis, float epsilonRadians)
{ {
float currentAngle (actor.getRefData().getPosition().rot[axis]); MWMechanics::Movement& movement = actor.getClass().getMovementSettings(actor);
float diff (targetAngleRadians - currentAngle); float diff = Misc::normalizeAngle(targetAngleRadians - actor.getRefData().getPosition().rot[axis]);
if (std::abs(diff) >= osg::DegreesToRadians(180.f))
{
if (diff >= 0)
{
diff = diff - osg::DegreesToRadians(360.f);
}
else
{
diff = osg::DegreesToRadians(360.f) + diff;
}
}
float absDiff = std::abs(diff); float absDiff = std::abs(diff);
// The turning animation actually moves you slightly, so the angle will be wrong again. // 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; return true;
float limit = getAngularVelocity(actor.getClass().getMaxSpeed(actor)) * MWBase::Environment::get().getFrameDuration(); 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) if (absDiff > limit)
diff = osg::sign(diff) * limit; diff = osg::sign(diff) * limit;
actor.getClass().getMovementSettings(actor).mRotation[axis] = diff; movement.mRotation[axis] = diff;
return false; return false;
} }

@ -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_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); texture2d->setWrap(osg::Texture::WRAP_T, wrapT ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE);
uvSet = tex.uvSet;
} }
else else
{ {

@ -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. 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 swim upward correction
---------------- ----------------------
:Type: boolean :Type: boolean
:Range: True/False :Range: True/False

@ -325,6 +325,15 @@ uncapped damage fatigue = false
# Turn lower body to movement direction. 'true' makes diagonal movement more realistic. # Turn lower body to movement direction. 'true' makes diagonal movement more realistic.
turn to movement direction = false 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. # Makes player swim a bit upward from the line of sight.
swim upward correction = false swim upward correction = false

@ -43,6 +43,16 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="0">
<widget class="QCheckBox" name="avoidCollisionsCheckBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If enabled NPCs apply evasion maneuver to avoid collisions with others.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>NPCs avoid collisions</string>
</property>
</widget>
</item>
<item row="4" column="0"> <item row="4" column="0">
<widget class="QCheckBox" name="enableNavigatorCheckBox"> <widget class="QCheckBox" name="enableNavigatorCheckBox">
<property name="toolTip"> <property name="toolTip">
@ -233,6 +243,16 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0">
<widget class="QCheckBox" name="smoothMovementCheckBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Makes NPCs and player movement more smooth. Recommended to use with "turn to movement direction" enabled.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Smooth movement</string>
</property>
</widget>
</item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QCheckBox" name="distantLandCheckBox"> <widget class="QCheckBox" name="distantLandCheckBox">
<property name="toolTip"> <property name="toolTip">
@ -283,7 +303,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0"> <item row="6" column="0">
<widget class="QCheckBox" name="animSourcesCheckBox"> <widget class="QCheckBox" name="animSourcesCheckBox">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Load per-group KF-files and skeleton files from Animations folder&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Load per-group KF-files and skeleton files from Animations folder&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>

Loading…
Cancel
Save