diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 73baa4e72e..5f425f7a03 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -64,7 +64,7 @@ add_openmw_dir (mwclass add_openmw_dir (mwmechanics mechanicsmanagerimp stat character creaturestats magiceffects movement actors activators drawstate spells activespells npcstats aipackage aisequence alchemy aiwander aitravel aifollow - aiescort aiactivate + aiescort aiactivate movementsolver ) add_openmw_dir (mwbase diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index a426e34543..5f6e27867d 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -20,6 +20,11 @@ namespace OEngine { class Fader; } + + namespace Physic + { + class PhysicEngine; + } } namespace ESM @@ -300,6 +305,8 @@ namespace MWBase /// 2 - player is underwater \n /// 3 - enemies are nearby (not implemented) + /// \todo Probably shouldn't be here + virtual OEngine::Physic::PhysicEngine* getPhysicEngine() const = 0; /// \todo Probably shouldn't be here virtual MWRender::Animation* getAnimation(const MWWorld::Ptr &ptr) = 0; diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index fed6e3e3ef..7962e197ee 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -25,9 +25,13 @@ #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" +#include "movementsolver.hpp" + + namespace MWMechanics { @@ -75,6 +79,7 @@ static void getStateInfo(CharacterState state, std::string *group) CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Animation *anim, CharacterState state, bool loop) : mPtr(ptr), mAnimation(anim), mState(state), mSkipAnim(false) { + mMovementSolver = new MovementSolver(mPtr); if(!mAnimation) return; @@ -93,12 +98,18 @@ CharacterController::CharacterController(const CharacterController &rhs) , mCurrentGroup(rhs.mCurrentGroup), mState(rhs.mState) , mSkipAnim(rhs.mSkipAnim) { + mMovementSolver = new MovementSolver(mPtr); if(!mAnimation) return; /* We've been copied. Update the animation with the new controller. */ mAnimation->setController(this); } +CharacterController::~CharacterController() +{ + delete mMovementSolver; +} + void CharacterController::markerEvent(float time, const std::string &evt) { @@ -160,18 +171,25 @@ Ogre::Vector3 CharacterController::update(float duration) setState(CharState_Idle, true); } - // FIXME: The speed should actually be determined by the character's stance - // (running, sneaking, etc) and stats, rather than the length of the vector. - float speed = std::max(1.0f, vec.length() / 32.0f); - if(mAnimation) - mAnimation->setSpeedMult(speed); - Ogre::Vector3 movement = Ogre::Vector3::ZERO; if(mAnimation && !mSkipAnim) + { + // FIXME: The speed should actually be determined by the character's + // stance (running, sneaking, etc) and stats + mAnimation->setSpeedMult(1.0f); movement += mAnimation->runAnimation(duration); + } mSkipAnim = false; - return movement; + if(duration > 0.0f) + { + Ogre::Vector3 pos(mPtr.getCellRef().mPos.pos); + // FIXME: Get the actual radius for the object. Maybe this should go into mwworld to replace pmove? + Ogre::Vector3 res = mMovementSolver->move(pos, movement, duration, Ogre::Vector3(15,15,30)); + MWBase::Environment::get().getWorld()->moveObject(mPtr, res.x, res.y, res.z); + } + + return Ogre::Vector3(0.0f); } diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index c5f29beef8..efd90ca196 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -13,6 +13,8 @@ namespace MWRender namespace MWMechanics { +class MovementSolver; + enum CharacterState { CharState_Idle, CharState_Idle2, @@ -49,6 +51,8 @@ class CharacterController CharacterState mState; bool mSkipAnim; + MovementSolver *mMovementSolver; + protected: /* Called by the animation whenever a new text key is reached. */ void markerEvent(float time, const std::string &evt); @@ -58,6 +62,7 @@ protected: public: CharacterController(const MWWorld::Ptr &ptr, MWRender::Animation *anim, CharacterState state, bool loop); CharacterController(const CharacterController &rhs); + virtual ~CharacterController(); Ogre::Vector3 update(float duration); diff --git a/apps/openmw/mwmechanics/movementsolver.cpp b/apps/openmw/mwmechanics/movementsolver.cpp new file mode 100644 index 0000000000..219f077e4a --- /dev/null +++ b/apps/openmw/mwmechanics/movementsolver.cpp @@ -0,0 +1,154 @@ +#include "movementsolver.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +namespace MWMechanics +{ + +MovementSolver::MovementSolver(const MWWorld::Ptr &ptr) + : mPtr(ptr) + , mEngine(MWBase::Environment::get().getWorld()->getPhysicEngine()) +{ +} + +MovementSolver::~MovementSolver() +{ + // nothing to do +} + +void MovementSolver::clipVelocity(const Ogre::Vector3& in, const Ogre::Vector3& normal, Ogre::Vector3& out, const float overbounce) +{ + //Math stuff. Basically just project the velocity vector onto the plane represented by the normal. + //More specifically, it projects velocity onto the normal, takes that result, multiplies it by overbounce and then subtracts it from velocity. + float backoff; + + backoff = in.dotProduct(normal); + if(backoff < 0.0f) + backoff *= overbounce; + else + backoff /= overbounce; + + out = in - (normal*backoff); +} + +void MovementSolver::projectVelocity(Ogre::Vector3& velocity, const Ogre::Vector3& direction) +{ + Ogre::Vector3 normalizedDirection(direction); + normalizedDirection.normalise(); + + // no divide by normalizedDirection.length necessary because it's normalized + velocity = normalizedDirection * velocity.dotProduct(normalizedDirection); +} + +bool MovementSolver::stepMove(Ogre::Vector3& position, const Ogre::Vector3 &velocity, float remainingTime, float verticalRotation, const Ogre::Vector3 &halfExtents, bool isInterior) +{ + static const float maxslope = 45.0f; + traceResults trace; // no initialization needed + + newtrace(&trace, position+Ogre::Vector3(0.0f,0.0f,STEPSIZE), + position+Ogre::Vector3(0.0f,0.0f,STEPSIZE)+velocity*remainingTime, + halfExtents, verticalRotation, isInterior, mEngine); + if(trace.fraction == 0.0f || (trace.fraction != 1.0f && getSlope(trace.planenormal) > maxslope)) + return false; + + newtrace(&trace, trace.endpos, trace.endpos-Ogre::Vector3(0,0,STEPSIZE), halfExtents, verticalRotation, isInterior, mEngine); + if(getSlope(trace.planenormal) < maxslope) + { + // only step down onto semi-horizontal surfaces. don't step down onto the side of a house or a wall. + position = trace.endpos; + return true; + } + + return false; +} + +float MovementSolver::getSlope(const Ogre::Vector3 &normal) +{ + return normal.angleBetween(Ogre::Vector3(0.0f,0.0f,1.0f)).valueDegrees(); +} + + +Ogre::Vector3 MovementSolver::move(const Ogre::Vector3 &position, const Ogre::Vector3 &movement, float time, const Ogre::Vector3 &halfExtents) +{ + mPhysicActor = mEngine->getCharacter(mPtr.getRefData().getHandle()); + + /* Anything to collide with? */ + if(1 || !mPhysicActor || !mPhysicActor->getCollisionMode()) + return position+movement; + + traceResults trace; //no initialization needed + int iterations=0, maxIterations=50; //arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared. + float maxslope=45; + + Ogre::Vector3 horizontalVelocity = movement/time; + Ogre::Vector3 velocity(horizontalVelocity.x, horizontalVelocity.y, verticalVelocity); // we need a copy of the velocity before we start clipping it for steps + Ogre::Vector3 clippedVelocity(horizontalVelocity.x, horizontalVelocity.y, verticalVelocity); + + float remainingTime = time; + bool isInterior = !mPtr.getCell()->isExterior(); + float verticalRotation = mPhysicActor->getRotation().getYaw().valueDegrees(); + + Ogre::Vector3 lastNormal(0.0f); + Ogre::Vector3 currentNormal(0.0f); + Ogre::Vector3 up(0.0f, 0.0f, 1.0f); + Ogre::Vector3 newPosition = position; + + newtrace(&trace, position, position+Ogre::Vector3(0,0,-10), halfExtents, verticalRotation, isInterior, mEngine); + if(trace.fraction < 1.0f) + { + if(getSlope(trace.planenormal) > maxslope) + { + // if we're on a really steep slope, don't listen to user input + clippedVelocity.x = clippedVelocity.y = 0.0f; + } + else + { + // if we're within 10 units of the ground, force velocity to track the ground + clipVelocity(clippedVelocity, trace.planenormal, clippedVelocity, 1.0f); + } + } + + do { + // trace to where character would go if there were no obstructions + newtrace(&trace, newPosition, newPosition+clippedVelocity*remainingTime, halfExtents, verticalRotation, isInterior, mEngine); + newPosition = trace.endpos; + currentNormal = trace.planenormal; + remainingTime = remainingTime * (1.0f-trace.fraction); + + // check for obstructions + if(trace.fraction != 1.0f) + { + //std::cout<<"angle: "< maxslope || currentNormal == lastNormal) + { + if(stepMove(newPosition, velocity, remainingTime, verticalRotation, halfExtents, mEngine)) + std::cout<< "stepped" < + +namespace MWMechanics +{ + class MovementSolver + { + public: + MovementSolver(const MWWorld::Ptr &ptr); + virtual ~MovementSolver(); + + Ogre::Vector3 move(const Ogre::Vector3 &position, const Ogre::Vector3 &movement, float time, const Ogre::Vector3 &halfExtents); + + private: + bool stepMove(Ogre::Vector3& position, const Ogre::Vector3 &velocity, float remainingTime, float verticalRotation, const Ogre::Vector3 &halfExtents, bool isInterior); + + void clipVelocity(const Ogre::Vector3& in, const Ogre::Vector3& normal, Ogre::Vector3& out, const float overbounce); + void projectVelocity(Ogre::Vector3& velocity, const Ogre::Vector3& direction); + + float getSlope(const Ogre::Vector3 &normal); + + MWWorld::Ptr mPtr; + OEngine::Physic::PhysicEngine *mEngine; + OEngine::Physic::PhysicActor *mPhysicActor; + + float verticalVelocity; + }; +} + +#endif /* GAME_MWMECHANICS_MOVEMENTSOLVER_H */ diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 4899a88070..062387e922 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -347,6 +347,10 @@ namespace MWWorld /// 2 - player is underwater \n /// 3 - enemies are nearby (not implemented) + /// \todo Probably shouldn't be here + virtual OEngine::Physic::PhysicEngine* getPhysicEngine() const + { return mPhysEngine; } + /// \todo Probably shouldn't be here virtual MWRender::Animation* getAnimation(const MWWorld::Ptr &ptr);