You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

575 lines
20 KiB

/*==LICENSE==*
CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011 Cyan Worlds, Inc.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Additional permissions under GNU GPL version 3 section 7
If you modify this Program, or any covered work, by linking or
combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK,
NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent
JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK
(or a modified version of those libraries),
containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA,
PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG
JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the
licensors of this Program grant you additional
permission to convey the resulting work. Corresponding Source for a
non-source form of such a combination shall include the source code for
the parts of OpenSSL and IJG JPEG Library used as well as that of the covered
work.
You can contact Cyan Worlds, Inc. by email legal@cyan.com
or by snail mail at:
Cyan Worlds, Inc.
14617 N Newport Hwy
Mead, WA 99021
*==LICENSE==*/
#include "plAvCallbackAction.h"
#include "plMessage/plLOSHitMsg.h"
#include "plArmatureMod.h" // for LOS enum type
#include "plMatrixChannel.h"
#include "hsTimer.h"
#include "plPhysicalControllerCore.h"
// Generic geom utils.
hsBool LinearVelocity(hsVector3 &outputV, float elapsed, hsMatrix44 &prevMat, hsMatrix44 &curMat);
void AngularVelocity(hsScalar &outputV, float elapsed, hsMatrix44 &prevMat, hsMatrix44 &curMat);
float AngleRad2d (float x1, float y1, float x3, float y3);
inline hsVector3 GetYAxis(hsMatrix44 &mat)
{
return hsVector3(mat.fMap[1][0], mat.fMap[1][1], mat.fMap[1][2]);
}
plAnimatedController::plAnimatedController(plSceneObject* rootObject, plAGApplicator* rootApp, plPhysicalControllerCore* controller)
: fRootObject(rootObject)
, fRootApp(rootApp)
, fController(controller)
, fTurnStr(0.f)
, fAnimAngVel(0.f)
, fAnimPosVel(0.f, 0.f, 0.f)
{
}
void plAnimatedController::RecalcVelocity(double timeNow, double timePrev, hsBool useAnim /* = true */)
{
if (useAnim)
{
// while you may think it would be correct to cache this,
// what we're actually asking is "what would the animation's
// position be at the previous time given its *current*
// parameters (particularly blends)"
hsMatrix44 prevMat = ((plMatrixChannel *)fRootApp->GetChannel())->Value(timePrev, true);
hsMatrix44 curMat = ((plMatrixChannel *)fRootApp->GetChannel())->Value(timeNow, true);
// If we get a valid linear velocity (ie, we didn't wrap around in the anim),
// use it. Otherwise just reuse the previous frames velocity.
hsVector3 linearVel;
if (LinearVelocity(linearVel, (float)(timeNow - timePrev), prevMat, curMat))
fAnimPosVel = linearVel;
// Automatically sets fAnimAngVel
AngularVelocity(fAnimAngVel, (float)(timeNow - timePrev), prevMat, curMat);
}
else
{
fAnimPosVel.Set(0.f, 0.f, 0.f);
fAnimAngVel = 0.f;
}
if (fController)
fController->SetVelocities(fAnimPosVel, fAnimAngVel + fTurnStr);
}
///////////////////////////////////////////////////////////////////////////
const hsScalar plWalkingController::kControlledFlightThreshold = 1.f; // seconds
plWalkingController::plWalkingController(plSceneObject* rootObject, plAGApplicator* rootApp, plPhysicalControllerCore* controller)
: plAnimatedController(rootObject, rootApp, controller)
, fHitGroundInThisAge(false)
, fWaitingForGround(false)
, fControlledFlightTime(0)
, fControlledFlight(0)
, fImpactTime(0.f)
, fImpactVelocity(0.f, 0.f, 0.f)
, fClearImpact(false)
, fGroundLastFrame(false)
{
if (fController)
{
fWalkingStrategy= TRACKED_NEW plWalkingStrategy(fController);
fController->SetMovementSimulationInterface(fWalkingStrategy);
}
else
fWalkingStrategy = nil;
}
void plWalkingController::RecalcVelocity(double timeNow, double timePrev, hsBool useAnim)
{
if (!fHitGroundInThisAge && fController && fController->IsEnabled() && fWalkingStrategy->IsOnGround())
fHitGroundInThisAge = true; // if we're not pinned and we're not in an age yet, we are now.
if (fClearImpact)
{
fImpactTime = 0.f;
fImpactVelocity.Set(0.f, 0.f, 0.f);
}
if (fController && !fWalkingStrategy->IsOnGround())
{
fImpactTime = fWalkingStrategy->GetAirTime();
fImpactVelocity = fController->GetLinearVelocity();
fClearImpact = false;
}
else
fClearImpact = true;
if (IsControlledFlight())
{
if (fWalkingStrategy && fWalkingStrategy->IsOnGround())
fControlledFlightTime = fWalkingStrategy->GetAirTime();
if(fGroundLastFrame&&(fWalkingStrategy && !fWalkingStrategy->IsOnGround()))
{
//we have started to leave the ground tell the movement strategy in case it cares
fWalkingStrategy->StartJump();
}
if (fControlledFlightTime > kControlledFlightThreshold)
EnableControlledFlight(false);
}
if (fWalkingStrategy)
fGroundLastFrame = fWalkingStrategy->IsOnGround();
else
fGroundLastFrame=false;
plAnimatedController::RecalcVelocity(timeNow, timePrev, useAnim);
}
void plWalkingController::Reset(bool newAge)
{
ActivateController();
if (newAge)
{
if (fWalkingStrategy)
fWalkingStrategy->ResetAirTime();
fHitGroundInThisAge = false;
}
}
void plWalkingController::ActivateController()
{
if (fWalkingStrategy)
{
fWalkingStrategy->RefreshConnectionToControllerCore();
}
else
{
fWalkingStrategy= TRACKED_NEW plWalkingStrategy(fController);
fWalkingStrategy->RefreshConnectionToControllerCore();
}
}
bool plWalkingController::EnableControlledFlight(bool status)
{
if (status)
{
if (fControlledFlight == 0)
fControlledFlightTime = 0.f;
++fControlledFlight;
fWaitingForGround = true;
}
else
fControlledFlight = __max(--fControlledFlight, 0);
return status;
}
plWalkingController::~plWalkingController()
{
delete fWalkingStrategy;
if (fController)
fController->SetMovementSimulationInterface(nil);
}
#if 0
void plWalkingController::Update()
{
// double elapsed = time.asDouble() - getRefresh().asDouble();
// setRefresh(time);
//
// hsBool isPhysical = !fPhysical->GetProperty(plSimulationInterface::kPinned);
// const Havok::Vector3 straightUp(0.0f, 0.0f, 1.0f);
// hsBool alreadyInAge = fHitGroundInThisAge;
//
// int numContacts = fPhysical->GetNumContacts();
// bool ground = false;
// fPushingPhysical = nil;
// int i, j;
/* for(i = 0; i < numContacts; i++)
{
plHKPhysical *contactPhys = fPhysical->GetContactPhysical(i);
if (!contactPhys)
continue; // Physical no longer exists. Skip it.
const Havok::ContactPoint *contact = fPhysical->GetContactPoint(i);
hsScalar dotUp = straightUp.dot(contact->m_normal);
if (dotUp > .5)
ground = true;
else if (contactPhys->GetProperty(plSimulationInterface::kAvAnimPushable))
{
hsPoint3 position;
hsQuat rotation;
fPhysical->GetPositionAndRotationSim(&position, &rotation);
hsQuat inverseRotation = rotation.Inverse();
hsVector3 normal(contact->m_normal.x, contact->m_normal.y, contact->m_normal.z);
fFacingPushingPhysical = (inverseRotation.Rotate(&kAvatarForward).InnerProduct(normal) < 0 ? true : false);
fPushingPhysical = contactPhys;
}
}
// We need to check for the case where the avatar hasn't collided with "ground", but is colliding
// with a few other objects so that he's not actually falling (wedged in between some slopes).
// We do this by answering the following question (in 2d top-down space): "If you sort the contact
// normals by angle, is there a large enough gap between normals?"
//
// If you think in terms of geometry, this means a collection of surfaces are all pushing on you.
// If they're pushing from all sides, you have nowhere to go, and you won't fall. There needs to be
// a gap, so that you're pushed out and have somewhere to fall. This is the same as finding a gap
// larger than 180 degrees between sorted normals.
//
// The problem is that on top of that, the avatar needs enough force to shove him out that gap (he
// has to overcome friction). I deal with that by making the threshold (360 - (180 - 60) = 240). I've
// seen up to 220 reached in actual gameplay in a situation where we'd want this to take effect.
// This is the same running into 2 walls where the angle between them is 60.
const hsScalar threshold = hsScalarDegToRad(240);
if (!ground && numContacts >= 2)
{
// Can probably do a special case for exactly 2 contacts. Not sure if it's worth it...
fCollisionAngles.SetCount(numContacts);
for (i = 0; i < numContacts; i++)
{
const Havok::ContactPoint *contact = fPhysical->GetContactPoint(i);
fCollisionAngles[i] = hsATan2(contact->m_normal.y, contact->m_normal.x);
}
// numContacts is rarely larger than 6, so let's do a simple bubble sort.
for (i = 0; i < numContacts; i++)
{
for (j = i + 1; j < numContacts; j++)
{
if (fCollisionAngles[i] > fCollisionAngles[j])
{
hsScalar tempAngle = fCollisionAngles[i];
fCollisionAngles[i] = fCollisionAngles[j];
fCollisionAngles[j] = tempAngle;
}
}
}
// sorted, now we check.
for (i = 1; i < numContacts; i++)
{
if (fCollisionAngles[i] - fCollisionAngles[i - 1] >= threshold)
break;
}
if (i == numContacts)
{
// We got to the end. Check the last with the first and make your decision.
if (!(fCollisionAngles[0] - fCollisionAngles[numContacts - 1] >= (threshold - 2 * hsScalarPI)))
ground = true;
}
}
*/
bool ground = fController ? fController->GotGroundHit() : true;
bool isPhysical = true;
if (!fHitGroundInThisAge && isPhysical)
fHitGroundInThisAge = true; // if we're not pinned and we're not in an age yet, we are now.
if (IsControlledFlight())
fControlledFlightTime += (hsScalar)elapsed;
if (fControlledFlightTime > kControlledFlightThreshold && numContacts > 0)
EnableControlledFlight(false);
if (ground || !isPhysical)
{
if (!IsControlledFlight() && !IsOnGround())
{
// The first ground contact in an age doesn't count.
// if (alreadyInAge)
// {
// hsVector3 vel;
// fPhysical->GetLinearVelocitySim(vel);
// fImpactVel = vel.fZ;
// fTimeInAirPeak = (hsScalar)(fTimeInAir + elapsed);
// }
fWaitingForGround = false;
}
fTimeInAir = 0;
}
else if (elapsed < plSimulationMgr::GetInstance()->GetMaxDelta())
{
// If the simultation skipped a huge chunk of time, we didn't process the
// collisions, which could trick us into thinking we've just gone a long
// time without hitting ground. So we only count the time if this wasn't
// the case.
fTimeInAir += (hsScalar)elapsed;
}
// Tweakage so that we still fall under the right conditions.
// If we're in controlled flight, or standing still with ground solidly under us (probe hit). We only use anim velocity.
// if (!IsControlledFlight() && !(ground && fProbeHitGround && fAnimPosVel.fX == 0 && fAnimPosVel.fY == 0))
// {
// hsVector3 curV;
// fPhysical->GetLinearVelocitySim(curV);
// fAnimPosVel.fZ = curV.fZ;
//
// // Prevents us from going airborn from running up bumps/inclines.
// if (IsOnGround() && fAnimPosVel.fZ > 0.f)
// fAnimPosVel.fZ = 0.f;
//
// // Unless we're on the ground and moving, or standing still with a probe hit, we use the sim's other axes too.
// if (!(IsOnGround() && (fProbeHitGround || fAnimPosVel.fX != 0 || fAnimPosVel.fY != 0)))
// {
// fAnimPosVel.fX = curV.fX;
// fAnimPosVel.fY = curV.fY;
// }
// }
//
// fPhysical->SetLinearVelocitySim(fAnimPosVel);
// fPhysical->SetSpin(fAnimAngVel + fTurnStr, hsVector3(0.0f, 0.0f, 1.0f));
}
#endif
#if 0
/////////////////////////////////////////////////////////////////////////
plSimDefs::ActionType plHorizontalFreezeAction::GetType()
{
return plSimDefs::kHorizontalFreeze;
}
void plHorizontalFreezeAction::apply(Havok::Subspace &s, Havok::hkTime time)
{
double elapsed = time.asDouble() - getRefresh().asDouble();
setRefresh(time);
int numContacts = fPhysical->GetNumContacts();
bool ground = false;
const Havok::Vector3 straightUp(0.0f, 0.0f, 1.0f);
int i;
for(i = 0; i < numContacts; i++)
{
const Havok::ContactPoint *contact = fPhysical->GetContactPoint(i);
hsScalar dotUp = straightUp.dot(contact->m_normal);
if (dotUp > .5)
ground = true;
}
hsVector3 vel;
fPhysical->GetLinearVelocitySim(vel);
vel.fX = 0.0;
vel.fY = 0.0;
if (ground)
vel.fZ = 0;
fPhysical->SetLinearVelocitySim(vel);
fPhysical->ClearContacts();
}
#endif
plSwimmingController::plSwimmingController(plSceneObject* rootObject, plAGApplicator* rootApp, plPhysicalControllerCore* controller)
:plAnimatedController(rootObject,rootApp,controller)
{
if (controller)
fSwimmingStrategy= TRACKED_NEW plSwimStrategy(controller);
else
fSwimmingStrategy = nil;
}
plSwimmingController::~plSwimmingController()
{
delete fSwimmingStrategy;
}
plRidingAnimatedPhysicalController::plRidingAnimatedPhysicalController(plSceneObject* rootObject, plAGApplicator* rootApp, plPhysicalControllerCore* controller)
: plWalkingController(rootObject, rootApp, controller)
{
if(controller)
fWalkingStrategy = TRACKED_NEW plRidingAnimatedPhysicalStrategy(controller);
else
fWalkingStrategy = nil;
}
plRidingAnimatedPhysicalController::~plRidingAnimatedPhysicalController()
{
delete fWalkingStrategy;
fWalkingStrategy=nil;
}
//////////////////////////////////////////////////////////////////////////
/*
Purpose:
ANGLE_RAD_2D returns the angle in radians swept out between two rays in 2D.
Discussion:
Except for the zero angle case, it should be true that
ANGLE_RAD_2D(X1,Y1,X2,Y2,X3,Y3)
+ ANGLE_RAD_2D(X3,Y3,X2,Y2,X1,Y1) = 2 * PI
Modified:
19 April 1999
Author:
John Burkardt
Parameters:
Input, float X1, Y1, X2, Y2, X3, Y3, define the rays
( X1-X2, Y1-Y2 ) and ( X3-X2, Y3-Y2 ) which in turn define the
angle, counterclockwise from ( X1-X2, Y1-Y2 ).
Output, float ANGLE_RAD_2D, the angle swept out by the rays, measured
in radians. 0 <= ANGLE_DEG_2D < 2 PI. If either ray has zero length,
then ANGLE_RAD_2D is set to 0.
*/
static float AngleRad2d ( float x1, float y1, float x3, float y3 )
{
float value;
float x;
float y;
x = ( x1 ) * ( x3 ) + ( y1 ) * ( y3 );
y = ( x1 ) * ( y3 ) - ( y1 ) * ( x3 );
if ( x == 0.0 && y == 0.0 ) {
value = 0.0;
}
else
{
value = atan2 ( y, x );
if ( value < 0.0 )
{
value = (float)(value + TWO_PI);
}
}
return value;
}
static hsBool LinearVelocity(hsVector3 &outputV, float elapsed, hsMatrix44 &prevMat, hsMatrix44 &curMat)
{
bool result = false;
hsPoint3 startPos(0.0f, 0.0f, 0.0f); // default position (at start of anim)
hsPoint3 prevPos = prevMat.GetTranslate(); // position previous frame
hsPoint3 nowPos = curMat.GetTranslate(); // position current frame
hsVector3 prev2Now = (hsVector3)(nowPos - prevPos); // frame-to-frame delta
if (fabs(prev2Now.fX) < 0.0001f && fabs(prev2Now.fY) < 0.0001f && fabs(prev2Now.fZ) < 0.0001f)
{
outputV.Set(0.f, 0.f, 0.f);
result = true;
}
else
{
hsVector3 start2Now = (hsVector3)(nowPos - startPos); // start-to-frame delta
float prev2NowMagSqr = prev2Now.MagnitudeSquared();
float start2NowMagSqr = start2Now.MagnitudeSquared();
float dot = prev2Now.InnerProduct(start2Now);
// HANDLING ANIMATION WRAPPING:
// the vector from the animation origin to the current frame should point in roughly
// the same direction as the vector from the previous animation position to the
// current animation position.
//
// If they don't agree (dot < 0,) then we probably mpst wrapped around.
// The right answer would be to compare the current frame to the start of
// the anim loop, but it's cheaper to cheat and return false,
// telling the caller to use the previous frame's velocity.
if (dot > 0.0f)
{
prev2Now /= elapsed;
float xfabs = fabs(prev2Now.fX);
float yfabs = fabs(prev2Now.fY);
float zfabs = fabs(prev2Now.fZ);
static const float maxVel = 20.0f;
hsBool valid = xfabs < maxVel && yfabs < maxVel && zfabs < maxVel;
if (valid)
{
outputV = prev2Now;
result = true;
}
}
}
return result;
}
static void AngularVelocity(hsScalar &outputV, float elapsed, hsMatrix44 &prevMat, hsMatrix44 &curMat)
{
outputV = 0.f;
hsScalar appliedVelocity = 0.0f;
hsVector3 prevForward = GetYAxis(prevMat);
hsVector3 curForward = GetYAxis(curMat);
hsScalar angleSincePrev = AngleRad2d(curForward.fX, curForward.fY, prevForward.fX, prevForward.fY);
hsBool sincePrevSign = angleSincePrev > 0.0f;
if (angleSincePrev > hsScalarPI)
angleSincePrev = angleSincePrev - TWO_PI;
const hsVector3 startForward = hsVector3(0, -1.0, 0); // the Y orientation of a "resting" armature....
hsScalar angleSinceStart = AngleRad2d(curForward.fX, curForward.fY, startForward.fX, startForward.fY);
hsBool sinceStartSign = angleSinceStart > 0.0f;
if (angleSinceStart > hsScalarPI)
angleSinceStart = angleSinceStart - TWO_PI;
// HANDLING ANIMATION WRAPPING:
// under normal conditions, the angle from rest to the current frame will have the same
// sign as the angle from the previous frame to the current frame.
// if it does not, we have (most likely) wrapped the motivating animation from frame n back
// to frame zero, creating a large angle from the previous frame to the current one
if (sincePrevSign == sinceStartSign)
{
// signs are the same; didn't wrap; use the frame-to-frame angle difference
appliedVelocity = angleSincePrev / elapsed; // rotation / time
if (fabs(appliedVelocity) < 3)
{
outputV = appliedVelocity;
}
}
}