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.
1061 lines
31 KiB
1061 lines
31 KiB
14 years ago
|
/*==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/>.
|
||
|
|
||
|
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);
|
||
|
}
|