Implement NPC head tracking (Fixes #1720)

moveref
scrawl 10 years ago
parent b9e5aa9db6
commit d962f0918d

@ -4,6 +4,7 @@
#include <typeinfo>
#include <OgreVector3.h>
#include <OgreSceneNode.h>
#include <components/esm/loadnpc.hpp>
@ -273,6 +274,40 @@ namespace MWMechanics
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)
{
CreatureStats& creatureStats = actor1.getClass().getCreatureStats(actor1);
@ -1138,9 +1173,11 @@ namespace MWMechanics
if(!paused)
{
static float timerUpdateAITargets = 0;
static float timerUpdateHeadTrack = 0;
// target lists get updated once every 1.0 sec
if (timerUpdateAITargets >= 1.0f) timerUpdateAITargets = 0;
if (timerUpdateHeadTrack >= 0.3f) timerUpdateHeadTrack = 0;
MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr();
@ -1174,6 +1211,19 @@ namespace MWMechanics
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)
updateCrimePersuit(iter->first, duration);
@ -1194,6 +1244,7 @@ namespace MWMechanics
}
timerUpdateAITargets += duration;
timerUpdateHeadTrack += duration;
// Looping magic VFX update
// 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 updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor,
MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance);
void restoreDynamicStats(bool sleep);
///< If the player is sleeping, this should be called every hour.

@ -42,6 +42,15 @@
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)
{
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;
// 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.
updateHeadTracking(duration);
}
else if(cls.getCreatureStats(mPtr).isDead())
{
@ -1845,4 +1856,55 @@ bool CharacterController::isKnockedOut() const
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 mSecondsOfRunning;
MWWorld::Ptr mHeadTrackTarget;
float mTurnAnimationThreshold; // how long to continue playing turning animation after actor stopped turning
std::string mAttackType; // slash, chop or thrust
@ -188,6 +190,8 @@ class CharacterController
bool updateCreatureState();
void updateIdleStormState();
void updateHeadTracking(float duration);
void castSpell(const std::string& spellid);
void updateMagicEffects();
@ -229,6 +233,9 @@ public:
bool isReadyToBlock() 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);

@ -306,6 +306,10 @@ public:
/// A relative factor (0-1) that decides if and how much the skeleton should be pitched
/// to indicate the facing orientation of the character.
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);

@ -207,7 +207,9 @@ NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, Ogre::SceneNode* node, int v
mFirstPersonOffset(0.f, 0.f, 0.f),
mAlpha(1.f),
mNpcType(Type_Normal),
mSoundsDisabled(disableSounds)
mSoundsDisabled(disableSounds),
mHeadPitch(0.f),
mHeadYaw(0.f)
{
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
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.
@ -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;
bool mSoundsDisabled;
Ogre::Radian mHeadYaw;
Ogre::Radian mHeadPitch;
void updateNpcBase();
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.
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 showCarriedLeft(bool show);

Loading…
Cancel
Save