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.
1076 lines
31 KiB
1076 lines
31 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 "hsTypes.h" |
|
#include "hsStlUtils.h" |
|
#include "hsMatrix44.h" |
|
#include "hsGeometry3.h" |
|
#include "plgDispatch.h" |
|
#include "hsResMgr.h" |
|
#include "../pnMessage/plTimeMsg.h" |
|
#include "../pnMessage/plRefMsg.h" |
|
#include "../plMessage/plAgeLoadedMsg.h" |
|
#include "../pnSceneObject/plSceneObject.h" |
|
#include "hsTimer.h" |
|
#include "../plMath/plRandom.h" |
|
#include "../pnMessage/plEnableMsg.h" |
|
#include "../plMessage/plAnimCmdMsg.h" |
|
#include "../plMessage/plLoadCloneMsg.h" |
|
|
|
//#include "../plPipeline/plDebugGeometry.h" |
|
|
|
#include <math.h> |
|
#include <algorithm> |
|
|
|
#include "pfObjectFlocker.h" |
|
|
|
#define PI 3.14159f |
|
#define HALF_PI (PI/2) |
|
#define GRAVITY 9.806650f // meters/second |
|
#ifdef INFINITY |
|
#undef INFINITY |
|
#endif |
|
#define INFINITY 999999.0f |
|
|
|
#define RAND() (float) (rand()/(RAND_MAX * 1.0)) |
|
#define SIGN(x) (((x) < 0) ? -1 : 1) |
|
|
|
const int pfObjectFlocker::fFileVersion = 1; |
|
|
|
#define FLOCKER_SHOW_DEBUG_LINES 0 |
|
|
|
#if FLOCKER_SHOW_DEBUG_LINES |
|
// make a few easy-to-use colors for the debug lines |
|
#define DEBUG_COLOR_RED 255, 0, 0 |
|
#define DEBUG_COLOR_GREEN 0, 255, 0 |
|
#define DEBUG_COLOR_BLUE 0, 0, 255 |
|
#define DEBUG_COLOR_YELLOW 255, 255, 0 |
|
#define DEBUG_COLOR_CYAN 0, 255, 255 |
|
#define DEBUG_COLOR_PINK 255, 0, 255 |
|
#endif |
|
|
|
/////////////////////////////////////////////////////////////////////////////// |
|
// Some quick utility functions |
|
/////////////////////////////////////////////////////////////////////////////// |
|
|
|
// Basic linear interpolation |
|
template<class T> |
|
inline T Interpolate(float alpha, const T& x0, const T& x1) |
|
{ |
|
return x0 + ((x1 - x0) * alpha); |
|
} |
|
|
|
// Clip a value to the min and max, if necessary |
|
inline float Clip(const float x, const float min, const float max) |
|
{ |
|
if (x < min) return min; |
|
if (x > max) return max; |
|
return x; |
|
} |
|
|
|
inline float ScalarRandomWalk(const float initial, const float walkspeed, const float min, const float max) |
|
{ |
|
const float next = initial + (((RAND() * 2) - 1) * walkspeed); |
|
if (next < min) return min; |
|
if (next > max) return max; |
|
return next; |
|
} |
|
|
|
// Classify a value relative to the interval between two bounds: |
|
// returns -1 when below the lower bound |
|
// returns 0 when between the bounds (inside the interval) |
|
// returns +1 when above the upper bound |
|
inline int IntervalComparison (float x, float lowerBound, float upperBound) |
|
{ |
|
if (x < lowerBound) return -1; |
|
if (x > upperBound) return +1; |
|
return 0; |
|
} |
|
|
|
// Blend the new value into the accumulator using the smooth rate |
|
// If smoothRate is 0 the accumulator will not change. |
|
// If smoothRate is 1 the accumulator will be set to the new value with no smoothing. |
|
// Useful values are "near zero". |
|
template<class T> |
|
inline void BlendIntoAccumulator(const float smoothRate, const T &newValue, T &smoothedAccumulator) |
|
{ |
|
smoothedAccumulator = Interpolate(Clip(smoothRate, 0, 1), smoothedAccumulator, newValue); |
|
} |
|
|
|
// return component of vector parallel to a unit basis vector |
|
// (IMPORTANT NOTE: assumes "basis" has unit magnitude (length==1)) |
|
inline hsVector3 ParallelComponent(const hsVector3 &vec, const hsVector3 &unitBasis) |
|
{ |
|
const float projection = vec * unitBasis; |
|
return unitBasis * projection; |
|
} |
|
|
|
// return component of vector perpendicular to a unit basis vector |
|
// (IMPORTANT NOTE: assumes "basis" has unit magnitude (length==1)) |
|
inline hsVector3 PerpendicularComponent(const hsVector3 &vec, const hsVector3& unitBasis) |
|
{ |
|
return vec - ParallelComponent(vec, unitBasis); |
|
} |
|
|
|
// clamps the length of a given vector to maxLength. If the vector is |
|
// shorter its value is returned unaltered, if the vector is longer |
|
// the value returned has length of maxLength and is parallel to the |
|
// original input. |
|
hsVector3 TruncateLength (const hsVector3 &vec, const float maxLength) |
|
{ |
|
const float maxLengthSquared = maxLength * maxLength; |
|
const float vecLengthSquared = vec.MagnitudeSquared(); |
|
if (vecLengthSquared <= maxLengthSquared) |
|
return vec; |
|
else |
|
return vec * (maxLength / sqrt(vecLengthSquared)); |
|
} |
|
|
|
// Enforce an upper bound on the angle by which a given arbitrary vector |
|
// diviates from a given reference direction (specified by a unit basis |
|
// vector). The effect is to clip the "source" vector to be inside a cone |
|
// defined by the basis and an angle. |
|
hsVector3 LimitMaxDeviationAngle(const hsVector3 &vec, const float cosineOfConeAngle, const hsVector3 &basis) |
|
{ |
|
// immediately return zero length input vectors |
|
float sourceLength = vec.Magnitude(); |
|
if (sourceLength == 0) return vec; |
|
|
|
// measure the angular diviation of "source" from "basis" |
|
const hsVector3 direction = vec / sourceLength; |
|
float cosineOfSourceAngle = direction * basis; |
|
|
|
// Simply return "source" if it already meets the angle criteria. |
|
if (cosineOfSourceAngle >= cosineOfConeAngle) return vec; |
|
|
|
// find the portion of "source" that is perpendicular to "basis" |
|
const hsVector3 perp = PerpendicularComponent(vec, basis); |
|
|
|
// normalize that perpendicular |
|
hsVector3 unitPerp = perp; |
|
unitPerp.Normalize(); |
|
|
|
// construct a new vector whose length equals the source vector, |
|
// and lies on the intersection of a plane (formed the source and |
|
// basis vectors) and a cone (whose axis is "basis" and whose |
|
// angle corresponds to cosineOfConeAngle) |
|
float perpDist = sqrt(1 - (cosineOfConeAngle * cosineOfConeAngle)); |
|
const hsVector3 c0 = basis * cosineOfConeAngle; |
|
const hsVector3 c1 = unitPerp * perpDist; |
|
return (c0 + c1) * sourceLength; |
|
} |
|
|
|
/////////////////////////////////////////////////////////////////////////////// |
|
// pfVehicle functions |
|
/////////////////////////////////////////////////////////////////////////////// |
|
|
|
void pfVehicle::IMeasurePathCurvature(const float elapsedTime) |
|
{ |
|
if (elapsedTime > 0) |
|
{ |
|
const hsVector3 deltaPosition(&fLastPos, &Position()); |
|
const hsVector3 deltaForward = (fLastForward - Forward()) / deltaPosition.Magnitude(); |
|
const hsVector3 lateral = PerpendicularComponent(deltaForward, Forward()); |
|
const float sign = ((lateral * Side()) < 0) ? 1.0f : -1.0f; |
|
fCurvature = lateral.Magnitude() * sign; |
|
BlendIntoAccumulator(elapsedTime * 4.0f, fCurvature, fSmoothedCurvature); |
|
fLastForward = Forward(); |
|
fLastPos = Position(); |
|
} |
|
} |
|
|
|
// Reset functions |
|
void pfVehicle::Reset() |
|
{ |
|
ResetLocalSpace(); |
|
|
|
SetMass(1); // defaults to 1 so acceleration = force |
|
SetSpeed(0); // speed along the forward direction |
|
|
|
SetMaxForce(10.0f); // steering force is clipped to this magnitude |
|
SetMaxSpeed(5.0f); // velocity is clipped to this magnitude |
|
|
|
SetRadius(0.5f); // size of bounding sphere |
|
|
|
// Reset bookkeeping for our averages |
|
ResetSmoothedPosition(); |
|
ResetSmoothedCurvature(); |
|
ResetSmoothedAcceleration(); |
|
} |
|
|
|
float pfVehicle::ResetSmoothedCurvature(float value /* = 0 */) |
|
{ |
|
fLastForward.Set(0, 0, 0); |
|
fLastPos.Set(0, 0, 0); |
|
return fSmoothedCurvature = fCurvature = value; |
|
} |
|
|
|
hsVector3 pfVehicle::ResetSmoothedAcceleration(const hsVector3 &value /* = hsVector3(0,0,0) */) |
|
{ |
|
return fSmoothedAcceleration = value; |
|
} |
|
|
|
hsPoint3 pfVehicle::ResetSmoothedPosition(const hsPoint3 &value /* = hsPoint3(0,0,0) */) |
|
{ |
|
return fSmoothedPosition = value; |
|
} |
|
|
|
void pfVehicle::ResetLocalSpace() |
|
{ |
|
fSide.Set(1, 0, 0); |
|
fForward.Set(0, 1, 0); |
|
fUp.Set(0, 0, 1); |
|
fPos.Set(0, 0, 0); |
|
} |
|
|
|
// Geometry functions |
|
void pfVehicle::SetUnitSideFromForwardAndUp() |
|
{ |
|
// derive new unit side vector from forward and up |
|
fSide = fForward % fUp; |
|
fSide.Normalize(); |
|
} |
|
|
|
void pfVehicle::RegenerateOrthonormalBasisUF(const hsVector3 &newUnitForward) |
|
{ |
|
fForward = newUnitForward; |
|
|
|
// derive new side vector from NEW forward and OLD up |
|
SetUnitSideFromForwardAndUp(); |
|
|
|
// derive new up vector from new side and new forward (should have unit length since side and forward are |
|
// perpendicular and unit length) |
|
fUp = fSide % fForward; |
|
} |
|
|
|
void pfVehicle::RegenerateLocalSpace(const hsVector3 &newVelocity, const float /*elapsedTime*/) |
|
{ |
|
// adjust orthonormal basis vectors to be aligned with new velocity |
|
if (Speed() > 0) |
|
RegenerateOrthonormalBasisUF(newVelocity / Speed()); |
|
} |
|
|
|
void pfVehicle::RegenerateLocalSpaceForBanking(const hsVector3 &newVelocity, const float elapsedTime) |
|
{ |
|
// the length of this global-upward-pointing vector controls the vehicle's |
|
// tendency to right itself as it is rolled over from turning acceleration |
|
const hsVector3 globalUp(0, 0, 0.2f); |
|
|
|
// acceleration points toward the center of local path curvature, the |
|
// length determines how much the vehicle will roll while turning |
|
const hsVector3 accelUp = fSmoothedAcceleration * 0.05f; |
|
|
|
// combined banking, sum of up due to turning and global up |
|
const hsVector3 bankUp = accelUp + globalUp; |
|
|
|
// blend bankUp into vehicle's up vector |
|
const float smoothRate = elapsedTime * 3; |
|
hsVector3 tempUp = Up(); |
|
BlendIntoAccumulator(smoothRate, bankUp, tempUp); |
|
tempUp.Normalize(); |
|
SetUp(tempUp); |
|
|
|
// adjust orthonormal basis vectors to be aligned with new velocity |
|
if (Speed() > 0) |
|
RegenerateOrthonormalBasisUF(newVelocity / Speed()); |
|
} |
|
|
|
// Physics functions |
|
void pfVehicle::ApplySteeringForce(const hsVector3 &force, const float deltaTime) |
|
{ |
|
const hsVector3 adjustedForce = AdjustRawSteeringForce(force, deltaTime); |
|
|
|
// enforce limit on magnitude of steering force |
|
const hsVector3 clippedForce = TruncateLength(adjustedForce, MaxForce()); |
|
|
|
#if FLOCKER_SHOW_DEBUG_LINES |
|
// Draw the adjusted force vector |
|
plDebugGeometry::Instance()->DrawLine(Position(), Position() + clippedForce, DEBUG_COLOR_GREEN); |
|
#endif |
|
|
|
// compute acceleration and velocity |
|
hsVector3 newAcceleration = (clippedForce / Mass()); |
|
hsVector3 newVelocity = Velocity(); |
|
|
|
// damp out abrupt changes and oscillations in steering acceleration |
|
// (rate is proportional to time step, then clipped into useful range) |
|
if (deltaTime > 0) |
|
{ |
|
const float smoothRate = Clip(9 * deltaTime, 0.15f, 0.4f); |
|
BlendIntoAccumulator(smoothRate, newAcceleration, fSmoothedAcceleration); |
|
} |
|
|
|
// Euler integrate (per frame) acceleration into velocity |
|
newVelocity += fSmoothedAcceleration * deltaTime; |
|
|
|
// enforce speed limit |
|
newVelocity = TruncateLength(newVelocity, MaxSpeed()); |
|
|
|
// update Speed |
|
SetSpeed(newVelocity.Magnitude()); |
|
|
|
// Euler integrate (per frame) velocity into position |
|
SetPosition(Position() + (newVelocity * deltaTime)); |
|
|
|
// regenerate local space (by default: align vehicle's forward axis with |
|
// new velocity, but this behavior may be overridden by derived classes.) |
|
RegenerateLocalSpace(newVelocity, deltaTime); |
|
|
|
// maintain path curvature information |
|
IMeasurePathCurvature(deltaTime); |
|
|
|
// running average of recent positions |
|
BlendIntoAccumulator(deltaTime * 0.06f, Position(), fSmoothedPosition); |
|
} |
|
|
|
hsVector3 pfVehicle::AdjustRawSteeringForce(const hsVector3 &force, const float deltaTime) |
|
{ |
|
const float maxAdjustedSpeed = 0.2f * MaxSpeed(); |
|
|
|
if ((Speed() > maxAdjustedSpeed) || (force == hsVector3(0,0,0))) |
|
return force; // no adjustment needed if they are going above 20% of max speed |
|
else |
|
{ |
|
const float range = Speed() / maxAdjustedSpeed; // make sure they don't turn too much if below 20% of max speed |
|
const float cosine = Interpolate(pow(range, 20), 1.0f, -1.0f); |
|
return LimitMaxDeviationAngle(force, cosine, Forward()); |
|
} |
|
} |
|
|
|
void pfVehicle::ApplyBrakingForce(const float rate, const float deltaTime) |
|
{ |
|
const float rawBraking = Speed() * rate; |
|
const float clipBraking = ((rawBraking < MaxForce()) ? rawBraking : MaxForce()); |
|
|
|
SetSpeed(Speed() - (clipBraking * deltaTime)); |
|
} |
|
|
|
hsPoint3 pfVehicle::PredictFuturePosition(const float predictionTime) |
|
{ |
|
return Position() + (Velocity() * predictionTime); |
|
} |
|
|
|
/////////////////////////////////////////////////////////////////////////////// |
|
// pfBoidGoal functions |
|
/////////////////////////////////////////////////////////////////////////////// |
|
|
|
pfBoidGoal::pfBoidGoal() |
|
{ |
|
fLastPos.Set(0, 0, 0); |
|
fCurPos.Set(0, 0, 0); |
|
fSpeed = 0; |
|
fHasLastPos = false; // our last pos doesn't make sense yet |
|
} |
|
|
|
void pfBoidGoal::Update(plSceneObject *goal, float deltaTime) |
|
{ |
|
if (!fHasLastPos) // the last pos is invalid, so we need to init now |
|
{ |
|
fLastPos = fCurPos = goal->GetLocalToWorld().GetTranslate(); |
|
fSpeed = 0; |
|
fForward.Set(1,0,0); // make a unit vector, it shouldn't matter that it's incorrect as this is only for one frame |
|
fHasLastPos = true; |
|
return; |
|
} |
|
|
|
fLastPos = fCurPos; |
|
fCurPos = goal->GetLocalToWorld().GetTranslate(); |
|
hsVector3 change(&fCurPos, &fLastPos); |
|
float unadjustedSpeed = change.Magnitude(); |
|
fSpeed = unadjustedSpeed / deltaTime; // update speed (mag is in meters, time is in seconds) |
|
if (unadjustedSpeed == 0) |
|
return; // if our speed is zero, don't recalc our forward vector (leave it as it was last time) |
|
fForward = change / unadjustedSpeed; |
|
|
|
#if FLOCKER_SHOW_DEBUG_LINES |
|
// Show where we are predicting the location to be in 1 second |
|
plDebugGeometry::Instance()->DrawLine(Position(), PredictFuturePosition(1), DEBUG_COLOR_BLUE); |
|
#endif |
|
} |
|
|
|
hsPoint3 pfBoidGoal::PredictFuturePosition(const float predictionTime) |
|
{ |
|
return fCurPos + (fForward * fSpeed * predictionTime); |
|
} |
|
|
|
/////////////////////////////////////////////////////////////////////////////// |
|
// pfBoid functions |
|
/////////////////////////////////////////////////////////////////////////////// |
|
|
|
pfBoid::pfBoid(pfProximityDatabase& pd, pfObjectFlocker *flocker, plKey &key) |
|
{ |
|
// allocate a token for this boid in the proximity database |
|
fProximityToken = NULL; |
|
ISetupToken(pd); |
|
|
|
Reset(); |
|
|
|
fFlockerPtr = flocker; |
|
fObjKey = key; |
|
|
|
IFlockDefaults(); |
|
|
|
fProximityToken->UpdateWithNewPosition(Position()); |
|
} |
|
|
|
pfBoid::pfBoid(pfProximityDatabase& pd, pfObjectFlocker *flocker, plKey &key, hsPoint3 &pos) |
|
{ |
|
// allocate a token for this boid in the proximity database |
|
fProximityToken = NULL; |
|
ISetupToken(pd); |
|
|
|
Reset(); |
|
|
|
fFlockerPtr = flocker; |
|
fObjKey = key; |
|
|
|
SetPosition(pos); |
|
|
|
IFlockDefaults(); |
|
|
|
fProximityToken->UpdateWithNewPosition(Position()); |
|
} |
|
|
|
pfBoid::pfBoid(pfProximityDatabase& pd, pfObjectFlocker *flocker, plKey &key, hsPoint3 &pos, float speed, hsVector3 &forward, hsVector3 &side, hsVector3 &up) |
|
{ |
|
// allocate a token for this boid in the proximity database |
|
fProximityToken = NULL; |
|
ISetupToken(pd); |
|
|
|
Reset(); |
|
|
|
fFlockerPtr = flocker; |
|
fObjKey = key; |
|
|
|
SetPosition(pos); |
|
SetSpeed(speed); |
|
SetForward(forward); |
|
SetSide(side); |
|
SetUp(up); |
|
|
|
IFlockDefaults(); |
|
|
|
fProximityToken->UpdateWithNewPosition(Position()); |
|
} |
|
|
|
pfBoid::~pfBoid() |
|
{ |
|
// delete this boid's token in the proximity database |
|
delete fProximityToken; |
|
|
|
plLoadCloneMsg* msg = TRACKED_NEW plLoadCloneMsg(fObjKey, fFlockerPtr->GetKey(), 0, false); |
|
msg->Send(); |
|
} |
|
|
|
void pfBoid::IFlockDefaults() |
|
{ |
|
fSeparationRadius = 5.0f; |
|
fSeparationAngle = -0.707f; |
|
fSeparationWeight = 12.0f; |
|
|
|
fCohesionRadius = 9.0f; |
|
fCohesionAngle = -0.15f; |
|
fCohesionWeight = 8.0f; |
|
|
|
fGoalWeight = 8.0f; |
|
fRandomWeight = 12.0f; |
|
} |
|
|
|
void pfBoid::ISetupToken(pfProximityDatabase &pd) |
|
{ |
|
// delete this boid's token in the old proximity database |
|
delete fProximityToken; |
|
|
|
// allocate a token for this boid in the proximity database |
|
fProximityToken = pd.MakeToken(this); |
|
} |
|
|
|
hsBool pfBoid::IInBoidNeighborhood(const pfVehicle &other, const float minDistance, const float maxDistance, const float cosMaxAngle) |
|
{ |
|
if (&other == this) // abort if we're looking at ourselves |
|
return false; |
|
else |
|
{ |
|
const hsVector3 offset(&(other.Position()), &Position()); |
|
const float distanceSquared = offset.MagnitudeSquared(); |
|
|
|
// definitely in neighborhood if inside minDistance sphere |
|
if (distanceSquared < (minDistance * minDistance)) |
|
return true; |
|
else |
|
{ |
|
// definitely not in neighborhood if outside maxDistance sphere |
|
if (distanceSquared > (maxDistance * maxDistance)) |
|
return false; |
|
else |
|
{ |
|
// otherwise, test angular offset from forward axis (can we "see" it?) |
|
const hsVector3 unitOffset = offset / sqrt(distanceSquared); |
|
const float forwardness = Forward() * unitOffset; |
|
return forwardness > cosMaxAngle; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Steering functions |
|
hsVector3 pfBoid::ISteerForWander(float timeDelta) |
|
{ |
|
// random walk the fWanderSide and fWanderUp variables between -1 and +1 |
|
const float speed = 10 * timeDelta; // the 10 value found through experimentation |
|
fWanderSide = ScalarRandomWalk(fWanderSide, speed, -1, +1); |
|
fWanderUp = ScalarRandomWalk(fWanderUp, speed, -1, +1); |
|
|
|
hsVector3 force = (Side() * fWanderSide) + (Up() * fWanderUp); |
|
|
|
#if FLOCKER_SHOW_DEBUG_LINES |
|
// Draw the random walk component |
|
plDebugGeometry::Instance()->DrawLine(Position(), Position()+force, DEBUG_COLOR_PINK); |
|
#endif |
|
|
|
return force; |
|
} |
|
|
|
hsVector3 pfBoid::ISteerForSeek(const hsPoint3 &target) |
|
{ |
|
#if FLOCKER_SHOW_DEBUG_LINES |
|
// Draw to where we are steering towards |
|
plDebugGeometry::Instance()->DrawLine(Position(), target, DEBUG_COLOR_RED); |
|
#endif |
|
|
|
const hsVector3 desiredVelocity(&target, &Position()); |
|
return desiredVelocity - Velocity(); |
|
} |
|
|
|
hsVector3 pfBoid::ISteerToGoal(pfBoidGoal &goal, float maxPredictionTime) |
|
{ |
|
// offset from this to quarry, that distance, unit vector toward quarry |
|
const hsVector3 offset(&goal.Position(), &Position()); |
|
const float distance = offset.Magnitude(); |
|
if (distance == 0) // nowhere to go |
|
return hsVector3(0, 0, 0); |
|
const hsVector3 unitOffset = offset / distance; |
|
|
|
// how parallel are the paths of "this" and the goal |
|
// (1 means parallel, 0 is pependicular, -1 is anti-parallel after later calculations) |
|
const float parallelness = Forward() * goal.Forward(); |
|
|
|
// how "forward" is the direction to the quarry |
|
// (1 means dead ahead, 0 is directly to the side, -1 is straight back after later calculations) |
|
const float forwardness = Forward() * unitOffset; |
|
|
|
float speed = Speed(); |
|
if (speed == 0) |
|
speed = 0.00001; // make it really small in case we start out not moving |
|
const float directTravelTime = distance / speed; |
|
const int f = IntervalComparison(forwardness, -0.707f, 0.707f); // -1 if below -0.707f, 0 if between, and +1 if above 0.707f) |
|
const int p = IntervalComparison(parallelness, -0.707f, 0.707f); // 0.707 is basically cos(45deg) (45deg = PI/4) |
|
|
|
float timeFactor = 0; // to be filled in below - how far ahead to predict position so it looks good |
|
|
|
// Break the pursuit into nine cases, the cross product of the |
|
// quarry being [ahead, aside, or behind] us and heading |
|
// [parallel, perpendicular, or anti-parallel] to us. |
|
switch (f) |
|
{ |
|
case +1: |
|
switch (p) |
|
{ |
|
case +1: // ahead, parallel |
|
timeFactor = 4; |
|
break; |
|
case 0: // ahead, perpendicular |
|
timeFactor = 1.8f; |
|
break; |
|
case -1: // ahead, anti-parallel |
|
timeFactor = 0.85f; |
|
break; |
|
} |
|
break; |
|
case 0: |
|
switch (p) |
|
{ |
|
case +1: // aside, parallel |
|
timeFactor = 1; |
|
break; |
|
case 0: // aside, perpendicular |
|
timeFactor = 0.8f; |
|
break; |
|
case -1: // aside, anti-parallel |
|
timeFactor = 4; |
|
break; |
|
} |
|
break; |
|
case -1: |
|
switch (p) |
|
{ |
|
case +1: // behind, parallel |
|
timeFactor = 0.5f; |
|
break; |
|
case 0: // behind, perpendicular |
|
timeFactor = 2; |
|
break; |
|
case -1: // behind, anti-parallel |
|
timeFactor = 2; |
|
break; |
|
} |
|
break; |
|
} |
|
|
|
// estimated time until intercept of quarry |
|
const float et = directTravelTime * timeFactor; |
|
const float etl = (et > maxPredictionTime) ? maxPredictionTime : et; |
|
|
|
// estimated position of quarry at intercept |
|
const hsPoint3 target = goal.PredictFuturePosition(etl); |
|
|
|
// steer directly for our target (which is a point ahead of the object we are pursuing) |
|
return ISteerForSeek(target); |
|
} |
|
|
|
hsVector3 pfBoid::ISteerForSeparation(const float maxDistance, const float cosMaxAngle, const std::vector<pfVehicle*> &flock) |
|
{ |
|
// steering accumulator and count of neighbors, both initially zero |
|
hsVector3 steering(0, 0, 0); |
|
int neighbors = 0; |
|
|
|
// for each of the other vehicles... |
|
for (std::vector<pfVehicle*>::const_iterator other = flock.begin(); other != flock.end(); other++) |
|
{ |
|
if (IInBoidNeighborhood(**other, Radius() * 3, maxDistance, cosMaxAngle)) |
|
{ |
|
// add in steering contribution |
|
// (opposite of the offset direction, divided once by distance |
|
// to normalize, divided another time to get 1/d falloff) |
|
const hsVector3 offset(&((**other).Position()), &Position()); |
|
const float distanceSquared = offset * offset; |
|
steering += (offset / -distanceSquared); |
|
|
|
// count neighbors |
|
neighbors++; |
|
} |
|
} |
|
|
|
// divide by neighbors, then normalize to pure direction |
|
if (neighbors > 0) |
|
{ |
|
steering = (steering / (float)neighbors); |
|
steering.Normalize(); |
|
} |
|
|
|
#if FLOCKER_SHOW_DEBUG_LINES |
|
// Draw the random walk component |
|
plDebugGeometry::Instance()->DrawLine(Position(), Position()+steering, DEBUG_COLOR_CYAN); |
|
#endif |
|
|
|
return steering; |
|
} |
|
|
|
hsVector3 pfBoid::ISteerForCohesion(const float maxDistance, const float cosMaxAngle, const std::vector<pfVehicle*> &flock) |
|
{ |
|
// steering accumulator and count of neighbors, both initially zero |
|
hsVector3 steering(0, 0, 0); |
|
int neighbors = 0; |
|
|
|
// for each of the other vehicles... |
|
for (std::vector<pfVehicle*>::const_iterator other = flock.begin(); other != flock.end(); other++) |
|
{ |
|
if (IInBoidNeighborhood(**other, Radius() * 3, maxDistance, cosMaxAngle)) |
|
{ |
|
// accumulate sum of neighbor's positions |
|
steering += (**other).Position(); |
|
|
|
// count neighbors |
|
neighbors++; |
|
} |
|
} |
|
|
|
// divide by neighbors, subtract off current position to get error- |
|
// correcting direction, then normalize to pure direction |
|
if (neighbors > 0) |
|
{ |
|
hsVector3 posVector(&(Position()), &(hsPoint3(0,0,0))); // quick hack to turn a point into a vector |
|
steering = ((steering / (float)neighbors) - posVector); |
|
steering.Normalize(); |
|
} |
|
|
|
#if FLOCKER_SHOW_DEBUG_LINES |
|
// Draw the random walk component |
|
plDebugGeometry::Instance()->DrawLine(Position(), Position()+steering, DEBUG_COLOR_YELLOW); |
|
#endif |
|
|
|
return steering; |
|
} |
|
|
|
// Used for frame-by-frame updates; no time deltas on positions. |
|
void pfBoid::Update(pfBoidGoal &goal, float deltaTime) |
|
{ |
|
const float maxTime = 20; // found by testing |
|
|
|
// find all flockmates within maxRadius using proximity database |
|
fNeighbors.clear(); |
|
fProximityToken->FindNeighbors(Position(), fSeparationRadius, fNeighbors); |
|
|
|
hsVector3 goalVector = ISteerToGoal(goal, maxTime); |
|
hsVector3 randomVector = ISteerForWander(deltaTime); |
|
hsVector3 separationVector = ISteerForSeparation(fSeparationRadius, fSeparationAngle, fNeighbors); |
|
hsVector3 cohesionVector = ISteerForCohesion(fCohesionRadius, fCohesionAngle, fNeighbors); |
|
|
|
hsVector3 steeringVector = (fGoalWeight * goalVector) + (fRandomWeight * randomVector) + |
|
(fSeparationWeight * separationVector) + (fCohesionWeight * cohesionVector); |
|
|
|
ApplySteeringForce(steeringVector, deltaTime); |
|
|
|
fProximityToken->UpdateWithNewPosition(Position()); |
|
} |
|
|
|
void pfBoid::RegenerateLocalSpace(const hsVector3 &newVelocity, const float elapsedTime) |
|
{ |
|
RegenerateLocalSpaceForBanking(newVelocity, elapsedTime); |
|
} |
|
|
|
/////////////////////////////////////////////////////////////////////////////// |
|
// pfFlock functions |
|
/////////////////////////////////////////////////////////////////////////////// |
|
pfFlock::pfFlock() : |
|
fGoalWeight(8.0f), |
|
fRandomWeight(12.0f), |
|
fSeparationRadius(5.0f), |
|
fSeparationWeight(12.0f), |
|
fCohesionRadius(9.0f), |
|
fCohesionWeight(8.0f), |
|
fMaxForce(10.0f), |
|
fMaxSpeed(5.0f), |
|
fMinSpeed(4.0f) |
|
{ |
|
fDatabase = TRACKED_NEW pfBasicProximityDatabase<pfVehicle*>(); |
|
} |
|
|
|
pfFlock::~pfFlock() |
|
{ |
|
int flock_size = fBoids.size(); |
|
for (int i = 0; i < flock_size; i++) |
|
{ |
|
delete fBoids[i]; |
|
fBoids[i] = nil; |
|
} |
|
fBoids.clear(); |
|
|
|
delete fDatabase; |
|
fDatabase = NULL; |
|
} |
|
|
|
void pfFlock::SetGoalWeight(float goalWeight) |
|
{ |
|
for (int i = 0; i < fBoids.size(); i++) |
|
fBoids[i]->SetGoalWeight(goalWeight); |
|
fGoalWeight = goalWeight; |
|
} |
|
|
|
void pfFlock::SetWanderWeight(float wanderWeight) |
|
{ |
|
for (int i = 0; i < fBoids.size(); i++) |
|
fBoids[i]->SetWanderWeight(wanderWeight); |
|
fRandomWeight = wanderWeight; |
|
} |
|
|
|
void pfFlock::SetSeparationWeight(float weight) |
|
{ |
|
for (int i = 0; i < fBoids.size(); i++) |
|
fBoids[i]->SetSeparationWeight(weight); |
|
fSeparationWeight = weight; |
|
} |
|
|
|
void pfFlock::SetSeparationRadius(float radius) |
|
{ |
|
for (int i = 0; i < fBoids.size(); i++) |
|
fBoids[i]->SetSeparationRadius(radius); |
|
fSeparationRadius = radius; |
|
} |
|
|
|
void pfFlock::SetCohesionWeight(float weight) |
|
{ |
|
for (int i = 0; i < fBoids.size(); i++) |
|
fBoids[i]->SetCohesionWeight(weight); |
|
fCohesionWeight = weight; |
|
} |
|
|
|
void pfFlock::SetCohesionRadius(float radius) |
|
{ |
|
for (int i = 0; i < fBoids.size(); i++) |
|
fBoids[i]->SetCohesionRadius(radius); |
|
fCohesionRadius = radius; |
|
} |
|
|
|
void pfFlock::SetMaxForce(float force) |
|
{ |
|
for (int i = 0; i < fBoids.size(); i++) |
|
fBoids[i]->SetMaxForce(force); |
|
fMaxForce = force; |
|
} |
|
|
|
void pfFlock::SetMaxSpeed(float speed) |
|
{ |
|
for (int i = 0; i < fBoids.size(); i++) |
|
{ |
|
float speedAdjust = (fMaxSpeed - fMinSpeed) * RAND(); |
|
fBoids[i]->SetMaxSpeed(speed - speedAdjust); |
|
} |
|
fMaxSpeed = speed; |
|
} |
|
|
|
void pfFlock::SetMinSpeed(float minSpeed) |
|
{ |
|
fMinSpeed = minSpeed; |
|
} |
|
|
|
void pfFlock::Update(plSceneObject *goal, float deltaTime) |
|
{ |
|
// update the goal data |
|
fBoidGoal.Update(goal, deltaTime); |
|
|
|
// update the flock |
|
float delta = (deltaTime > 0.3f) ? 0.3f : deltaTime; |
|
std::vector<pfBoid*>::iterator i; |
|
|
|
for (i = fBoids.begin(); i != fBoids.end(); i++) |
|
(*i)->Update(fBoidGoal, delta); |
|
} |
|
|
|
void pfFlock::AddBoid(pfObjectFlocker *flocker, plKey &key, hsPoint3 &pos) |
|
{ |
|
pfBoid *newBoid = TRACKED_NEW pfBoid(*fDatabase, flocker, key, pos); |
|
|
|
newBoid->SetGoalWeight(fGoalWeight); |
|
newBoid->SetWanderWeight(fRandomWeight); |
|
|
|
newBoid->SetSeparationWeight(fSeparationWeight); |
|
newBoid->SetSeparationRadius(fSeparationRadius); |
|
|
|
newBoid->SetCohesionWeight(fCohesionWeight); |
|
newBoid->SetCohesionRadius(fCohesionRadius); |
|
|
|
newBoid->SetMaxForce(fMaxForce); |
|
float speedAdjust = (fMaxSpeed - fMinSpeed) * RAND(); |
|
newBoid->SetMaxSpeed(fMaxSpeed - speedAdjust); |
|
|
|
fBoids.push_back(newBoid); |
|
} |
|
|
|
pfBoid *pfFlock::GetBoid(int i) |
|
{ |
|
if (i >= 0 && i < fBoids.size()) |
|
return fBoids[i]; |
|
else |
|
return nil; |
|
} |
|
|
|
pfObjectFlocker::pfObjectFlocker() : |
|
fUseTargetRotation(false), |
|
fRandomizeAnimationStart(true), |
|
fNumBoids(0) |
|
{ |
|
} |
|
|
|
pfObjectFlocker::~pfObjectFlocker() |
|
{ |
|
plgDispatch::Dispatch()->UnRegisterForExactType(plEvalMsg::Index(), GetKey()); |
|
} |
|
|
|
void pfObjectFlocker::SetNumBoids(UInt8 val) |
|
{ |
|
fNumBoids = val; |
|
} |
|
|
|
hsBool pfObjectFlocker::MsgReceive(plMessage* msg) |
|
{ |
|
plInitialAgeStateLoadedMsg* loadMsg = plInitialAgeStateLoadedMsg::ConvertNoRef(msg); |
|
if (loadMsg) |
|
{ |
|
plEnableMsg* pMsg = TRACKED_NEW plEnableMsg; |
|
pMsg->AddReceiver(fBoidKey); |
|
pMsg->SetCmd(plEnableMsg::kDrawable); |
|
pMsg->AddType(plEnableMsg::kDrawable); |
|
pMsg->SetBCastFlag(plMessage::kPropagateToModifiers | plMessage::kPropagateToChildren); |
|
pMsg->SetCmd(plEnableMsg::kDisable); |
|
pMsg->Send(); |
|
|
|
hsPoint3 pos(fTarget->GetLocalToWorld().GetTranslate()); |
|
for (int i = 0; i < fNumBoids; i++) |
|
{ |
|
plLoadCloneMsg* cloneMsg = TRACKED_NEW plLoadCloneMsg(fBoidKey->GetUoid(), GetKey(), 0); |
|
plKey newKey = cloneMsg->GetCloneKey(); |
|
cloneMsg->Send(); |
|
|
|
hsScalar xAdjust = (2 * RAND()) - 1; // produces a random number between -1 and 1 |
|
hsScalar yAdjust = (2 * RAND()) - 1; |
|
hsScalar zAdjust = (2 * RAND()) - 1; |
|
hsPoint3 boidPos(pos.fX + xAdjust, pos.fY + yAdjust, pos.fZ + zAdjust); |
|
fFlock.AddBoid(this, newKey, boidPos); |
|
} |
|
plgDispatch::Dispatch()->UnRegisterForExactType(plInitialAgeStateLoadedMsg::Index(), GetKey()); |
|
plgDispatch::Dispatch()->RegisterForExactType(plEvalMsg::Index(), GetKey()); |
|
|
|
return true; |
|
} |
|
|
|
plLoadCloneMsg* lcMsg = plLoadCloneMsg::ConvertNoRef(msg); |
|
if (lcMsg && lcMsg->GetIsLoading()) |
|
{ |
|
if (fRandomizeAnimationStart) |
|
{ |
|
plAnimCmdMsg* pMsg = TRACKED_NEW plAnimCmdMsg; |
|
pMsg->SetSender(GetKey()); |
|
pMsg->SetBCastFlag(plMessage::kPropagateToModifiers | plMessage::kPropagateToChildren); |
|
pMsg->AddReceiver( lcMsg->GetCloneKey() ); |
|
pMsg->SetCmd(plAnimCmdMsg::kGoToPercent); |
|
pMsg->fTime = RAND(); |
|
pMsg->Send(); |
|
} |
|
} |
|
|
|
return plSingleModifier::MsgReceive(msg); |
|
} |
|
|
|
hsBool pfObjectFlocker::IEval(double secs, hsScalar del, UInt32 dirty) |
|
{ |
|
fFlock.Update(fTarget, del); |
|
|
|
plSceneObject* boidSO = nil; |
|
for (int i = 0; i < fNumBoids; i++) |
|
{ |
|
pfBoid* boid = fFlock.GetBoid(i); |
|
boidSO = plSceneObject::ConvertNoRef(boid->GetKey()->VerifyLoaded()); |
|
|
|
hsMatrix44 l2w; |
|
hsMatrix44 w2l; |
|
|
|
if (fUseTargetRotation) |
|
l2w = fTarget->GetLocalToWorld(); |
|
else |
|
{ |
|
l2w = boidSO->GetLocalToWorld(); |
|
hsVector3 forward = boid->Forward(); |
|
hsVector3 up = boid->Up(); |
|
hsVector3 side = boid->Side(); |
|
|
|
// copy the vectors over |
|
for(int i = 0; i < 3; i++) |
|
{ |
|
l2w.fMap[i][0] = side[i]; |
|
l2w.fMap[i][1] = forward[i]; |
|
l2w.fMap[i][2] = up[i]; |
|
} |
|
} |
|
|
|
hsScalarTriple pos = boid->Position(); |
|
l2w.SetTranslate(&pos); |
|
l2w.GetInverse(&w2l); |
|
|
|
boidSO->SetTransform(l2w, w2l); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
|
|
void pfObjectFlocker::SetTarget(plSceneObject* so) |
|
{ |
|
plSingleModifier::SetTarget(so); |
|
|
|
if( fTarget ) |
|
{ |
|
plgDispatch::Dispatch()->RegisterForExactType(plInitialAgeStateLoadedMsg::Index(), GetKey()); |
|
} |
|
} |
|
|
|
void pfObjectFlocker::Read(hsStream* s, hsResMgr* mgr) |
|
{ |
|
plSingleModifier::Read(s, mgr); |
|
|
|
int version = s->ReadByte(); |
|
hsAssert(version <= fFileVersion, "Flocker data is newer then client, please update your client"); |
|
|
|
SetNumBoids(s->ReadByte()); |
|
fBoidKey = mgr->ReadKey(s); |
|
|
|
fFlock.SetGoalWeight(s->ReadSwapScalar()); |
|
fFlock.SetWanderWeight(s->ReadSwapScalar()); |
|
|
|
fFlock.SetSeparationWeight(s->ReadSwapScalar()); |
|
fFlock.SetSeparationRadius(s->ReadSwapScalar()); |
|
|
|
fFlock.SetCohesionWeight(s->ReadSwapScalar()); |
|
fFlock.SetCohesionRadius(s->ReadSwapScalar()); |
|
|
|
fFlock.SetMaxForce(s->ReadSwapScalar()); |
|
fFlock.SetMaxSpeed(s->ReadSwapScalar()); |
|
fFlock.SetMinSpeed(s->ReadSwapScalar()); |
|
|
|
fUseTargetRotation = s->ReadBool(); |
|
fRandomizeAnimationStart = s->ReadBool(); |
|
} |
|
|
|
void pfObjectFlocker::Write(hsStream* s, hsResMgr* mgr) |
|
{ |
|
plSingleModifier::Write(s, mgr); |
|
|
|
s->WriteByte(fFileVersion); |
|
|
|
s->WriteByte(fNumBoids); |
|
mgr->WriteKey(s, fBoidKey); |
|
|
|
s->WriteSwapScalar(fFlock.GoalWeight()); |
|
s->WriteSwapScalar(fFlock.WanderWeight()); |
|
|
|
s->WriteSwapScalar(fFlock.SeparationWeight()); |
|
s->WriteSwapScalar(fFlock.SeparationRadius()); |
|
|
|
s->WriteSwapScalar(fFlock.CohesionWeight()); |
|
s->WriteSwapScalar(fFlock.CohesionRadius()); |
|
|
|
s->WriteSwapScalar(fFlock.MaxForce()); |
|
s->WriteSwapScalar(fFlock.MaxSpeed()); |
|
s->WriteSwapScalar(fFlock.MinSpeed()); |
|
|
|
s->WriteBool(fUseTargetRotation); |
|
s->WriteBool(fRandomizeAnimationStart); |
|
}
|
|
|