Files
RP412/MUNGA/MOVER.cpp
T
CydandClaude Opus 5 178714198a The warmup path was the one that skipped the clamp
Live trace of the median predictor: in steady state the tick is gone -
zero spikes over five seconds where there were seven, every step blending
instead of 182 in 250, the blend fraction floored at 0.294 instead of
0.014, and the prediction within 13 to 20 ms of the gap that followed it.

But the log also read "predicting 2.054s", above the 1.0s clamp, which
should not have been reachable. It was: the fewer-than-three-samples path
returned the raw gap without clamping it. The arithmetic identifies it
exactly - 0.0201 / (2.054 + 0.0201) is 0.009685, against a logged blend
fraction of 0.00968523.

So the old near-stall survived, confined to the first three updates after
an entity appears. That is every respawn, and a pod is being watched
closely at exactly that moment.

Route every path through one ClampPredictedInterval, and tighten the
bounds now that the real send rate is known to be about 30ms: nothing
slower than half a second enters the sample window, and no prediction
reaches beyond 250ms. The second of those puts a floor under the blend
fraction itself - at a 20ms step the worst case is 0.02/(0.25+0.02),
roughly 7% of the gap per step, so a pod converges on its projection in a
dozen steps rather than crawling toward it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 08:46:31 -05:00

2439 lines
61 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.
//
// Measured send rate on a live connection is about 30ms, so half a second
// is already sixteen times slower than anything healthy.
//
static const Scalar kMinimumUpdateInterval = 0.001f;
static const Scalar kOutlierUpdateInterval = 0.5f;
static const Scalar kMinimumPredictedInterval = 0.010f;
//
// Never predict further ahead than this, which puts a floor under the
// dead reckoner's blend fraction: at a 20ms step the worst case becomes
// 0.02/(0.25+0.02), near enough 7% of the gap per step, so a pod still
// converges on its projection in a dozen steps instead of crawling.
//
static const Scalar kMaximumPredictedInterval = 0.25f;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// RP412NETPREDICT=0 restores the original single-sample prediction, so the
// two can be compared on the same build and the same connection.
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Every path out of PredictUpdateInterval goes through here. It used not
// to, and the one that skipped it was the bug.
//
static Scalar
ClampPredictedInterval(Scalar interval)
{
if (interval < kMinimumPredictedInterval)
{
return kMinimumPredictedInterval;
}
if (interval > kMaximumPredictedInterval)
{
return kMaximumPredictedInterval;
}
return interval;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
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 gap we just saw
// rather than inventing a rate we have no evidence for - but clamp it
// like any other answer. Leaving this path unclamped let a 2.05s gap
// through in the first updates after an entity appeared, which drove
// the blend fraction to 0.0097 and stalled the step. That is every
// respawn, and it is exactly when the pod is being watched.
//
if (updateIntervalCount < 3)
{
return ClampPredictedInterval(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;
}
return ClampPredictedInterval(sorted[updateIntervalCount / 2]);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
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, &center);
//
//---------------------------------
// 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());
}