Another player's pod is moved between updates by dead reckoning, which
advances it toward the projected position by a fraction of the remaining
gap each step:
percent = time_slice / ((nextUpdate - lastPerformance) + time_slice)
That fraction is decided by nextUpdate, so predicting when the next
packet lands is not cosmetic - it sets how far every step moves. The
prediction was labelled HACK in the original source and deserved it: the
next gap was assumed to equal the previous gap. On a LAN the gaps were
all alike and it held. Over Steam a single late packet doubles the
expected gap, percent collapses, the pod barely advances for one step and
then catches up on the next - a visible tick.
Measured on a live connection, in the replicant's own step grid: 7 spikes
in 5 seconds, about 1.4 a second, matching the reported symptom, with
percent bottoming out at 0.014 against a normal range of 0.27 to 0.95.
Predict from the median of the last eight gaps instead. A median has a
breakdown point of half its samples, so one straggler - or three - moves
it not at all, while a genuine change in the send rate still carries it
within a few updates. Gaps that are non-positive (duplicate or reordered)
or multi-second (a join or a stall) never enter the window, and the
window is reset outright when the stream is interrupted.
Against the measured jitter pattern the blend fraction holds 0.282..0.286
where it previously swung 0.095..0.294 - a spread fifty times smaller.
This changes how remote pods MOVE, not merely how they are drawn, so it
feeds collisions with them as well. RP412NETPREDICT=0 restores the old
prediction for comparison on the same build, and the environ.ini entry
says to keep the setting alike on every machine in a race.
The RP412CAMLOG trace now also reports the interval being predicted and
the worst one-step-ahead miss, scored per entity rather than through the
file-scope statics the percent readings use - those are written by
whichever mover ran last, which is exactly the crossed frame of reference
this whole investigation kept tripping over.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2414 lines
60 KiB
C++
2414 lines
60 KiB
C++
#include "munga.h"
|
|
#pragma hdrstop
|
|
|
|
#include "mover.h"
|
|
#include "player.h"
|
|
#include "boxsolid.h"
|
|
#include "interest.h"
|
|
#include "collasst.h"
|
|
#include "doorfram.h"
|
|
#include "door.h"
|
|
#include "line.h"
|
|
#include "app.h"
|
|
#include "notation.h"
|
|
|
|
//
|
|
// The blend fraction the last dead-reckoned step used, and whether it
|
|
// blended at all rather than snapping. Only read by the RP412CAMLOG trace
|
|
// in Mover::DeadReckon, which needs them from the branch that computes
|
|
// them a few lines earlier.
|
|
//
|
|
static Logical gLastLerpUsed = False;
|
|
static Scalar gLastPercent = 0.0f;
|
|
|
|
//
|
|
// Bounds on the replication interval estimate, in seconds.
|
|
//
|
|
// The first pair decide what is allowed into the sample window at all: a
|
|
// non-positive gap is a duplicate or a reordered packet, and a multi-second
|
|
// one is a join, a pause or a stall. Neither says anything about the rate
|
|
// the sender is actually keeping.
|
|
//
|
|
// The second pair are a backstop on the answer, set deliberately wide so
|
|
// that in every sane case the median decides it and these never bind.
|
|
//
|
|
static const Scalar kMinimumUpdateInterval = 0.001f;
|
|
static const Scalar kOutlierUpdateInterval = 2.0f;
|
|
static const Scalar kMinimumPredictedInterval = 0.010f;
|
|
static const Scalar kMaximumPredictedInterval = 1.0f;
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// RP412NETPREDICT=0 restores the original single-sample prediction, so the
|
|
// two can be compared on the same build and the same connection.
|
|
//
|
|
static Logical
|
|
UseMedianPrediction()
|
|
{
|
|
static int cached = -1;
|
|
|
|
if (cached < 0)
|
|
{
|
|
const char *setting = getenv("RP412NETPREDICT");
|
|
cached = (setting && *setting == '0') ? 0 : 1;
|
|
}
|
|
return cached ? True : False;
|
|
}
|
|
|
|
//#############################################################################
|
|
//############################### Mover #################################
|
|
//#############################################################################
|
|
|
|
//#############################################################################
|
|
// Shared Data Support
|
|
//
|
|
Derivation* Mover::GetClassDerivations()
|
|
{
|
|
static Derivation classDerivations(Entity::GetClassDerivations(), "Mover");
|
|
return &classDerivations;
|
|
}
|
|
|
|
Mover::SharedData
|
|
Mover::DefaultData(
|
|
Mover::GetClassDerivations(),
|
|
Mover::GetMessageHandlers(),
|
|
Mover::GetAttributeIndex(),
|
|
Mover::StateCount,
|
|
(Entity::MakeHandler)Mover::Make
|
|
);
|
|
|
|
//#############################################################################
|
|
// Message Support
|
|
//
|
|
#if 0
|
|
const Receiver::HandlerEntry
|
|
Mover::MessageHandlerEntries[]=
|
|
{
|
|
MESSAGE_ENTRY(Mover, Update)
|
|
};
|
|
|
|
Entity::MessageHandlerSet
|
|
Mover::MessageHandlers(
|
|
ELEMENTS(Mover::MessageHandlerEntries),
|
|
Mover::MessageHandlerEntries,
|
|
Entity::GetMessageHandlers()
|
|
);
|
|
#endif
|
|
|
|
//#############################################################################
|
|
// Attribute Support
|
|
//
|
|
const Mover::IndexEntry
|
|
Mover::AttributePointers[]=
|
|
{
|
|
ATTRIBUTE_ENTRY(Mover, LocalVelocity, localVelocity),
|
|
ATTRIBUTE_ENTRY(Mover, LocalAcceleration, localAcceleration),
|
|
ATTRIBUTE_ENTRY(Mover, WorldLinearVelocity, worldLinearVelocity),
|
|
ATTRIBUTE_ENTRY(Mover, WorldLinearAcceleration, worldLinearAcceleration),
|
|
ATTRIBUTE_ENTRY(Mover, MoverMass, moverMass),
|
|
ATTRIBUTE_ENTRY(Mover, MomentOfInertia, momentOfInertia),
|
|
ATTRIBUTE_ENTRY(
|
|
Mover,
|
|
PositiveLinearDragCoefficients,
|
|
positiveLinearDragCoefficients
|
|
),
|
|
ATTRIBUTE_ENTRY(
|
|
Mover,
|
|
NegativeLinearDragCoefficients,
|
|
negativeLinearDragCoefficients
|
|
),
|
|
ATTRIBUTE_ENTRY(Mover, AngularDragCoefficients, angularDragCoefficients),
|
|
ATTRIBUTE_ENTRY(Mover, FrictionCoefficient, frictionCoefficient),
|
|
ATTRIBUTE_ENTRY(Mover, ElasticityCoefficient, elasticityCoefficient),
|
|
ATTRIBUTE_ENTRY(Mover, MinimumBounceSpeed, minimumBounceSpeed)
|
|
};
|
|
|
|
Mover::AttributeIndexSet& Mover::GetAttributeIndex()
|
|
{
|
|
static Mover::AttributeIndexSet attributeIndex(ELEMENTS(Mover::AttributePointers),
|
|
Mover::AttributePointers,
|
|
Entity::GetAttributeIndex()
|
|
);
|
|
return attributeIndex;
|
|
}
|
|
|
|
//#############################################################################
|
|
// Model Support
|
|
//
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::UpdateWorldMotion()
|
|
{
|
|
Check(this);
|
|
|
|
//
|
|
//---------------------------------------------------
|
|
// Move the accelerations back into world coordinates
|
|
//---------------------------------------------------
|
|
//
|
|
worldLinearAcceleration.Multiply(
|
|
localAcceleration.linearMotion,
|
|
localToWorld
|
|
);
|
|
worldLinearVelocity.Multiply(
|
|
localVelocity.linearMotion,
|
|
localToWorld
|
|
);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::UpdateLocalMotion()
|
|
{
|
|
Check(this);
|
|
localVelocity.linearMotion.MultiplyByInverse(
|
|
worldLinearVelocity,
|
|
localToWorld
|
|
);
|
|
localAcceleration.linearMotion.MultiplyByInverse(
|
|
worldLinearAcceleration,
|
|
localToWorld
|
|
);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::ApplyWorldAccelerations(Scalar time_slice)
|
|
{
|
|
Check(this);
|
|
Verify(time_slice > 0.0f);
|
|
|
|
//
|
|
//--------------------------------------------------
|
|
// Calculate the new position as p += v*t + a*.5*t*t
|
|
//--------------------------------------------------
|
|
//
|
|
Scalar
|
|
half_t_squared = 0.5f * time_slice * time_slice;
|
|
Vector3D
|
|
position_delta;
|
|
position_delta.Multiply(worldLinearAcceleration, half_t_squared);
|
|
Check_Fpu();
|
|
|
|
position_delta.AddScaled(
|
|
position_delta,
|
|
worldLinearVelocity,
|
|
time_slice
|
|
);
|
|
Check_Fpu();
|
|
|
|
localOrigin.linearPosition.Add(localOrigin.linearPosition, position_delta);
|
|
Check_Fpu();
|
|
|
|
position_delta.Multiply(localAcceleration.angularMotion, half_t_squared);
|
|
Check_Fpu();
|
|
|
|
position_delta.AddScaled(
|
|
position_delta,
|
|
localVelocity.angularMotion,
|
|
time_slice
|
|
);
|
|
Check_Fpu();
|
|
|
|
Quaternion
|
|
old_position = localOrigin.angularPosition;
|
|
localOrigin.angularPosition.Add(old_position, position_delta);
|
|
Check_Fpu();
|
|
//
|
|
//-----------------------------------
|
|
// Calculate our velocity as v += a*t
|
|
//-----------------------------------
|
|
//
|
|
worldLinearVelocity.AddScaled(
|
|
worldLinearVelocity,
|
|
worldLinearAcceleration,
|
|
time_slice
|
|
);
|
|
Check_Fpu();
|
|
localVelocity.angularMotion.AddScaled(
|
|
localVelocity.angularMotion,
|
|
localAcceleration.angularMotion,
|
|
time_slice
|
|
);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::CalculateDrag(
|
|
Vector3D *drag,
|
|
const Vector3D &velocity,
|
|
const Vector3D &positive_CODs,
|
|
const Vector3D &negative_CODs,
|
|
Scalar power
|
|
)
|
|
{
|
|
Environment *air = GetEnvironment();
|
|
Check(air);
|
|
Vector3D temp,temp2;
|
|
temp.MultiplyByInverse(air->GetWindVelocity(), localToWorld);
|
|
temp += velocity;
|
|
|
|
if (temp.x < 0.0f)
|
|
{
|
|
drag->x = negative_CODs.x;
|
|
temp2.x = Power(-temp.x, power);
|
|
Check_Fpu();
|
|
}
|
|
else
|
|
{
|
|
drag->x = -positive_CODs.x;
|
|
temp2.x = Power(temp.x, power);
|
|
Check_Fpu();
|
|
}
|
|
|
|
if (temp.y < 0.0f)
|
|
{
|
|
drag->y = negative_CODs.y;
|
|
temp2.y = Power(-temp.y, power);
|
|
Check_Fpu();
|
|
}
|
|
else
|
|
{
|
|
drag->y = -positive_CODs.y;
|
|
temp2.y = Power(temp.y, power);
|
|
Check_Fpu();
|
|
}
|
|
|
|
if (temp.z < 0.0f)
|
|
{
|
|
drag->z = negative_CODs.z;
|
|
temp2.z = Power(-temp.z, power);
|
|
Check_Fpu();
|
|
}
|
|
else
|
|
{
|
|
drag->z = -positive_CODs.z;
|
|
temp2.z = Power(temp.z, power);
|
|
Check_Fpu();
|
|
}
|
|
|
|
*drag *= air->airDensity;
|
|
drag->Multiply(*drag, temp2);
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::ApplyAirResistanceAndGravity(Scalar power)
|
|
{
|
|
Check(this);
|
|
|
|
//
|
|
//-------------------------------------------------------------------------
|
|
// Apply drag to the system, allowing for different drag numbers based upon
|
|
// the direction of motion along the axis
|
|
//-------------------------------------------------------------------------
|
|
//
|
|
Vector3D acceleration;
|
|
CalculateDrag(
|
|
&acceleration,
|
|
localVelocity.linearMotion,
|
|
positiveLinearDragCoefficients,
|
|
negativeLinearDragCoefficients,
|
|
power
|
|
);
|
|
localAcceleration.linearMotion += acceleration;
|
|
|
|
acceleration.Multiply(angularDragCoefficients, localVelocity.angularMotion);
|
|
localAcceleration.angularMotion -= acceleration;
|
|
|
|
//
|
|
//---------------------------
|
|
// Apply gravity to the craft
|
|
//---------------------------
|
|
//
|
|
UpdateWorldMotion();
|
|
worldLinearAcceleration.y -= GetEnvironment()->gravityConstant;
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::ApplyLocalForce(
|
|
const Vector3D &force,
|
|
const Vector3D &moment
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(&force);
|
|
Check(&moment);
|
|
|
|
Vector3D acceleration;
|
|
Verify(!Small_Enough(moverMass));
|
|
acceleration.Divide(force, moverMass);
|
|
ApplyLocalAcceleration(acceleration, moment);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::ApplyLocalAcceleration(
|
|
const Vector3D &acceleration,
|
|
const Vector3D &moment
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(&acceleration);
|
|
Check(&moment);
|
|
|
|
localAcceleration.linearMotion += acceleration;
|
|
Vector3D torque;
|
|
torque.Cross(moment, acceleration);
|
|
torque *= momentOfInertia;
|
|
localAcceleration.angularMotion += torque;
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Logical
|
|
Mover::NoDeadReckoner()
|
|
{
|
|
Check(this);
|
|
|
|
//
|
|
//-------------------------------------------------------------------------
|
|
// If we are the replicant instance and we are not yet past the anticipated
|
|
// time for the next event, project out to the next event
|
|
//-------------------------------------------------------------------------
|
|
//
|
|
projectedOrigin = updateOrigin;
|
|
Check_Fpu();
|
|
return False;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Logical
|
|
Mover::LinearDeadReckoner()
|
|
{
|
|
Check(this);
|
|
Logical lerp_mode;
|
|
Scalar time_slice;
|
|
|
|
//
|
|
//-------------------------------------------------------------------------
|
|
// If we are the replicant instance and we are not yet past the anticipated
|
|
// time for the next event, project out to the next event
|
|
//-------------------------------------------------------------------------
|
|
//
|
|
if (GetInstance() == ReplicantInstance && lastPerformance < nextUpdate)
|
|
{
|
|
time_slice = nextUpdate - lastUpdate;
|
|
lerp_mode = True;
|
|
}
|
|
else
|
|
{
|
|
time_slice = lastPerformance - lastUpdate;
|
|
lerp_mode = False;
|
|
}
|
|
|
|
//
|
|
//---------------------------------------
|
|
// Calculate the new position as p += v*t
|
|
//---------------------------------------
|
|
//
|
|
Vector3D position_delta;
|
|
position_delta.Multiply(updateVelocity.linearMotion, time_slice);
|
|
projectedOrigin.linearPosition.Add(
|
|
updateOrigin.linearPosition,
|
|
position_delta
|
|
);
|
|
|
|
//
|
|
//-------------------------------
|
|
// Handle projecting the rotation
|
|
//-------------------------------
|
|
//
|
|
position_delta.Multiply(updateVelocity.angularMotion, time_slice);
|
|
projectedOrigin.angularPosition.Add(
|
|
updateOrigin.angularPosition,
|
|
position_delta
|
|
);
|
|
projectedVelocity = updateVelocity;
|
|
|
|
Check_Fpu();
|
|
return lerp_mode;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Logical
|
|
Mover::AcceleratedDeadReckoner()
|
|
{
|
|
Check(this);
|
|
Logical lerp_mode;
|
|
Scalar time_slice;
|
|
|
|
//
|
|
//-------------------------------------------------------------------------
|
|
// If we are the replicant instance and we are not yet past the anticipated
|
|
// time for the next event, project out to the next event
|
|
//-------------------------------------------------------------------------
|
|
//
|
|
if (GetInstance() == ReplicantInstance && lastPerformance < nextUpdate)
|
|
{
|
|
time_slice = nextUpdate - lastUpdate;
|
|
lerp_mode = True;
|
|
}
|
|
else
|
|
{
|
|
time_slice = lastPerformance - lastUpdate;
|
|
lerp_mode = False;
|
|
}
|
|
|
|
//
|
|
//--------------------------------------------------
|
|
// Calculate the new position as p += v*t + a*.5*t*t
|
|
//--------------------------------------------------
|
|
//
|
|
Scalar half_t_squared = 0.5f * time_slice * time_slice;
|
|
Vector3D position_delta;
|
|
position_delta.Multiply(updateAcceleration.linearMotion, half_t_squared);
|
|
position_delta.AddScaled(
|
|
position_delta,
|
|
updateVelocity.linearMotion,
|
|
time_slice
|
|
);
|
|
projectedOrigin.linearPosition.Add(
|
|
updateOrigin.linearPosition,
|
|
position_delta
|
|
);
|
|
|
|
//
|
|
//-------------------------------
|
|
// Handle projecting the rotation
|
|
//-------------------------------
|
|
//
|
|
position_delta.Multiply(updateAcceleration.angularMotion, half_t_squared);
|
|
position_delta.AddScaled(
|
|
position_delta,
|
|
updateVelocity.angularMotion,
|
|
time_slice
|
|
);
|
|
projectedOrigin.angularPosition.Add(
|
|
updateOrigin.angularPosition,
|
|
position_delta
|
|
);
|
|
|
|
//
|
|
//-----------------------------------
|
|
// Calculate our velocity as v += a*t
|
|
//-----------------------------------
|
|
//
|
|
if (GetInstance() == ReplicantInstance)
|
|
{
|
|
projectedVelocity.linearMotion.AddScaled(
|
|
updateVelocity.linearMotion,
|
|
worldLinearAcceleration,
|
|
time_slice
|
|
);
|
|
projectedVelocity.angularMotion.AddScaled(
|
|
updateVelocity.angularMotion,
|
|
localAcceleration.angularMotion,
|
|
time_slice
|
|
);
|
|
}
|
|
Check_Fpu();
|
|
return lerp_mode;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::DeadReckon(Scalar time_slice)
|
|
{
|
|
Check(this);
|
|
|
|
//
|
|
//------------------------------
|
|
// Run the chosen dead reckoning
|
|
//------------------------------
|
|
//
|
|
Verify(GetInstance() == ReplicantInstance);
|
|
if (deadReckoner)
|
|
{
|
|
//
|
|
//---------------------------------------------------------------------
|
|
// Merge the projected origin with the current origin if we are in lerp
|
|
// mode. If not, just copy the projected origin into the local origin
|
|
//---------------------------------------------------------------------
|
|
//
|
|
if ((this->*deadReckoner)())
|
|
{
|
|
Scalar percent =
|
|
time_slice / ((nextUpdate - lastPerformance) + time_slice);
|
|
|
|
// for the RP412CAMLOG trace at the end of this function
|
|
gLastLerpUsed = True;
|
|
gLastPercent = percent;
|
|
|
|
//
|
|
//------------------------------------------
|
|
// Do a spherical lerp on the angular motion
|
|
//------------------------------------------
|
|
//
|
|
localOrigin.angularPosition.Lerp(
|
|
localOrigin.angularPosition,
|
|
projectedOrigin.angularPosition,
|
|
percent
|
|
);
|
|
localVelocity.angularMotion.Lerp(
|
|
localVelocity.angularMotion,
|
|
projectedVelocity.angularMotion,
|
|
percent
|
|
);
|
|
|
|
//
|
|
//-------------------------
|
|
// Spline the linear motion
|
|
//-------------------------
|
|
//
|
|
#if 0
|
|
CubicCurve
|
|
spline(
|
|
localOrigin.linearPosition,
|
|
worldLinearVelocity,
|
|
projectedOrigin.linearPosition,
|
|
projectedVelocity.linearMotion
|
|
);
|
|
spline.Evaluate(
|
|
percent,
|
|
&localOrigin.linearPosition,
|
|
&worldLinearVelocity
|
|
);
|
|
#else
|
|
localOrigin.linearPosition.Lerp(
|
|
localOrigin.linearPosition,
|
|
projectedOrigin.linearPosition,
|
|
percent
|
|
);
|
|
worldLinearVelocity.Lerp(
|
|
worldLinearVelocity,
|
|
projectedVelocity.linearMotion,
|
|
percent
|
|
);
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
gLastLerpUsed = False; // snapped, not blended
|
|
localOrigin = projectedOrigin;
|
|
worldLinearVelocity = projectedVelocity.linearMotion;
|
|
localVelocity.angularMotion = projectedVelocity.angularMotion;
|
|
}
|
|
|
|
//
|
|
//----------------------------------------------------
|
|
// Update the collision volume and the local variables
|
|
//----------------------------------------------------
|
|
//
|
|
if (IsCollisionVolume())
|
|
{
|
|
MoveCollisionVolume();
|
|
}
|
|
else
|
|
{
|
|
localToWorld = localOrigin;
|
|
}
|
|
UpdateLocalMotion();
|
|
|
|
//
|
|
// RP412CAMLOG: is a replicant's motion actually uniform?
|
|
//
|
|
// Measured HERE, in the replicant's own step, and nowhere else.
|
|
// Every previous attempt at this question sampled from another
|
|
// clock - the camera's step grid, or an arriving packet's
|
|
// timestamp - and two independent clocks alias against each other
|
|
// whatever the game is doing, so those numbers could never
|
|
// separate a real hitch from the measurement's own beat. This one
|
|
// has a single frame of reference: consecutive steps of the entity
|
|
// being asked about.
|
|
//
|
|
// percent is the whole mechanism above: it is how far this step
|
|
// moves toward the projected position, and it depends on
|
|
// nextUpdate being a decent guess at when the next packet lands.
|
|
// If that guess is poor the fraction swings, and swinging fraction
|
|
// is uneven motion no matter how clean the packets were.
|
|
//
|
|
// One entity only - the first replicant seen - because these
|
|
// counters are shared and a grid full of pods would blend into
|
|
// noise.
|
|
//
|
|
if (RPCameraLog())
|
|
{
|
|
static EntityID watched = EntityID::Null;
|
|
static Logical latched = False;
|
|
|
|
if (!latched)
|
|
{
|
|
latched = True;
|
|
watched = GetEntityID();
|
|
}
|
|
if (watched == GetEntityID())
|
|
{
|
|
static Scalar next_say = 0.0f;
|
|
static Point3D last_pos(0.0f, 0.0f, 0.0f);
|
|
static Logical have_last = False;
|
|
static int steps = 0;
|
|
static int spikes = 0;
|
|
static int stalls = 0;
|
|
static int lerped = 0;
|
|
static Scalar mean_step = 0.0f;
|
|
static Scalar min_percent = 1.0f;
|
|
static Scalar max_percent = 0.0f;
|
|
static Scalar worst_error = 0.0f;
|
|
|
|
++steps;
|
|
if (have_last)
|
|
{
|
|
Vector3D moved;
|
|
moved.Subtract(localOrigin.linearPosition, last_pos);
|
|
Scalar distance = moved.Length();
|
|
|
|
if (distance > 50.0f)
|
|
{
|
|
mean_step = 0.0f; // respawn, not motion
|
|
}
|
|
else if (mean_step > 0.01f)
|
|
{
|
|
Scalar ratio = distance / mean_step;
|
|
if (ratio > 2.5f) { ++spikes; }
|
|
else if (ratio < 0.4f) { ++stalls; }
|
|
mean_step = mean_step * 0.9f + distance * 0.1f;
|
|
}
|
|
else
|
|
{
|
|
mean_step = distance;
|
|
}
|
|
}
|
|
last_pos = localOrigin.linearPosition;
|
|
have_last = True;
|
|
|
|
if ((Scalar) Now() >= next_say)
|
|
{
|
|
if (next_say > 0.0f)
|
|
{
|
|
DEBUG_STREAM << "CamLog: replicant motion - " << steps
|
|
<< " own steps, " << spikes << " spike(s), "
|
|
<< stalls << " stall(s), " << lerped
|
|
<< " lerped, percent " << min_percent << ".."
|
|
<< max_percent << ", mean step " << mean_step
|
|
<< "m, predicting " << predictedInterval
|
|
<< "s worst miss " << worst_error << "s\n"
|
|
<< std::flush;
|
|
}
|
|
next_say = ((Scalar) Now()) + 5.0f;
|
|
steps = 0;
|
|
spikes = 0;
|
|
stalls = 0;
|
|
lerped = 0;
|
|
min_percent = 1.0f;
|
|
max_percent = 0.0f;
|
|
worst_error = 0.0f;
|
|
}
|
|
{
|
|
Scalar missed =
|
|
(predictionError < 0.0f) ? -predictionError : predictionError;
|
|
|
|
if (missed > worst_error) { worst_error = missed; }
|
|
}
|
|
if (gLastLerpUsed)
|
|
{
|
|
++lerped;
|
|
if (gLastPercent < min_percent) { min_percent = gLastPercent; }
|
|
if (gLastPercent > max_percent) { max_percent = gLastPercent; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::PerformAndWatch(
|
|
const Time &till,
|
|
MemoryStream *update_stream
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(&till);
|
|
|
|
int i;
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// Make sure that the time into the simulations is stable. If a half-second
|
|
// delay occurs, or we are in stasis, just bring everything up to date
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
Scalar time_slice = till - lastPerformance;
|
|
if (time_slice < SMALL)
|
|
{
|
|
Tell("No time!\n");
|
|
Bye_Bye:
|
|
WriteSimulationUpdate(update_stream);
|
|
return;
|
|
}
|
|
|
|
if (GetSimulationState() == StasisState || time_slice > 0.5f)
|
|
{
|
|
lastPerformance = till;
|
|
if (GetSimulationState() == StasisState)
|
|
{
|
|
lastUpdate = till;
|
|
}
|
|
if (subsystemArray)
|
|
{
|
|
Check_Pointer(subsystemArray);
|
|
for (i=0; i<subsystemCount; ++i)
|
|
{
|
|
if (subsystemArray[i])
|
|
{
|
|
Check(subsystemArray[i]);
|
|
subsystemArray[i]->SetLastPerformance(till);
|
|
}
|
|
}
|
|
}
|
|
//SetSimulationState(DefaultState);
|
|
goto Bye_Bye;
|
|
}
|
|
|
|
//
|
|
//------------------------------------
|
|
// Set up for local motion calculation
|
|
//------------------------------------
|
|
//
|
|
localVelocity.linearMotion.MultiplyByInverse(
|
|
worldLinearVelocity,
|
|
localToWorld
|
|
);
|
|
localAcceleration = Motion::Identity;
|
|
previousOrigin = localOrigin;
|
|
|
|
//
|
|
//-----------------------
|
|
// Process the subsystems
|
|
//-----------------------
|
|
//
|
|
Entity::PerformAndWatch(till, update_stream);
|
|
|
|
//
|
|
//-----------------------------------------------
|
|
// Make sure the position quaternion stays stable
|
|
//
|
|
// Frame-counting, so it only runs on the frame-coupled path - fixed
|
|
// steps do the same thing in BeginStep, counted in STEPS, because
|
|
// "every 20 frames" lands at a different point of the step sequence
|
|
// on every machine and rounding at different points is drift.
|
|
//-----------------------------------------------
|
|
//
|
|
if (Simulation::FixedStep() <= (Scalar) 0 && ++normalizeCount >= 20)
|
|
{
|
|
localOrigin.angularPosition.Normalize();
|
|
normalizeCount = 0;
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// The per-STEP set-up. This is the same work Mover::PerformAndWatch does
|
|
// once per frame above - and once per frame is exactly wrong under fixed
|
|
// stepping: the thrusters ADD their forces into localAcceleration every
|
|
// step, so an accumulator cleared per frame carries step one's thrust
|
|
// into step two whenever a frame holds two steps. How many steps a frame
|
|
// holds depends on wall-clock jitter, which made identical runs diverge
|
|
// by a quarter of a metre while sitting still on the pad.
|
|
//
|
|
// Idempotent on purpose: the frame-level copy still runs first on every
|
|
// path, and repeating this at each step start is a recompute from
|
|
// current state, not an accumulation.
|
|
//
|
|
void
|
|
Mover::BeginStep()
|
|
{
|
|
Check(this);
|
|
|
|
localVelocity.linearMotion.MultiplyByInverse(
|
|
worldLinearVelocity,
|
|
localToWorld
|
|
);
|
|
localAcceleration = Motion::Identity;
|
|
previousOrigin = localOrigin;
|
|
|
|
if (++normalizeCount >= 20)
|
|
{
|
|
localOrigin.angularPosition.Normalize();
|
|
normalizeCount = 0;
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::ResetUpdateIntervals()
|
|
{
|
|
Check(this);
|
|
|
|
updateIntervalCount = 0;
|
|
updateIntervalWrite = 0;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// Estimate how long until the next update for this entity arrives.
|
|
//
|
|
// This is not a cosmetic guess. DeadReckon blends toward the projected
|
|
// origin by
|
|
//
|
|
// percent = time_slice / ((nextUpdate - lastPerformance) + time_slice)
|
|
//
|
|
// so the prediction sets how far every single step moves. The original code
|
|
// predicted the next gap from the one previous gap. On a LAN that was fine,
|
|
// because the gaps were all alike. Over the internet a late packet doubles
|
|
// the prediction, percent collapses toward zero, the entity barely advances
|
|
// for a step and then catches up on the following ones - which is a visible
|
|
// tick. Measured on a live Steam connection at about 1.4 a second, with
|
|
// percent bottoming out at 0.014 against a normal range of 0.27 to 0.95.
|
|
//
|
|
// A median has a breakdown point of half its samples, so one straggler - or
|
|
// three - moves it not at all, while a real change in the send rate still
|
|
// carries it within a few updates. That is the whole trick: ignore the
|
|
// outlier, follow the trend.
|
|
//
|
|
Scalar
|
|
Mover::PredictUpdateInterval(Scalar latest)
|
|
{
|
|
Check(this);
|
|
|
|
//
|
|
// Only plausible gaps go into the window. Letting a join or a stall in
|
|
// would poison the estimate for the next eight updates - precisely when
|
|
// the entity is most conspicuous, just after it appears.
|
|
//
|
|
if (latest > kMinimumUpdateInterval && latest < kOutlierUpdateInterval)
|
|
{
|
|
updateIntervals[updateIntervalWrite] = latest;
|
|
updateIntervalWrite = (updateIntervalWrite + 1) % UpdateIntervalSamples;
|
|
if (updateIntervalCount < UpdateIntervalSamples)
|
|
{
|
|
updateIntervalCount++;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Too few samples to hold an opinion. Fall back to the old behaviour
|
|
// rather than inventing a rate we have no evidence for.
|
|
//
|
|
if (updateIntervalCount < 3)
|
|
{
|
|
return latest;
|
|
}
|
|
|
|
//
|
|
// Insertion sort - the window is eight samples, and this runs once per
|
|
// arriving packet per entity.
|
|
//
|
|
Scalar sorted[UpdateIntervalSamples];
|
|
int i;
|
|
|
|
for (i = 0; i < updateIntervalCount; i++)
|
|
{
|
|
sorted[i] = updateIntervals[i];
|
|
}
|
|
for (i = 1; i < updateIntervalCount; i++)
|
|
{
|
|
Scalar value = sorted[i];
|
|
int j = i - 1;
|
|
|
|
while (j >= 0 && sorted[j] > value)
|
|
{
|
|
sorted[j + 1] = sorted[j];
|
|
j--;
|
|
}
|
|
sorted[j + 1] = value;
|
|
}
|
|
|
|
Scalar median = sorted[updateIntervalCount / 2];
|
|
|
|
if (median < kMinimumPredictedInterval)
|
|
{
|
|
median = kMinimumPredictedInterval;
|
|
}
|
|
if (median > kMaximumPredictedInterval)
|
|
{
|
|
median = kMaximumPredictedInterval;
|
|
}
|
|
return median;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::ReadUpdateRecord(Simulation::UpdateRecord *record)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(record);
|
|
switch (record->recordID)
|
|
{
|
|
case DefaultUpdateModelBit:
|
|
{
|
|
|
|
//
|
|
//---------------------------------------
|
|
// Precalculation for next update time
|
|
//---------------------------------------
|
|
//
|
|
nextUpdate = Now();
|
|
Scalar diff = nextUpdate - lastUpdate;
|
|
if (diff < 10.0f)
|
|
{
|
|
if (UseMedianPrediction())
|
|
{
|
|
Scalar predicted = PredictUpdateInterval(diff);
|
|
|
|
//
|
|
// Score the previous prediction against the gap that
|
|
// actually just elapsed - a true one-step-ahead error,
|
|
// kept per entity so a trace reads the entity it is
|
|
// watching and not whichever one updated last.
|
|
//
|
|
if (predictedInterval > 0.0f)
|
|
{
|
|
predictionError = predictedInterval - diff;
|
|
}
|
|
predictedInterval = predicted;
|
|
|
|
nextUpdate += predicted;
|
|
}
|
|
else
|
|
{
|
|
nextUpdate.ticks += nextUpdate.ticks - lastUpdate.ticks;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// The stream was interrupted - a join, a pause, a long
|
|
// stall. Nothing recorded before it describes the rate
|
|
// now, so start the window over.
|
|
//
|
|
ResetUpdateIntervals();
|
|
}
|
|
|
|
//
|
|
//---------------------------------------
|
|
// Handle updating the entity information
|
|
//---------------------------------------
|
|
//
|
|
Entity::ReadUpdateRecord(record);
|
|
|
|
//
|
|
//-----------------------
|
|
// Update the motion data
|
|
//-----------------------
|
|
//
|
|
UpdateRecord *update = (UpdateRecord*)record;
|
|
|
|
localAcceleration = update->localAcceleration;
|
|
worldLinearAcceleration = update->worldLinearAcceleration;
|
|
|
|
updateVelocity.linearMotion = update->worldLinearVelocity;
|
|
updateVelocity.angularMotion = update->localVelocity.angularMotion;
|
|
|
|
updateAcceleration.linearMotion = update->worldLinearAcceleration;
|
|
updateAcceleration.angularMotion = update->localAcceleration.angularMotion;
|
|
|
|
//
|
|
//-----------------------------------------
|
|
// Update the collision volume if necessary
|
|
//-----------------------------------------
|
|
//
|
|
if (IsCollisionVolume())
|
|
{
|
|
MoveCollisionVolume();
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
Entity::ReadUpdateRecord(record);
|
|
break;
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::WriteUpdateRecord(
|
|
Simulation::UpdateRecord *record,
|
|
int update_model
|
|
)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(record);
|
|
switch (update_model)
|
|
{
|
|
case DefaultUpdateModelBit:
|
|
{
|
|
Entity::WriteUpdateRecord(record, update_model);
|
|
|
|
UpdateRecord *update = (UpdateRecord*)record;
|
|
|
|
update->recordLength = sizeof(*update);
|
|
update->localVelocity = localVelocity;
|
|
update->localAcceleration = localAcceleration;
|
|
|
|
update->worldLinearVelocity = worldLinearVelocity;
|
|
update->worldLinearAcceleration = worldLinearAcceleration;
|
|
|
|
updateVelocity.linearMotion = worldLinearVelocity;
|
|
updateVelocity.angularMotion = localVelocity.angularMotion;
|
|
|
|
updateAcceleration.linearMotion = worldLinearAcceleration;
|
|
updateAcceleration.angularMotion = localAcceleration.angularMotion;
|
|
}
|
|
break;
|
|
default:
|
|
Entity::WriteUpdateRecord(record, update_model);
|
|
break;
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//#############################################################################
|
|
// Collision support
|
|
//
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::MoveCollisionVolume()
|
|
{
|
|
//
|
|
//---------------------------------------------------
|
|
// Make sure that there is a collision volume to move
|
|
//---------------------------------------------------
|
|
//
|
|
Check(this);
|
|
if (!collisionVolumeCount)
|
|
{
|
|
Check_Fpu();
|
|
return;
|
|
}
|
|
|
|
//
|
|
//------------------------------------------------------------------------
|
|
// Set up the extents of the collision volume from the template and the
|
|
// current position. We must find the center point of the template volume
|
|
// and rotate it about the y axis
|
|
//------------------------------------------------------------------------
|
|
//
|
|
Check(collisionTemplate);
|
|
Check(collisionVolume);
|
|
Verify(collisionVolumeCount == 1);
|
|
Verify(collisionTemplate->solidType == BoxedSolid::YAxisCylinderType);
|
|
|
|
localToWorld = localOrigin;
|
|
Point3D centerPoint;
|
|
centerPoint.x = (collisionTemplate->minX + collisionTemplate->maxX) * 0.5f;
|
|
centerPoint.y = (collisionTemplate->minY + collisionTemplate->maxY) * 0.5f;
|
|
centerPoint.z = (collisionTemplate->minZ + collisionTemplate->maxZ) * 0.5f;
|
|
Vector3D radius;
|
|
radius.x = collisionTemplate->maxX - centerPoint.x;
|
|
radius.y = collisionTemplate->maxY - centerPoint.y;
|
|
radius.z = collisionTemplate->maxZ - centerPoint.z;
|
|
Point3D rotated;
|
|
rotated.Multiply(centerPoint, localToWorld);
|
|
|
|
collisionVolume->minX = rotated.x - radius.x;
|
|
collisionVolume->maxX = rotated.x + radius.x;
|
|
collisionVolume->minY = rotated.y - radius.y;
|
|
collisionVolume->maxY = rotated.y + radius.y;
|
|
collisionVolume->minZ = rotated.z - radius.z;
|
|
collisionVolume->maxZ = rotated.z + radius.z;
|
|
|
|
//
|
|
//------------------------------------------------------------
|
|
// Now, Find the smallest node containing our collision column
|
|
//------------------------------------------------------------
|
|
//
|
|
if (GetInstance() != ReplicantInstance)
|
|
{
|
|
InterestManager *interest_mgr =
|
|
application->GetInterestManager();
|
|
Check(interest_mgr);
|
|
InterestZone *zone =
|
|
interest_mgr->GetInterestZone(interestZoneID);
|
|
Check(zone);
|
|
BoxedSolidTree* tree = zone->GetCollisionRoot();
|
|
Check(tree);
|
|
containedByNode =
|
|
tree->FindSmallestNodeContainingColumn(*collisionVolume);
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
BoxedSolidCollisionList*
|
|
Mover::AllocateCollisionList()
|
|
{
|
|
Check(this);
|
|
|
|
//
|
|
//-----------------------------------------------------------
|
|
// Find the correct collision list to use, and reset to empty
|
|
//-----------------------------------------------------------
|
|
//
|
|
BoxedSolidCollisionList *collision_list;
|
|
if (lastCollisionList == collisionLists)
|
|
{
|
|
collision_list = &collisionLists[1];
|
|
}
|
|
else
|
|
{
|
|
collision_list = collisionLists;
|
|
}
|
|
Check(collision_list);
|
|
collision_list->Reset();
|
|
return collision_list;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
BoxedSolidCollisionList*
|
|
Mover::GetCurrentCollisions(BoxedSolidCollisionList *collision_list)
|
|
{
|
|
Check(this);
|
|
|
|
if (!collision_list)
|
|
{
|
|
collision_list = AllocateCollisionList();
|
|
}
|
|
Check(collision_list);
|
|
|
|
//
|
|
//---------------------------------
|
|
// Test against the tangible movers
|
|
//---------------------------------
|
|
//
|
|
Check(collisionAssistant);
|
|
CollisionAssistant::MovingEntityIterator iterator(collisionAssistant);
|
|
Entity *entity;
|
|
Check(collisionVolume);
|
|
while ((entity = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
//
|
|
//------------------------------------------------------------------
|
|
// If we are checking against ourselves, or something more than 50m
|
|
// away, skip it
|
|
//------------------------------------------------------------------
|
|
//
|
|
Check(entity);
|
|
if (entity == this)
|
|
{
|
|
continue;
|
|
}
|
|
Vector3D delta;
|
|
delta.Subtract(
|
|
entity->localOrigin.linearPosition,
|
|
localOrigin.linearPosition
|
|
);
|
|
if (delta.LengthSquared() > 2500.0f)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
//
|
|
//--------------------------------------------------
|
|
// If we have a mover class object, check against it
|
|
//--------------------------------------------------
|
|
//
|
|
if (entity->IsDerivedFrom(*Mover::GetClassDerivations()))
|
|
{
|
|
Mover *mover = (Mover*)entity;
|
|
Check(mover);
|
|
Check(mover->collisionVolume);
|
|
CheckAgainstBoxedSolidChain(collision_list, mover->collisionVolume);
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// If we have a door, check against its subsystems if we are close enough
|
|
// for it to matter
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
else if (entity->IsDerivedFrom(*DoorFrame::GetClassDerivations()))
|
|
{
|
|
DoorFrame *door_frame = (DoorFrame*)entity;
|
|
Check(door_frame);
|
|
for (int i=0; i<door_frame->GetSubsystemCount(); ++i)
|
|
{
|
|
Door *door = (Door*)door_frame->GetSubsystem(i);
|
|
CheckAgainstBoxedSolidChain(
|
|
collision_list,
|
|
door->GetFirstBoxedSolid()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//------------------------------
|
|
// Test against the static world
|
|
//------------------------------
|
|
//
|
|
containedByNode->FindBoundingBoxesContaining(
|
|
collisionVolume,
|
|
*collisionVolume,
|
|
*collision_list
|
|
);
|
|
|
|
Check_Fpu();
|
|
return collision_list;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
BoxedSolid*
|
|
Mover::FindBoxedSolidHitBy(
|
|
Line *line,
|
|
Entity *except
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(line);
|
|
|
|
//
|
|
//-------------------------------------------------------------------------
|
|
// Calculate the midpoint of the line, and sweep a sphere out around the
|
|
// line from that point, including an extra 50 meters. This extra distance
|
|
// takes into account the doors...
|
|
//-------------------------------------------------------------------------
|
|
//
|
|
Point3D center;
|
|
Scalar radius = line->length * 0.5f;
|
|
line->Project(radius, ¢er);
|
|
|
|
//
|
|
//---------------------------------
|
|
// Test against the tangible movers
|
|
//---------------------------------
|
|
//
|
|
Check(collisionAssistant);
|
|
CollisionAssistant::MovingEntityIterator iterator(collisionAssistant);
|
|
Entity *entity;
|
|
BoxedSolid
|
|
*solid = NULL,
|
|
*result;
|
|
|
|
while ((entity = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
//
|
|
//---------------------------------------------------------------
|
|
// If we are checking against ourselves or the exception, skip it
|
|
//---------------------------------------------------------------
|
|
//
|
|
Check(entity);
|
|
if (entity == this || except && except == entity)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
//
|
|
//-------------------------------------------------------------------
|
|
// If we have a mover class object, check against it. If we have no
|
|
// collision volume, we are just using a line, so just run it against
|
|
// the collision volume chain
|
|
//-------------------------------------------------------------------
|
|
//
|
|
if (entity->IsDerivedFrom(*Mover::GetClassDerivations()))
|
|
{
|
|
Mover *mover = (Mover*)entity;
|
|
Check(mover);
|
|
Check(mover->collisionVolume);
|
|
|
|
//
|
|
//-----------------------------------------------------------------
|
|
// If the mover is close enough to the radius of the line, check it
|
|
//-----------------------------------------------------------------
|
|
//
|
|
Vector3D delta;
|
|
delta.Subtract(entity->localOrigin.linearPosition, center);
|
|
Scalar r2 =
|
|
mover->collisionVolume->maxX - mover->collisionVolume->minX;
|
|
r2 += mover->collisionVolume->maxY - mover->collisionVolume->minY;
|
|
r2 *= 0.5f;
|
|
r2 += radius;
|
|
if (delta.LengthSquared() > r2*r2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result =
|
|
CheckLineAgainstBoxedSolidChain(line, mover->collisionVolume);
|
|
if (result)
|
|
{
|
|
Check(result);
|
|
solid = result;
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// If we have a door, check against its subsystems if we are close enough
|
|
// for it to matter
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
else if (entity->IsDerivedFrom(*DoorFrame::GetClassDerivations()))
|
|
{
|
|
//
|
|
//-----------------------------------------------------------------
|
|
// If the mover is close enough to the radius of the line, check it
|
|
//-----------------------------------------------------------------
|
|
//
|
|
Vector3D delta;
|
|
delta.Subtract(entity->localOrigin.linearPosition, center);
|
|
Scalar r2 = radius + 50.0f;
|
|
if (delta.LengthSquared() > r2*r2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DoorFrame *door_frame = (DoorFrame*)entity;
|
|
Check(door_frame);
|
|
for (int i=0; i<door_frame->GetSubsystemCount(); ++i)
|
|
{
|
|
Door *door = (Door*)door_frame->GetSubsystem(i);
|
|
result =
|
|
CheckLineAgainstBoxedSolidChain(
|
|
line,
|
|
door->GetFirstBoxedSolid()
|
|
);
|
|
if (result)
|
|
{
|
|
Check(result);
|
|
solid = result;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//------------------------------
|
|
// Test against the static world
|
|
//------------------------------
|
|
//
|
|
InterestManager *interest_mgr = application->GetInterestManager();
|
|
Check(interest_mgr);
|
|
InterestZone *zone = interest_mgr->GetInterestZone(interestZoneID);
|
|
Check(zone);
|
|
BoxedSolidTree* tree = zone->GetCollisionRoot();
|
|
Check(tree);
|
|
result = (BoxedSolid*)tree->FindBoundingBoxHitBy(line);
|
|
if (result)
|
|
{
|
|
Check(result);
|
|
solid = result;
|
|
}
|
|
Check_Fpu();
|
|
return solid;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
BoxedSolidCollisionList*
|
|
Mover::CollideCenterOfMotion(
|
|
Line *line,
|
|
BoxedSolidCollisionList *list
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(line);
|
|
|
|
//
|
|
//-----------------------------------------------------------
|
|
// Find the correct collision list to use, and reset to empty
|
|
//-----------------------------------------------------------
|
|
//
|
|
if (!list)
|
|
{
|
|
list = AllocateCollisionList();
|
|
}
|
|
Check(list);
|
|
|
|
//
|
|
//------------------------------------------------------------------------
|
|
// If the length of the line has changed, we must reposition the collision
|
|
// volume appropriately
|
|
//------------------------------------------------------------------------
|
|
//
|
|
BoxedSolid *solid = FindBoxedSolidHitBy(line, NULL);
|
|
if (solid && IsCollisionVolume())
|
|
{
|
|
line->FindEnd(&localOrigin.linearPosition);
|
|
MoveCollisionVolume();
|
|
ExtentBox slice;
|
|
slice.Intersect(*collisionVolume, *solid);
|
|
Verify(list->GetCollisionsLeft());
|
|
list->AddCollisionToList(solid, slice);
|
|
}
|
|
|
|
Check_Fpu();
|
|
return list;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::ProcessCollisionList(
|
|
BoxedSolidCollisionList *collisions,
|
|
Scalar time_slice,
|
|
const Point3D &old_position,
|
|
Damage *damage
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(collisions);
|
|
Verify(time_slice > 0.0f);
|
|
Check(&old_position);
|
|
Check_Pointer(damage);
|
|
|
|
damage->damageAmount = 0.0f;
|
|
damage->damageType = Damage::CollisionDamageType;
|
|
damage->impactPoint = Point3D::Identity;
|
|
|
|
if (collisions->GetCollisionCount())
|
|
{
|
|
//
|
|
//------------------------------------------------------------------
|
|
// Reduce the number of collisions we have to play with based on our
|
|
// velocity
|
|
//------------------------------------------------------------------
|
|
//
|
|
collisions->ReduceCollisionList(worldLinearVelocity);
|
|
|
|
//
|
|
//-----------------------------------------------------------------
|
|
// Setup up the totaling variables to handle averaging out multiple
|
|
// collisions
|
|
//-----------------------------------------------------------------
|
|
//
|
|
int total_collisions = 0;
|
|
Vector3D resultant_velocity = Vector3D::Identity;
|
|
Point3D resultant_position = Point3D::Identity;
|
|
Vector3D resultant_normal = Vector3D::Identity;
|
|
Vector3D initial_velocity = worldLinearVelocity;
|
|
Vector3D initial_position = localOrigin.linearPosition;
|
|
Scalar total_damage = 0.0f;
|
|
|
|
//
|
|
//---------------------------------------------------------------------
|
|
// For each hit in the list, process it, and if the collision is
|
|
// determined to be valid, bounce it and add the result into the others
|
|
//---------------------------------------------------------------------
|
|
//
|
|
for (int i=0; i<collisions->GetRealCollisions(); ++i)
|
|
{
|
|
//
|
|
//----------------------------------------------------------
|
|
// Make sure to bounce the vehicle from the correct location
|
|
//----------------------------------------------------------
|
|
//
|
|
worldLinearVelocity = initial_velocity;
|
|
localOrigin.linearPosition = initial_position;
|
|
damage->damageAmount = 0.0f;
|
|
ProcessCollision(
|
|
time_slice,
|
|
(*collisions)[i],
|
|
old_position,
|
|
damage
|
|
);
|
|
if (damage->damageAmount > 0.0f)
|
|
{
|
|
++total_collisions;
|
|
resultant_velocity += worldLinearVelocity;
|
|
resultant_position += localOrigin.linearPosition;
|
|
resultant_normal += damage->surfaceNormal;
|
|
total_damage += damage->damageAmount;
|
|
ExtentBox *box = &(*collisions)[i].collisionSlice;
|
|
damage->impactPoint.x += 0.5 *
|
|
(
|
|
box->minX + box->maxX
|
|
- (collisionVolume->minX - collisionVolume->maxX)
|
|
);
|
|
damage->impactPoint.y += 0.5 *
|
|
(
|
|
box->minY + box->maxY
|
|
- (collisionVolume->minY - collisionVolume->maxY)
|
|
);
|
|
damage->impactPoint.z += 0.5 *
|
|
(
|
|
box->minZ + box->maxZ
|
|
- (collisionVolume->minZ - collisionVolume->maxZ)
|
|
);
|
|
}
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------
|
|
// If we collided with more than one thing, average out the results
|
|
//-----------------------------------------------------------------
|
|
//
|
|
if (total_collisions > 1)
|
|
{
|
|
worldLinearVelocity.Divide(resultant_velocity, total_collisions);
|
|
localOrigin.linearPosition.Divide(
|
|
resultant_position,
|
|
total_collisions
|
|
);
|
|
damage->surfaceNormal.Vector3D::Divide(
|
|
resultant_normal,
|
|
total_collisions
|
|
);
|
|
goto Figure_Normal;
|
|
}
|
|
|
|
//
|
|
//------------------------------------------------------
|
|
// Otherwise, just set up the positions from the results
|
|
//------------------------------------------------------
|
|
//
|
|
else if (total_collisions == 1)
|
|
{
|
|
worldLinearVelocity = resultant_velocity;
|
|
localOrigin.linearPosition = resultant_position;
|
|
damage->surfaceNormal.operator=(resultant_normal);
|
|
|
|
//
|
|
//---------------------------------------------------------
|
|
// Figure out the normal, and calculate the collision force
|
|
//---------------------------------------------------------
|
|
//
|
|
Figure_Normal:
|
|
if (Small_Enough(damage->surfaceNormal.LengthSquared()))
|
|
{
|
|
damage->surfaceNormal.x = 0.0f;
|
|
damage->surfaceNormal.y = 1.0f;
|
|
damage->surfaceNormal.z = 0.0f;
|
|
}
|
|
else
|
|
{
|
|
damage->surfaceNormal.Normalize(damage->surfaceNormal);
|
|
}
|
|
MoveCollisionVolume();
|
|
damage->damageAmount = total_damage;
|
|
damage->damageForce.Subtract(worldLinearVelocity, initial_velocity);
|
|
}
|
|
lastCollisionList = collisions;
|
|
}
|
|
else
|
|
{
|
|
lastCollisionList = NULL;
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::ProcessCollision(
|
|
Scalar time_slice,
|
|
BoxedSolidCollision &collision,
|
|
const Point3D &old_position,
|
|
Damage *damage
|
|
)
|
|
{
|
|
Check(this);
|
|
Verify(time_slice > 0.0f);
|
|
Check(&collision);
|
|
Check(&old_position);
|
|
Check_Pointer(damage);
|
|
|
|
Scalar penetration;
|
|
|
|
//
|
|
//------------------------------------------------------------------------
|
|
// If we really have a collision, do a static bounce off of the normal
|
|
// generated. This is default behavior, and any derived class should make
|
|
// sure to handle any handshaking that needs to be done
|
|
//------------------------------------------------------------------------
|
|
//
|
|
if (
|
|
collisionVolume->ProcessCollision(
|
|
collision,
|
|
worldLinearVelocity,
|
|
lastCollisionList,
|
|
&damage->surfaceNormal,
|
|
&penetration
|
|
)
|
|
)
|
|
{
|
|
Max_Clamp(penetration, time_slice);
|
|
Scalar r = penetration / time_slice;
|
|
Scalar elasticity = elasticityCoefficient;
|
|
Scalar friction = frictionCoefficient;
|
|
|
|
damage->damageAmount =
|
|
StaticBounce(
|
|
old_position,
|
|
time_slice,
|
|
r,
|
|
damage->surfaceNormal,
|
|
&elasticity,
|
|
minimumBounceSpeed,
|
|
&friction
|
|
);
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::StartCollisionAssistant()
|
|
{
|
|
Check(this);
|
|
Verify(collisionAssistant == NULL);
|
|
|
|
collisionAssistant = CollisionAssistant::Make(this);
|
|
Register_Object(collisionAssistant);
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Scalar
|
|
Mover::StaticBounce(
|
|
const Point3D &, //old_position,
|
|
Scalar delta_t,
|
|
Scalar penetration,
|
|
const Normal &normal,
|
|
Scalar *elasticity,
|
|
Scalar bounce_min,
|
|
Scalar *friction
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(&normal);
|
|
Check_Pointer(elasticity);
|
|
Check_Pointer(friction);
|
|
|
|
Verify(penetration >= 0.0f && penetration <= 1.0f);
|
|
Verify(*elasticity >= 0.0f && *elasticity <= 1.0f);
|
|
Verify(*friction >= 0.0f);
|
|
Verify(delta_t > SMALL);
|
|
|
|
// penetration = 1.0f; // HACK - should keep stuff from going through the floor
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// Calculate the impact speed and vectors. If we didn't hit fast enough,
|
|
// don't do any bounce
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
Scalar impact = worldLinearVelocity * normal;
|
|
Vector3D vn,vp;
|
|
vn.Multiply(normal, impact);
|
|
vp.Subtract(worldLinearVelocity, vn);
|
|
if (impact > 0.0f)
|
|
{
|
|
Check_Fpu();
|
|
return 0.0f;
|
|
}
|
|
if (-impact <= bounce_min * delta_t)
|
|
{
|
|
*elasticity = 0.0f;
|
|
}
|
|
|
|
//
|
|
//--------------------------------------
|
|
// Calculate the energy lost to friction
|
|
//--------------------------------------
|
|
//
|
|
Scalar resistance = vp.Length();
|
|
if (Small_Enough(resistance))
|
|
{
|
|
*friction = resistance = 0.0f;
|
|
}
|
|
else
|
|
{
|
|
resistance =
|
|
1.0f + *friction * (1.0f + *elasticity) * impact / resistance;
|
|
if (resistance < 0.0f)
|
|
{
|
|
*friction = resistance = 0.0f;
|
|
}
|
|
}
|
|
|
|
//
|
|
//----------------------------------------------------
|
|
// Compute the velocity delta created by the collision
|
|
//----------------------------------------------------
|
|
//
|
|
Scalar temp = resistance - 1.0f;
|
|
Vector3D delta_v;
|
|
delta_v.Multiply(worldLinearVelocity, temp);
|
|
temp = resistance + *elasticity;
|
|
delta_v.AddScaled(delta_v, vn, -temp);
|
|
|
|
//
|
|
//------------------------------------
|
|
// Figure out the kinetic energy stuff
|
|
//------------------------------------
|
|
//
|
|
temp = -1.0f - *elasticity;
|
|
vn *= temp;
|
|
vp.AddScaled(vn, worldLinearVelocity, 2.0f);
|
|
|
|
//
|
|
// Reflect the velocity vector
|
|
//
|
|
worldLinearVelocity += delta_v;
|
|
temp = penetration * delta_t;
|
|
delta_v *= temp;
|
|
localOrigin.linearPosition += delta_v;
|
|
|
|
//
|
|
// Compute the kinetic energy loss
|
|
//
|
|
Check_Fpu();
|
|
return -0.0005 * (vn * vp) * moverMass;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Scalar
|
|
Mover::DynamicBounce(
|
|
Mover *other,
|
|
Scalar delta_t,
|
|
Scalar penetration,
|
|
const Normal &normal,
|
|
Scalar *elasticity
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(other);
|
|
Check(&normal);
|
|
Check_Pointer(elasticity);
|
|
|
|
Verify(penetration >= 0.0f && penetration <= 1.0f);
|
|
Verify(*elasticity >= 0.0f && *elasticity <= 1.0f);
|
|
Verify(delta_t > SMALL);
|
|
|
|
//
|
|
//------------------------------------------------------------------------
|
|
// Get the relative velocity of the other guy, and figure out the velocity
|
|
// delta along the normal
|
|
//------------------------------------------------------------------------
|
|
//
|
|
Scalar k1 = worldLinearVelocity.LengthSquared();
|
|
Scalar k2 = other->worldLinearVelocity.LengthSquared();
|
|
|
|
Scalar mass_ratio = other->moverMass / (moverMass + other->moverMass);
|
|
Check_Fpu();
|
|
Vector3D v;
|
|
v.Subtract(other->worldLinearVelocity, worldLinearVelocity);
|
|
Scalar temp = (1.0f + *elasticity) * (v*normal);
|
|
Vector3D delta_v;
|
|
delta_v.Multiply(normal, temp);
|
|
|
|
//
|
|
//-------------------------------------------------------------------------
|
|
// Figure out the kinetic energy loss in kilojoules, and bounce the primary
|
|
// mover
|
|
//
|
|
// There was an additional multiplication by mass ratio in system 3 code...
|
|
// we should make sure it is really needed...
|
|
//-------------------------------------------------------------------------
|
|
//
|
|
v.AddScaled(delta_v, v, -2.0f);
|
|
worldLinearVelocity.AddScaled(
|
|
worldLinearVelocity,
|
|
delta_v,
|
|
mass_ratio
|
|
);
|
|
localOrigin.linearPosition.AddScaled(
|
|
localOrigin.linearPosition,
|
|
delta_v,
|
|
delta_t * penetration
|
|
);
|
|
|
|
//
|
|
//----------------------------------------------------------
|
|
// Bounce the second object, and reset it's update values...
|
|
//----------------------------------------------------------
|
|
//
|
|
other->worldLinearVelocity.AddScaled(
|
|
other->worldLinearVelocity,
|
|
delta_v,
|
|
mass_ratio - 1.0f
|
|
);
|
|
other->localOrigin.linearPosition.AddScaled(
|
|
other->localOrigin.linearPosition,
|
|
delta_v,
|
|
delta_t
|
|
);
|
|
other->updateVelocity.linearMotion = other->worldLinearVelocity;
|
|
other->updateOrigin.linearPosition = other->localOrigin.linearPosition;
|
|
other->lastUpdate = Now();
|
|
|
|
//
|
|
//--------------------------------
|
|
// Return the result in kilojoules
|
|
//--------------------------------
|
|
//
|
|
k1 -= worldLinearVelocity.LengthSquared();
|
|
k2 -= other->worldLinearVelocity.LengthSquared();
|
|
Check_Fpu();
|
|
return 0.0005f * mass_ratio * (moverMass * k1 + other->moverMass * k2);
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::CheckAgainstBoxedSolidChain(
|
|
BoxedSolidCollisionList *collisions,
|
|
BoxedSolid *chain
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(collisions);
|
|
|
|
//
|
|
//----------------------------------------------------------------------
|
|
// If the two movers collided against with each other, add the result to
|
|
// the collision list
|
|
//----------------------------------------------------------------------
|
|
//
|
|
while (chain)
|
|
{
|
|
Check(chain);
|
|
ExtentBox slice;
|
|
if (chain->Intersects(*collisionVolume, &slice))
|
|
{
|
|
Verify(collisions->GetCollisionsLeft());
|
|
collisions->AddCollisionToList(chain, slice);
|
|
if (!collisions->GetCollisionsLeft())
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
chain = chain->GetNextSolid();
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Mover::CheckVolumeAgainstBoxedSolidChain(
|
|
BoxedSolidCollisionList *collisions,
|
|
BoxedSolid *chain
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(collisions);
|
|
|
|
//
|
|
//----------------------------------------------------------------------
|
|
// If the two movers collided against with each other, add the result to
|
|
// the collision list
|
|
//----------------------------------------------------------------------
|
|
//
|
|
while (chain)
|
|
{
|
|
Check(chain);
|
|
ExtentBox slice;
|
|
if (chain->Intersects(*collisionVolume, &slice))
|
|
{
|
|
Verify(collisions->GetCollisionsLeft());
|
|
collisions->AddCollisionToList(collisionVolume, slice);
|
|
if (!collisions->GetCollisionsLeft())
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
chain = chain->GetNextSolid();
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
BoxedSolid*
|
|
Mover::CheckLineAgainstBoxedSolidChain(
|
|
Line *line,
|
|
BoxedSolid *chain
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(line);
|
|
|
|
//
|
|
//----------------------------------------------------------------------
|
|
// If the two movers collided against with each other, add the result to
|
|
// the collision list
|
|
//----------------------------------------------------------------------
|
|
//
|
|
BoxedSolid *result = NULL;
|
|
while (chain)
|
|
{
|
|
Check(chain);
|
|
if (chain->HitBy(line))
|
|
{
|
|
result = chain;
|
|
}
|
|
chain = chain->GetNextSolid();
|
|
}
|
|
Check_Fpu();
|
|
return result;
|
|
}
|
|
|
|
//#############################################################################
|
|
// Construction and Destruction
|
|
//
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Mover::Mover(
|
|
Mover::MakeMessage *creation_message,
|
|
Mover::SharedData &virtual_data
|
|
):
|
|
Entity(creation_message, virtual_data)
|
|
{
|
|
Check_Pointer(this);
|
|
Check(creation_message);
|
|
|
|
Check(application);
|
|
ResourceFile *res_file = application->GetResourceFile();
|
|
Check(res_file);
|
|
|
|
//
|
|
//------------------------------
|
|
// Initialize the motion vectors
|
|
//------------------------------
|
|
//
|
|
localVelocity = creation_message->localVelocity;
|
|
localAcceleration = creation_message->localAcceleration;
|
|
worldLinearAcceleration.Multiply(
|
|
localAcceleration.linearMotion,
|
|
localToWorld
|
|
);
|
|
worldLinearVelocity.Multiply(
|
|
localVelocity.linearMotion,
|
|
localToWorld
|
|
);
|
|
|
|
updateVelocity.linearMotion = worldLinearVelocity;
|
|
updateVelocity.angularMotion = localVelocity.linearMotion;
|
|
|
|
updateAcceleration.linearMotion = worldLinearAcceleration;
|
|
updateAcceleration.angularMotion = localAcceleration.linearMotion;
|
|
nextUpdate = lastUpdate;
|
|
|
|
ResetUpdateIntervals();
|
|
predictedInterval = 0.0f;
|
|
predictionError = 0.0f;
|
|
|
|
normalizeCount = 0;
|
|
if (IsInitialStasis())
|
|
{
|
|
SetSimulationState(StasisState);
|
|
}
|
|
|
|
collisionVolume = NULL;
|
|
collisionTemplate = NULL;
|
|
containedByNode = NULL;
|
|
collisionLists = NULL;
|
|
lastCollisionList = NULL;
|
|
collisionAssistant = NULL;
|
|
deadReckoner = NULL;
|
|
collisionVolumeCount = 0;
|
|
|
|
ResourceDescription *res =
|
|
res_file->SearchList(
|
|
resourceID,
|
|
ResourceDescription::GameModelResourceType
|
|
);
|
|
Check(res);
|
|
res->Lock();
|
|
ModelResource* model = (ModelResource*)res->resourceAddress;
|
|
Check_Pointer(model);
|
|
|
|
moverMass = model->moverMass;
|
|
Verify(!Small_Enough(model->momentOfInertia.x));
|
|
momentOfInertia.x = 1.0f/model->momentOfInertia.x;
|
|
Verify(!Small_Enough(model->momentOfInertia.y));
|
|
momentOfInertia.y = 1.0f/model->momentOfInertia.y;
|
|
Verify(!Small_Enough(model->momentOfInertia.z));
|
|
momentOfInertia.z = 1.0f/model->momentOfInertia.z;
|
|
positiveLinearDragCoefficients = model->positiveLinearDragCoefficients;
|
|
negativeLinearDragCoefficients = model->negativeLinearDragCoefficients;
|
|
angularDragCoefficients = model->angularDragCoefficients;
|
|
frictionCoefficient = model->frictionCoefficient;
|
|
elasticityCoefficient = model->elasticityCoefficient;
|
|
minimumBounceSpeed = model->minimumBounceSpeed;
|
|
|
|
//
|
|
//--------------------------------------------------------------------
|
|
// Read the collision information from the resource file, but for now,
|
|
// assume a VTV
|
|
//--------------------------------------------------------------------
|
|
//
|
|
collisionLists = new BoxedSolidCollisionList[2];
|
|
Register_Pointer(collisionLists);
|
|
res->Unlock();
|
|
if (IsCollisionVolume())
|
|
{
|
|
res =
|
|
res_file->SearchList(
|
|
resourceID,
|
|
ResourceDescription::BoxedSolidStreamResourceType
|
|
);
|
|
Check(res);
|
|
res->Lock();
|
|
|
|
BoxedSolidResource* box = (BoxedSolidResource*)res->resourceAddress;
|
|
Check_Pointer(box);
|
|
collisionVolumeCount = res->resourceSize / sizeof(BoxedSolidResource);
|
|
|
|
for (int i=0; i<collisionVolumeCount; ++i)
|
|
{
|
|
collisionTemplate =
|
|
BoxedSolid::MakeBoxedSolid(box, this, collisionTemplate);
|
|
Register_Object(collisionTemplate);
|
|
|
|
collisionVolume =
|
|
BoxedSolid::MakeBoxedSolid(box, this, collisionVolume);
|
|
Register_Object(collisionVolume);
|
|
|
|
++box;
|
|
}
|
|
|
|
res->Unlock();
|
|
MoveCollisionVolume();
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Logical
|
|
Mover::CreateMakeMessage(
|
|
MakeMessage *creation_message,
|
|
NotationFile *model_file,
|
|
const ResourceDirectories *directories
|
|
)
|
|
{
|
|
Check(creation_message);
|
|
Check(model_file);
|
|
|
|
if (!Entity::CreateMakeMessage(creation_message, model_file, directories))
|
|
{
|
|
return False;
|
|
}
|
|
|
|
creation_message->messageLength = sizeof(Mover::MakeMessage);
|
|
creation_message->classToCreate = RegisteredClass::TrivialMoverClassID;
|
|
// creation_message->instanceFlags = DefaultFlags;
|
|
creation_message->localVelocity = Motion::Identity;
|
|
creation_message->localAcceleration = Motion::Identity;
|
|
Check_Fpu();
|
|
return True;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
ResourceDescription::ResourceID
|
|
Mover::CreateModelResource(
|
|
ResourceFile *resource_file,
|
|
const char* model_name,
|
|
NotationFile *model_file,
|
|
const ResourceDirectories *,//directories,
|
|
ModelResource *model
|
|
)
|
|
{
|
|
Check(resource_file);
|
|
Check_Pointer(model_name);
|
|
Check(model_file);
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// If we were not provided a buffer to write the model data into, we must
|
|
// create it ourselves
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
ModelResource *local_model = model;
|
|
if (!local_model)
|
|
{
|
|
local_model = new ModelResource;
|
|
Register_Pointer(local_model);
|
|
}
|
|
|
|
//
|
|
//-----------------
|
|
// Read in the mass
|
|
//-----------------
|
|
//
|
|
if (!model_file->GetEntry("gamedata", "MoverMass", &local_model->moverMass))
|
|
{
|
|
std::cerr << model_name << " missing MoverMass!\n";
|
|
Dump_And_Die:
|
|
if (!model)
|
|
{
|
|
Unregister_Pointer(local_model);
|
|
delete local_model;
|
|
}
|
|
Check_Fpu();
|
|
return -1;
|
|
}
|
|
|
|
//
|
|
//------------------------------
|
|
// Read in the moment of inertia
|
|
//------------------------------
|
|
//
|
|
const char* entry;
|
|
if (
|
|
!model_file->GetEntry(
|
|
"gamedata",
|
|
"MomentOfInertia",
|
|
&entry
|
|
)
|
|
)
|
|
{
|
|
std::cerr << model_name << " missing MomentOfInertia!\n";
|
|
goto Dump_And_Die;
|
|
}
|
|
sscanf(
|
|
entry,
|
|
"%f %f %f",
|
|
&local_model->momentOfInertia.x,
|
|
&local_model->momentOfInertia.y,
|
|
&local_model->momentOfInertia.z
|
|
);
|
|
|
|
//
|
|
//------------------------------
|
|
// Read in the drag coefficients
|
|
//------------------------------
|
|
//
|
|
if (
|
|
!model_file->GetEntry(
|
|
"gamedata",
|
|
"PositiveLinearDragCoefficients",
|
|
&entry
|
|
)
|
|
)
|
|
{
|
|
std::cerr << model_name << " missing PositiveLinearDragCoefficients!\n";
|
|
goto Dump_And_Die;
|
|
}
|
|
sscanf(
|
|
entry,
|
|
"%f %f %f",
|
|
&local_model->positiveLinearDragCoefficients.x,
|
|
&local_model->positiveLinearDragCoefficients.y,
|
|
&local_model->positiveLinearDragCoefficients.z
|
|
);
|
|
|
|
if (
|
|
!model_file->GetEntry(
|
|
"gamedata",
|
|
"NegativeLinearDragCoefficients",
|
|
&entry
|
|
)
|
|
)
|
|
{
|
|
std::cerr << model_name << " missing NegativeLinearDragCoefficients!\n";
|
|
goto Dump_And_Die;
|
|
}
|
|
sscanf(
|
|
entry,
|
|
"%f %f %f",
|
|
&local_model->negativeLinearDragCoefficients.x,
|
|
&local_model->negativeLinearDragCoefficients.y,
|
|
&local_model->negativeLinearDragCoefficients.z
|
|
);
|
|
|
|
//
|
|
//-------------------------
|
|
// Read in the angular drag
|
|
//-------------------------
|
|
//
|
|
if (
|
|
!model_file->GetEntry(
|
|
"gamedata",
|
|
"AngularDragCoefficients",
|
|
&entry
|
|
)
|
|
)
|
|
{
|
|
std::cerr << model_name << " missing AngularDragCoefficients!\n";
|
|
goto Dump_And_Die;
|
|
}
|
|
sscanf(
|
|
entry,
|
|
"%f %f %f",
|
|
&local_model->angularDragCoefficients.x,
|
|
&local_model->angularDragCoefficients.y,
|
|
&local_model->angularDragCoefficients.z
|
|
);
|
|
|
|
//
|
|
//---------------------
|
|
// Read in the friction
|
|
//---------------------
|
|
//
|
|
if (
|
|
!model_file->GetEntry(
|
|
"gamedata",
|
|
"FrictionCoefficient",
|
|
&local_model->frictionCoefficient
|
|
)
|
|
)
|
|
{
|
|
std::cerr << model_name << " missing FrictionCoefficient!\n";
|
|
goto Dump_And_Die;
|
|
}
|
|
|
|
//
|
|
//-----------------------
|
|
// Read in the elasticity
|
|
//-----------------------
|
|
//
|
|
if (
|
|
!model_file->GetEntry(
|
|
"gamedata",
|
|
"ElasticityCoefficient",
|
|
&local_model->elasticityCoefficient
|
|
)
|
|
)
|
|
{
|
|
std::cerr << model_name << " missing ElasticityCoefficient!\n";
|
|
goto Dump_And_Die;
|
|
}
|
|
|
|
//
|
|
//---------------------------------
|
|
// Read in the minimum bounce speed
|
|
//---------------------------------
|
|
//
|
|
if (
|
|
!model_file->GetEntry(
|
|
"gamedata",
|
|
"MinimumBounceSpeed",
|
|
&local_model->minimumBounceSpeed
|
|
)
|
|
)
|
|
{
|
|
std::cerr << model_name << " missing MinimumBounceSpeed!\n";
|
|
goto Dump_And_Die;
|
|
}
|
|
|
|
//
|
|
//-------------------------------------------------------------------------
|
|
// If we created the model buffer, then we have the responsibility to write
|
|
// it out to the resource file
|
|
//-------------------------------------------------------------------------
|
|
//
|
|
if (!model)
|
|
{
|
|
ResourceDescription *new_res =
|
|
resource_file->AddResource(
|
|
model_name,
|
|
ResourceDescription::GameModelResourceType,
|
|
1,
|
|
ResourceDescription::Preload,
|
|
local_model,
|
|
sizeof(*local_model)
|
|
);
|
|
Unregister_Pointer(local_model);
|
|
delete local_model;
|
|
Check(new_res);
|
|
Check_Fpu();
|
|
return new_res->resourceID;
|
|
}
|
|
else
|
|
{
|
|
Check_Fpu();
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Mover*
|
|
Mover::Make(Mover::MakeMessage *creation_message)
|
|
{
|
|
return new Mover(creation_message, DefaultData);
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Mover::~Mover()
|
|
{
|
|
Unregister_Pointer(collisionLists);
|
|
delete[] collisionLists;
|
|
|
|
if (IsCollisionVolume())
|
|
{
|
|
BoxedSolid *box = collisionTemplate;
|
|
while (box)
|
|
{
|
|
BoxedSolid *next_box = box->GetNextSolid();
|
|
Unregister_Object(box);
|
|
delete box;
|
|
box = next_box;
|
|
}
|
|
box = collisionVolume;
|
|
while (box)
|
|
{
|
|
BoxedSolid *next_box = box->GetNextSolid();
|
|
Unregister_Object(box);
|
|
delete box;
|
|
box = next_box;
|
|
}
|
|
}
|
|
|
|
if (collisionAssistant)
|
|
{
|
|
Unregister_Object(collisionAssistant);
|
|
delete collisionAssistant;
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
Logical
|
|
Mover::TestInstance() const
|
|
{
|
|
return IsDerivedFrom(*GetClassDerivations());
|
|
}
|