The tick is now fully accounted for. Corrections arrive about 26 times a second per pod, mean 0.25 to 0.66m against a step of roughly a metre. localOrigin is never snapped during a mission - only updateOrigin is replaced - so the pod position stays continuous and its TARGET jumps. The lerp then translates a target discontinuity into a step-size one: fifteen steps smooth to a tenth of a percent, then a single step at 20-33%, then recovery. Every capture has that shape and the ratios match the correction size arithmetically. Position is C0-continuous, so no amount of position interpolation can hide it; the discontinuity is in the rate. Before smoothing anything, ask whether the correction is even real. Half a metre at 52 m/s is ten milliseconds of travel, and a dead reckoner tracking constant velocity across a 38ms gap should be right to within centimetres. That smells like evaluating the projection at the wrong instant rather than like a prediction that genuinely failed. Settle it without involving any clock we do not trust. Take the position and velocity the sender reported last time, carry them forward by the gap between the two SENDER timestamps, and compare against the position the sender reports now. Both stamps come from one machine, so latency, clock offset and RP412NETCLOCK play no part whatsoever. Split the error along the path and across it. Along is time: divided by speed it IS the milliseconds the window is out by, and its sign says which way. Across cannot be a timing fault at all - that is a pod turning, and no clock fix would touch it. TIMING says fix the extrapolation window and the tick shrinks at the source with no smoothing and no lag. MANOEUVRE says the corrections are honest, the pods really are cornering, and smoothing is the only remaining answer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
535 lines
12 KiB
C++
535 lines
12 KiB
C++
#pragma once
|
|
|
|
#include "entity.h"
|
|
#include "motion.h"
|
|
|
|
class Mover__SharedData;
|
|
class Mover;
|
|
class CollisionAssistant;
|
|
class BoundingBoxTreeNode;
|
|
class BoxedSolid;
|
|
class BoxedSolidCollision;
|
|
class BoxedSolidCollisionList;
|
|
class Normal;
|
|
class Line;
|
|
class Environment;
|
|
|
|
//##########################################################################
|
|
//#################### Mover::UpdateMessage ##########################
|
|
//##########################################################################
|
|
|
|
struct Mover__UpdateRecord:
|
|
public Entity::UpdateRecord
|
|
{
|
|
public:
|
|
Motion
|
|
localVelocity,
|
|
localAcceleration;
|
|
Vector3D
|
|
worldLinearVelocity,
|
|
worldLinearAcceleration;
|
|
};
|
|
|
|
//##########################################################################
|
|
//##################### Mover::MakeMessage ##########################
|
|
//##########################################################################
|
|
|
|
class Mover__MakeMessage:
|
|
public Entity::MakeMessage
|
|
{
|
|
public:
|
|
Motion
|
|
localVelocity,
|
|
localAcceleration;
|
|
|
|
Mover__MakeMessage(
|
|
Receiver::MessageID message_ID,
|
|
size_t length,
|
|
Entity::ClassID class_ID,
|
|
const EntityID &owner_ID,
|
|
ResourceDescription::ResourceID resource_ID,
|
|
LWord instance_flags,
|
|
const Origin &origin,
|
|
const Motion &velocity,
|
|
const Motion &acceleration
|
|
):
|
|
Entity::MakeMessage(
|
|
message_ID,
|
|
length,
|
|
class_ID,
|
|
owner_ID,
|
|
resource_ID,
|
|
instance_flags,
|
|
origin
|
|
),
|
|
localVelocity(velocity),
|
|
localAcceleration(acceleration)
|
|
{}
|
|
Mover__MakeMessage(
|
|
Receiver::MessageID message_ID,
|
|
size_t length,
|
|
const EntityID &entity_ID,
|
|
Entity::ClassID class_ID,
|
|
const EntityID &owner_ID,
|
|
ResourceDescription::ResourceID resource_ID,
|
|
LWord instance_flags,
|
|
const Origin &origin,
|
|
const Motion &velocity,
|
|
const Motion &acceleration
|
|
):
|
|
Entity::MakeMessage(
|
|
message_ID,
|
|
length,
|
|
entity_ID,
|
|
class_ID,
|
|
owner_ID,
|
|
resource_ID,
|
|
instance_flags,
|
|
origin
|
|
),
|
|
localVelocity(velocity),
|
|
localAcceleration(acceleration)
|
|
{}
|
|
};
|
|
|
|
//##########################################################################
|
|
//##################### Mover::ModelResource #########################
|
|
//##########################################################################
|
|
|
|
struct Mover__ModelResource
|
|
{
|
|
Scalar moverMass;
|
|
Vector3D
|
|
momentOfInertia,
|
|
positiveLinearDragCoefficients,
|
|
negativeLinearDragCoefficients,
|
|
angularDragCoefficients;
|
|
Scalar
|
|
frictionCoefficient,
|
|
elasticityCoefficient,
|
|
minimumBounceSpeed;
|
|
};
|
|
|
|
//##########################################################################
|
|
//############################ Mover #################################
|
|
//##########################################################################
|
|
|
|
class Mover:
|
|
public Entity
|
|
{
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Shared Data support
|
|
//
|
|
public:
|
|
static Derivation *GetClassDerivations();
|
|
|
|
static SharedData DefaultData;
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Attribute Support
|
|
//
|
|
public:
|
|
enum {
|
|
LocalVelocityAttributeID = Entity::NextAttributeID,
|
|
LocalAccelerationAttributeID,
|
|
WorldLinearVelocityAttributeID,
|
|
WorldLinearAccelerationAttributeID,
|
|
MoverMassAttributeID,
|
|
MomentOfInertiaAttributeID,
|
|
PositiveLinearDragCoefficientsAttributeID,
|
|
NegativeLinearDragCoefficientsAttributeID,
|
|
AngularDragCoefficientsAttributeID,
|
|
FrictionCoefficientAttributeID,
|
|
ElasticityCoefficientAttributeID,
|
|
MinimumBounceSpeedAttributeID,
|
|
NextAttributeID
|
|
};
|
|
|
|
private:
|
|
static const IndexEntry AttributePointers[];
|
|
|
|
protected:
|
|
//static AttributeIndexSet AttributeIndex
|
|
static AttributeIndexSet& GetAttributeIndex();
|
|
|
|
//
|
|
// public attribute declarations go here
|
|
//
|
|
public:
|
|
Motion
|
|
localVelocity,
|
|
localAcceleration;
|
|
Vector3D
|
|
worldLinearVelocity,
|
|
worldLinearAcceleration;
|
|
Scalar moverMass;
|
|
Vector3D
|
|
momentOfInertia,
|
|
positiveLinearDragCoefficients,
|
|
negativeLinearDragCoefficients,
|
|
angularDragCoefficients;
|
|
Scalar
|
|
frictionCoefficient,
|
|
elasticityCoefficient,
|
|
minimumBounceSpeed;
|
|
|
|
//
|
|
// virtual access
|
|
//
|
|
public:
|
|
virtual Vector3D
|
|
GetWorldLinearVelocity()
|
|
{return worldLinearVelocity;}
|
|
virtual Vector3D
|
|
GetWorldLinearAcceleration()
|
|
{return worldLinearAcceleration;}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Model Support
|
|
//
|
|
public:
|
|
enum {
|
|
StasisState = Entity::StateCount,
|
|
StateCount
|
|
};
|
|
|
|
typedef Mover__UpdateRecord UpdateRecord;
|
|
typedef Mover__MakeMessage MakeMessage;
|
|
typedef void
|
|
(Mover::*Performance)(Scalar time_slice);
|
|
void
|
|
SetPerformance(Performance performance)
|
|
{
|
|
Check(this);
|
|
activePerformance = (Simulation::Performance)performance;
|
|
}
|
|
|
|
void
|
|
UpdateWorldMotion();
|
|
void
|
|
UpdateLocalMotion();
|
|
void
|
|
ApplyWorldAccelerations(Scalar time_slice);
|
|
void
|
|
ApplyAirResistanceAndGravity(Scalar power=1.0f);
|
|
void
|
|
CalculateDrag(
|
|
Vector3D *drag,
|
|
const Vector3D &velocity,
|
|
const Vector3D &positive_CODs,
|
|
const Vector3D &negative_CODs,
|
|
Scalar power
|
|
);
|
|
|
|
void
|
|
ApplyLocalForce(
|
|
const Vector3D &force,
|
|
const Vector3D &moment
|
|
);
|
|
void
|
|
ApplyLocalAcceleration(
|
|
const Vector3D &acceleration,
|
|
const Vector3D &moment
|
|
);
|
|
|
|
Environment*
|
|
GetEnvironment()
|
|
{Check(this); return localEnvironment;}
|
|
|
|
typedef Logical (Mover::*DeadReckoner)();
|
|
void
|
|
SetDeadReckoner(DeadReckoner reckoner)
|
|
{Check(this); deadReckoner = reckoner;}
|
|
|
|
Logical
|
|
NoDeadReckoner();
|
|
Logical
|
|
LinearDeadReckoner();
|
|
Logical
|
|
AcceleratedDeadReckoner();
|
|
void
|
|
DeadReckon(Scalar time_slice);
|
|
|
|
protected:
|
|
void
|
|
WriteUpdateRecord(
|
|
Simulation::UpdateRecord *message,
|
|
int update_model
|
|
);
|
|
void
|
|
ReadUpdateRecord(Simulation::UpdateRecord *message);
|
|
|
|
void
|
|
PerformAndWatch(
|
|
const Time& till,
|
|
MemoryStream *update_stream
|
|
);
|
|
|
|
//
|
|
// Per-step set-up under fixed stepping: clears the force accumulator
|
|
// the thrusters add into, so each step integrates only its own
|
|
// forces. See the definition for the frame-jitter bug this closes.
|
|
//
|
|
void
|
|
BeginStep();
|
|
|
|
int
|
|
normalizeCount;
|
|
Environment
|
|
*localEnvironment;
|
|
DeadReckoner
|
|
deadReckoner;
|
|
Origin
|
|
projectedOrigin,
|
|
previousOrigin;
|
|
Motion
|
|
projectedVelocity,
|
|
updateAcceleration,
|
|
updateVelocity;
|
|
Time
|
|
nextUpdate;
|
|
|
|
//
|
|
// Recent gaps between replication updates for this entity, as a ring,
|
|
// and the running estimate drawn from them. The original code predicted
|
|
// the next gap from the single previous gap; see PredictUpdateInterval
|
|
// for why that stalls a step every time a packet runs late.
|
|
//
|
|
enum {UpdateIntervalSamples = 8};
|
|
Scalar
|
|
updateIntervals[UpdateIntervalSamples];
|
|
int
|
|
updateIntervalCount,
|
|
updateIntervalWrite;
|
|
|
|
//
|
|
// The interval last predicted, and how wrong the prediction before it
|
|
// proved to be once the gap it described actually elapsed. Per entity,
|
|
// so a trace reads the entity it is watching.
|
|
//
|
|
Scalar
|
|
predictedInterval,
|
|
predictionError;
|
|
|
|
//
|
|
// Arrival statistics for the window a trace reports over. A long gap
|
|
// followed by normal gaps means the sender went quiet; a long gap
|
|
// followed by a burst of near-zero ones means OUR loop stalled and the
|
|
// packets queued up behind it. The two look identical from inside the
|
|
// dead reckoner and want opposite fixes.
|
|
//
|
|
Scalar
|
|
widestGap;
|
|
int
|
|
longGapCount,
|
|
queuedGapCount;
|
|
|
|
//
|
|
// The last position, velocity and timestamp the SENDER reported, kept
|
|
// so an arriving update can be scored against what the one before it
|
|
// predicted. All three come from the same machine, so the comparison
|
|
// owes nothing to latency or to clock alignment.
|
|
//
|
|
Time
|
|
senderStamp;
|
|
Point3D
|
|
senderPosition;
|
|
Vector3D
|
|
senderVelocity;
|
|
Logical
|
|
haveSenderSample;
|
|
|
|
Scalar
|
|
PredictUpdateInterval(Scalar latest);
|
|
void
|
|
ResetUpdateIntervals();
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Collision support
|
|
//
|
|
public:
|
|
virtual void
|
|
MoveCollisionVolume();
|
|
BoundingBoxTreeNode*
|
|
GetMoverCollisionRoot()
|
|
{Check(this); return containedByNode;}
|
|
|
|
BoxedSolidCollisionList*
|
|
AllocateCollisionList();
|
|
BoxedSolidCollisionList*
|
|
GetCurrentCollisions(BoxedSolidCollisionList *list=NULL);
|
|
BoxedSolid*
|
|
FindBoxedSolidHitBy(
|
|
Line *line,
|
|
Entity *except_by
|
|
);
|
|
BoxedSolidCollisionList*
|
|
CollideCenterOfMotion(
|
|
Line *line,
|
|
BoxedSolidCollisionList *list
|
|
);
|
|
void
|
|
ProcessCollisionList(
|
|
BoxedSolidCollisionList *collisions,
|
|
Scalar time_slice,
|
|
const Point3D &old_position,
|
|
Damage *damage
|
|
);
|
|
|
|
Scalar
|
|
StaticBounce(
|
|
const Point3D &old_position,
|
|
Scalar delta_t,
|
|
Scalar penetration,
|
|
const Normal &normal,
|
|
Scalar *elasticity,
|
|
Scalar bounce_min,
|
|
Scalar *friction
|
|
);
|
|
Scalar
|
|
DynamicBounce(
|
|
Mover *other,
|
|
Scalar delta_t,
|
|
Scalar penetration,
|
|
const Normal &normal,
|
|
Scalar *elasticity
|
|
);
|
|
|
|
BoxedSolid*
|
|
GetCollisionVolume()
|
|
{Check(this); return collisionVolume;}
|
|
BoxedSolid*
|
|
GetCollisionTemplate()
|
|
{Check(this); return collisionTemplate;}
|
|
int
|
|
GetCollisionVolumeCount()
|
|
{Check(this); return collisionVolumeCount;}
|
|
|
|
virtual void
|
|
StartCollisionAssistant();
|
|
|
|
protected:
|
|
int collisionVolumeCount;
|
|
BoxedSolid
|
|
*collisionVolume,
|
|
*collisionTemplate;
|
|
BoundingBoxTreeNode *containedByNode;
|
|
BoxedSolidCollisionList
|
|
*collisionLists,
|
|
*lastCollisionList;
|
|
CollisionAssistant *collisionAssistant;
|
|
|
|
virtual void
|
|
ProcessCollision(
|
|
Scalar time_slice,
|
|
BoxedSolidCollision &collision,
|
|
const Point3D &old_position,
|
|
Damage *damage
|
|
);
|
|
|
|
void
|
|
CheckAgainstBoxedSolidChain(
|
|
BoxedSolidCollisionList *collisions,
|
|
BoxedSolid *chain
|
|
);
|
|
|
|
public:
|
|
void
|
|
CheckVolumeAgainstBoxedSolidChain(
|
|
BoxedSolidCollisionList *collisions,
|
|
BoxedSolid *chain
|
|
);
|
|
|
|
protected:
|
|
BoxedSolid*
|
|
CheckLineAgainstBoxedSolidChain(
|
|
Line *line,
|
|
BoxedSolid *chain
|
|
);
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Flag Support
|
|
//
|
|
public:
|
|
enum {
|
|
NoCollisionVolumeBit = Entity::NextBit,
|
|
NoCollisionTestBit,
|
|
InitialStasisBit,
|
|
NextBit
|
|
};
|
|
|
|
enum {
|
|
NoCollisionVolumeFlag = 1<<NoCollisionVolumeBit,
|
|
NoCollisionTestFlag = 1<<NoCollisionTestBit,
|
|
InitialStasisFlag = 1<<InitialStasisBit,
|
|
DefaultFlags = DynamicFlag|MasterInstance
|
|
};
|
|
|
|
void
|
|
NoCollisionVolume()
|
|
{Check(this); simulationFlags |= NoCollisionVolumeFlag;}
|
|
void
|
|
SetCollisionVolume()
|
|
{Check(this); simulationFlags &= ~NoCollisionVolumeFlag;}
|
|
Logical
|
|
IsCollisionVolume()
|
|
{Check(this); return (simulationFlags&NoCollisionVolumeFlag) == 0;}
|
|
|
|
void
|
|
NoCollisionTest()
|
|
{Check(this); simulationFlags |= NoCollisionTestFlag;}
|
|
void
|
|
SetCollisionTest()
|
|
{Check(this); simulationFlags &= ~NoCollisionTestFlag;}
|
|
Logical
|
|
IsCollisionTestable()
|
|
{Check(this); return (simulationFlags&NoCollisionTestFlag) == 0;}
|
|
|
|
Logical
|
|
IsInitialStasis()
|
|
{Check(this); return (simulationFlags&InitialStasisFlag) != 0;}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Construction and Destruction
|
|
//
|
|
public:
|
|
typedef Mover__ModelResource ModelResource;
|
|
|
|
static Mover*
|
|
Make(MakeMessage *creation_message);
|
|
//
|
|
// Warning... This function requires a properly set up strtok!!!!
|
|
//
|
|
static Logical
|
|
CreateMakeMessage(
|
|
MakeMessage *creation_message,
|
|
NotationFile *model_file,
|
|
const ResourceDirectories *directories
|
|
);
|
|
static ResourceDescription::ResourceID
|
|
CreateModelResource(
|
|
ResourceFile *resource_file,
|
|
const char* model_name,
|
|
NotationFile *model_file,
|
|
const ResourceDirectories *directories,
|
|
ModelResource* model = NULL
|
|
);
|
|
|
|
Mover(
|
|
MakeMessage *creation_message,
|
|
SharedData &virtual_data
|
|
);
|
|
~Mover();
|
|
|
|
Logical
|
|
TestInstance() const;
|
|
};
|
|
|
|
//
|
|
// The replicant the RP412CAMLOG traces are describing, so the renderer can
|
|
// report on the same pod from the other end - what its motion looks like on
|
|
// screen. Null until a replicant has stepped at least once.
|
|
//
|
|
EntityID
|
|
MoverTracedEntity();
|