mirror of
https://github.com/OpenMW/openmw.git
synced 2025-01-16 21:59:55 +00:00
Implement NPC head tracking (Fixes #1720)
This commit is contained in:
parent
b9e5aa9db6
commit
d962f0918d
7 changed files with 165 additions and 1 deletions
|
@ -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…
Reference in a new issue