mirror of
https://github.com/TES3MP/TES3MP.git
synced 2025-09-27 23:21:20 -04:00
Implement NPC head tracking (Fixes #1720)
This commit is contained in:
parent
b9e5aa9db6
commit
d962f0918d
@ -4,6 +4,7 @@
|
|||||||
#include <typeinfo>
|
#include <typeinfo>
|
||||||
|
|
||||||
#include <OgreVector3.h>
|
#include <OgreVector3.h>
|
||||||
|
#include <OgreSceneNode.h>
|
||||||
|
|
||||||
#include <components/esm/loadnpc.hpp>
|
#include <components/esm/loadnpc.hpp>
|
||||||
|
|
||||||
@ -273,6 +274,40 @@ namespace MWMechanics
|
|||||||
calculateRestoration(ptr, duration);
|
calculateRestoration(ptr, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Actors::updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor,
|
||||||
|
MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance)
|
||||||
|
{
|
||||||
|
static const float fMaxHeadTrackDistance = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>()
|
||||||
|
.find("fMaxHeadTrackDistance")->getFloat();
|
||||||
|
static const float fInteriorHeadTrackMult = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>()
|
||||||
|
.find("fInteriorHeadTrackMult")->getFloat();
|
||||||
|
float maxDistance = fMaxHeadTrackDistance;
|
||||||
|
const ESM::Cell* currentCell = actor.getCell()->getCell();
|
||||||
|
if (!currentCell->isExterior() && !(currentCell->mData.mFlags & ESM::Cell::QuasiEx))
|
||||||
|
maxDistance *= fInteriorHeadTrackMult;
|
||||||
|
|
||||||
|
const ESM::Position& actor1Pos = actor.getRefData().getPosition();
|
||||||
|
const ESM::Position& actor2Pos = targetActor.getRefData().getPosition();
|
||||||
|
float sqrDist = Ogre::Vector3(actor1Pos.pos).squaredDistance(Ogre::Vector3(actor2Pos.pos));
|
||||||
|
|
||||||
|
if (sqrDist > maxDistance*maxDistance)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// stop tracking when target is behind the actor
|
||||||
|
Ogre::Vector3 actorDirection (actor.getRefData().getBaseNode()->getOrientation().yAxis());
|
||||||
|
Ogre::Vector3 targetDirection (Ogre::Vector3(actor2Pos.pos) - Ogre::Vector3(actor1Pos.pos));
|
||||||
|
actorDirection.z = 0;
|
||||||
|
targetDirection.z = 0;
|
||||||
|
if (actorDirection.angleBetween(targetDirection) < Ogre::Degree(90)
|
||||||
|
&& sqrDist <= sqrHeadTrackDistance
|
||||||
|
&& 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))
|
||||||
|
{
|
||||||
|
sqrHeadTrackDistance = sqrDist;
|
||||||
|
headTrackTarget = targetActor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Actors::engageCombat (const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, bool againstPlayer)
|
void Actors::engageCombat (const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, bool againstPlayer)
|
||||||
{
|
{
|
||||||
CreatureStats& creatureStats = actor1.getClass().getCreatureStats(actor1);
|
CreatureStats& creatureStats = actor1.getClass().getCreatureStats(actor1);
|
||||||
@ -1138,9 +1173,11 @@ namespace MWMechanics
|
|||||||
if(!paused)
|
if(!paused)
|
||||||
{
|
{
|
||||||
static float timerUpdateAITargets = 0;
|
static float timerUpdateAITargets = 0;
|
||||||
|
static float timerUpdateHeadTrack = 0;
|
||||||
|
|
||||||
// target lists get updated once every 1.0 sec
|
// target lists get updated once every 1.0 sec
|
||||||
if (timerUpdateAITargets >= 1.0f) timerUpdateAITargets = 0;
|
if (timerUpdateAITargets >= 1.0f) timerUpdateAITargets = 0;
|
||||||
|
if (timerUpdateHeadTrack >= 0.3f) timerUpdateHeadTrack = 0;
|
||||||
|
|
||||||
MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr();
|
MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr();
|
||||||
|
|
||||||
@ -1174,6 +1211,19 @@ namespace MWMechanics
|
|||||||
engageCombat(iter->first, it->first, it->first == player);
|
engageCombat(iter->first, it->first, it->first == player);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (timerUpdateHeadTrack == 0)
|
||||||
|
{
|
||||||
|
float sqrHeadTrackDistance = std::numeric_limits<float>::max();
|
||||||
|
MWWorld::Ptr headTrackTarget;
|
||||||
|
|
||||||
|
for(PtrControllerMap::iterator it(mActors.begin()); it != mActors.end(); ++it)
|
||||||
|
{
|
||||||
|
if (it->first == iter->first)
|
||||||
|
continue;
|
||||||
|
updateHeadTracking(iter->first, it->first, headTrackTarget, sqrHeadTrackDistance);
|
||||||
|
}
|
||||||
|
iter->second->setHeadTrackTarget(headTrackTarget);
|
||||||
|
}
|
||||||
|
|
||||||
if (iter->first.getClass().isNpc() && iter->first != player)
|
if (iter->first.getClass().isNpc() && iter->first != player)
|
||||||
updateCrimePersuit(iter->first, duration);
|
updateCrimePersuit(iter->first, duration);
|
||||||
@ -1194,6 +1244,7 @@ namespace MWMechanics
|
|||||||
}
|
}
|
||||||
|
|
||||||
timerUpdateAITargets += duration;
|
timerUpdateAITargets += duration;
|
||||||
|
timerUpdateHeadTrack += duration;
|
||||||
|
|
||||||
// Looping magic VFX update
|
// Looping magic VFX update
|
||||||
// Note: we need to do this before any of the animations are updated.
|
// Note: we need to do this before any of the animations are updated.
|
||||||
|
@ -89,6 +89,9 @@ namespace MWMechanics
|
|||||||
*/
|
*/
|
||||||
void engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, bool againstPlayer);
|
void engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, bool againstPlayer);
|
||||||
|
|
||||||
|
void updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor,
|
||||||
|
MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance);
|
||||||
|
|
||||||
void restoreDynamicStats(bool sleep);
|
void restoreDynamicStats(bool sleep);
|
||||||
///< If the player is sleeping, this should be called every hour.
|
///< If the player is sleeping, this should be called every hour.
|
||||||
|
|
||||||
|
@ -42,6 +42,15 @@
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
|
// Wraps a value to (-PI, PI]
|
||||||
|
void wrap(Ogre::Radian& rad)
|
||||||
|
{
|
||||||
|
if (rad.valueRadians()>0)
|
||||||
|
rad = Ogre::Radian(std::fmod(rad.valueRadians()+Ogre::Math::PI, 2.0f*Ogre::Math::PI)-Ogre::Math::PI);
|
||||||
|
else
|
||||||
|
rad = Ogre::Radian(std::fmod(rad.valueRadians()-Ogre::Math::PI, 2.0f*Ogre::Math::PI)+Ogre::Math::PI);
|
||||||
|
}
|
||||||
|
|
||||||
std::string getBestAttack (const ESM::Weapon* weapon)
|
std::string getBestAttack (const ESM::Weapon* weapon)
|
||||||
{
|
{
|
||||||
int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2;
|
int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2;
|
||||||
@ -1627,6 +1636,8 @@ void CharacterController::update(float duration)
|
|||||||
cls.getMovementSettings(mPtr).mPosition[0] = cls.getMovementSettings(mPtr).mPosition[1] = 0;
|
cls.getMovementSettings(mPtr).mPosition[0] = cls.getMovementSettings(mPtr).mPosition[1] = 0;
|
||||||
// Can't reset jump state (mPosition[2]) here; we don't know for sure whether the PhysicSystem will actually handle it in this frame
|
// Can't reset jump state (mPosition[2]) here; we don't know for sure whether the PhysicSystem will actually handle it in this frame
|
||||||
// due to the fixed minimum timestep used for the physics update. It will be reset in PhysicSystem::move once the jump is handled.
|
// due to the fixed minimum timestep used for the physics update. It will be reset in PhysicSystem::move once the jump is handled.
|
||||||
|
|
||||||
|
updateHeadTracking(duration);
|
||||||
}
|
}
|
||||||
else if(cls.getCreatureStats(mPtr).isDead())
|
else if(cls.getCreatureStats(mPtr).isDead())
|
||||||
{
|
{
|
||||||
@ -1845,4 +1856,55 @@ bool CharacterController::isKnockedOut() const
|
|||||||
return mHitState == CharState_KnockOut;
|
return mHitState == CharState_KnockOut;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CharacterController::setHeadTrackTarget(const MWWorld::Ptr &target)
|
||||||
|
{
|
||||||
|
mHeadTrackTarget = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterController::updateHeadTracking(float duration)
|
||||||
|
{
|
||||||
|
Ogre::Node* head = mAnimation->getNode("Bip01 Head");
|
||||||
|
if (!head)
|
||||||
|
return;
|
||||||
|
Ogre::Radian zAngle (0.f);
|
||||||
|
Ogre::Radian xAngle (0.f);
|
||||||
|
if (!mHeadTrackTarget.isEmpty())
|
||||||
|
{
|
||||||
|
Ogre::Vector3 headPos = mPtr.getRefData().getBaseNode()->convertLocalToWorldPosition(head->_getDerivedPosition());
|
||||||
|
Ogre::Vector3 targetPos (mHeadTrackTarget.getRefData().getPosition().pos);
|
||||||
|
if (MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mHeadTrackTarget))
|
||||||
|
{
|
||||||
|
Ogre::Node* targetHead = anim->getNode("Head");
|
||||||
|
if (!targetHead)
|
||||||
|
targetHead = anim->getNode("Bip01 Head");
|
||||||
|
if (targetHead)
|
||||||
|
targetPos = mHeadTrackTarget.getRefData().getBaseNode()->convertLocalToWorldPosition(
|
||||||
|
targetHead->_getDerivedPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ogre::Vector3 direction = targetPos - headPos;
|
||||||
|
direction.normalise();
|
||||||
|
|
||||||
|
const Ogre::Vector3 actorDirection = mPtr.getRefData().getBaseNode()->getOrientation().yAxis();
|
||||||
|
|
||||||
|
zAngle = Ogre::Math::ATan2(direction.x,direction.y) -
|
||||||
|
Ogre::Math::ATan2(actorDirection.x, actorDirection.y);
|
||||||
|
xAngle = -Ogre::Math::ASin(direction.z);
|
||||||
|
wrap(zAngle);
|
||||||
|
wrap(xAngle);
|
||||||
|
xAngle = Ogre::Degree(std::min(xAngle.valueDegrees(), 40.f));
|
||||||
|
xAngle = Ogre::Degree(std::max(xAngle.valueDegrees(), -40.f));
|
||||||
|
zAngle = Ogre::Degree(std::min(zAngle.valueDegrees(), 30.f));
|
||||||
|
zAngle = Ogre::Degree(std::max(zAngle.valueDegrees(), -30.f));
|
||||||
|
|
||||||
|
}
|
||||||
|
float factor = duration*5;
|
||||||
|
factor = std::min(factor, 1.f);
|
||||||
|
xAngle = (1.f-factor) * mAnimation->getHeadPitch() + factor * (-xAngle);
|
||||||
|
zAngle = (1.f-factor) * mAnimation->getHeadYaw() + factor * (-zAngle);
|
||||||
|
|
||||||
|
mAnimation->setHeadPitch(xAngle);
|
||||||
|
mAnimation->setHeadYaw(zAngle);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -175,6 +175,8 @@ class CharacterController
|
|||||||
float mSecondsOfSwimming;
|
float mSecondsOfSwimming;
|
||||||
float mSecondsOfRunning;
|
float mSecondsOfRunning;
|
||||||
|
|
||||||
|
MWWorld::Ptr mHeadTrackTarget;
|
||||||
|
|
||||||
float mTurnAnimationThreshold; // how long to continue playing turning animation after actor stopped turning
|
float mTurnAnimationThreshold; // how long to continue playing turning animation after actor stopped turning
|
||||||
|
|
||||||
std::string mAttackType; // slash, chop or thrust
|
std::string mAttackType; // slash, chop or thrust
|
||||||
@ -188,6 +190,8 @@ class CharacterController
|
|||||||
bool updateCreatureState();
|
bool updateCreatureState();
|
||||||
void updateIdleStormState();
|
void updateIdleStormState();
|
||||||
|
|
||||||
|
void updateHeadTracking(float duration);
|
||||||
|
|
||||||
void castSpell(const std::string& spellid);
|
void castSpell(const std::string& spellid);
|
||||||
|
|
||||||
void updateMagicEffects();
|
void updateMagicEffects();
|
||||||
@ -229,6 +233,9 @@ public:
|
|||||||
|
|
||||||
bool isReadyToBlock() const;
|
bool isReadyToBlock() const;
|
||||||
bool isKnockedOut() const;
|
bool isKnockedOut() const;
|
||||||
|
|
||||||
|
/// Make this character turn its head towards \a target. To turn off head tracking, pass an empty Ptr.
|
||||||
|
void setHeadTrackTarget(const MWWorld::Ptr& target);
|
||||||
};
|
};
|
||||||
|
|
||||||
void getWeaponGroup(WeaponType weaptype, std::string &group);
|
void getWeaponGroup(WeaponType weaptype, std::string &group);
|
||||||
|
@ -306,6 +306,10 @@ public:
|
|||||||
/// A relative factor (0-1) that decides if and how much the skeleton should be pitched
|
/// A relative factor (0-1) that decides if and how much the skeleton should be pitched
|
||||||
/// to indicate the facing orientation of the character.
|
/// to indicate the facing orientation of the character.
|
||||||
virtual void setPitchFactor(float factor) {}
|
virtual void setPitchFactor(float factor) {}
|
||||||
|
virtual void setHeadPitch(Ogre::Radian factor) {}
|
||||||
|
virtual void setHeadYaw(Ogre::Radian factor) {}
|
||||||
|
virtual Ogre::Radian getHeadPitch() const { return Ogre::Radian(0.f); }
|
||||||
|
virtual Ogre::Radian getHeadYaw() const { return Ogre::Radian(0.f); }
|
||||||
|
|
||||||
virtual Ogre::Vector3 runAnimation(float duration);
|
virtual Ogre::Vector3 runAnimation(float duration);
|
||||||
|
|
||||||
|
@ -207,7 +207,9 @@ NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, Ogre::SceneNode* node, int v
|
|||||||
mFirstPersonOffset(0.f, 0.f, 0.f),
|
mFirstPersonOffset(0.f, 0.f, 0.f),
|
||||||
mAlpha(1.f),
|
mAlpha(1.f),
|
||||||
mNpcType(Type_Normal),
|
mNpcType(Type_Normal),
|
||||||
mSoundsDisabled(disableSounds)
|
mSoundsDisabled(disableSounds),
|
||||||
|
mHeadPitch(0.f),
|
||||||
|
mHeadYaw(0.f)
|
||||||
{
|
{
|
||||||
mNpc = mPtr.get<ESM::NPC>()->mBase;
|
mNpc = mPtr.get<ESM::NPC>()->mBase;
|
||||||
|
|
||||||
@ -621,6 +623,13 @@ Ogre::Vector3 NpcAnimation::runAnimation(float timepassed)
|
|||||||
{
|
{
|
||||||
// In third person mode we may still need pitch for ranged weapon targeting
|
// In third person mode we may still need pitch for ranged weapon targeting
|
||||||
pitchSkeleton(mPtr.getRefData().getPosition().rot[0], baseinst);
|
pitchSkeleton(mPtr.getRefData().getPosition().rot[0], baseinst);
|
||||||
|
|
||||||
|
Ogre::Node* node = baseinst->getBone("Bip01 Head");
|
||||||
|
if (node)
|
||||||
|
{
|
||||||
|
node->rotate(Ogre::Vector3::UNIT_Z, mHeadYaw, Ogre::Node::TS_WORLD);
|
||||||
|
node->rotate(Ogre::Vector3::UNIT_X, mHeadPitch, Ogre::Node::TS_WORLD);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mFirstPersonOffset = 0.f; // reset the X, Y, Z offset for the next frame.
|
mFirstPersonOffset = 0.f; // reset the X, Y, Z offset for the next frame.
|
||||||
|
|
||||||
@ -993,4 +1002,24 @@ void NpcAnimation::setVampire(bool vampire)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NpcAnimation::setHeadPitch(Ogre::Radian pitch)
|
||||||
|
{
|
||||||
|
mHeadPitch = pitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NpcAnimation::setHeadYaw(Ogre::Radian yaw)
|
||||||
|
{
|
||||||
|
mHeadYaw = yaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ogre::Radian NpcAnimation::getHeadPitch() const
|
||||||
|
{
|
||||||
|
return mHeadPitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ogre::Radian NpcAnimation::getHeadYaw() const
|
||||||
|
{
|
||||||
|
return mHeadYaw;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -100,6 +100,9 @@ private:
|
|||||||
float mAlpha;
|
float mAlpha;
|
||||||
bool mSoundsDisabled;
|
bool mSoundsDisabled;
|
||||||
|
|
||||||
|
Ogre::Radian mHeadYaw;
|
||||||
|
Ogre::Radian mHeadPitch;
|
||||||
|
|
||||||
void updateNpcBase();
|
void updateNpcBase();
|
||||||
|
|
||||||
NifOgre::ObjectScenePtr insertBoundedPart(const std::string &model, int group, const std::string &bonename,
|
NifOgre::ObjectScenePtr insertBoundedPart(const std::string &model, int group, const std::string &bonename,
|
||||||
@ -142,6 +145,11 @@ public:
|
|||||||
/// to indicate the facing orientation of the character.
|
/// to indicate the facing orientation of the character.
|
||||||
virtual void setPitchFactor(float factor) { mPitchFactor = factor; }
|
virtual void setPitchFactor(float factor) { mPitchFactor = factor; }
|
||||||
|
|
||||||
|
virtual void setHeadPitch(Ogre::Radian pitch);
|
||||||
|
virtual void setHeadYaw(Ogre::Radian yaw);
|
||||||
|
virtual Ogre::Radian getHeadPitch() const;
|
||||||
|
virtual Ogre::Radian getHeadYaw() const;
|
||||||
|
|
||||||
virtual void showWeapons(bool showWeapon);
|
virtual void showWeapons(bool showWeapon);
|
||||||
virtual void showCarriedLeft(bool show);
|
virtual void showCarriedLeft(bool show);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user