Rejecting the sender-stall theory was right: a gap of over a second is a death pause, not a tick. The remaining symptom is described precisely - a RHYTHMIC tick as a pod moves past the camera - and rhythm is the clue. A fixed period points at a cadence in our own code, because the network has no period. There are at least four candidates and they are only distinguishable by their interval: the 20ms physics step, the 30ms update rate, the sawtooth in the dead reckoner blend (percent climbs from 0.29 to 0.87 across each update interval as lastPerformance approaches nextUpdate, then resets), and the twenty-step quaternion renormalisation in Mover::BeginStep, which falls at 0.4s - a few times a second. So measure the interval rather than guess among them. Per frame, take the angle the traced pod subtends at the eye, flag the frames whose angular step is far above the running mean, and report the time BETWEEN those events. Angle rather than distance because a pod crossing the view moves far across the screen while barely changing range, which is the geometry the tick was reported in. Both samples come from one frame, marked by the eye's own frame counter rather than assumed from draw order, and the renderer reports on the same pod the mover trace describes - MoverTracedEntity now exposes that latch. Two traces about two different pods, or two different frames, would compare nothing; that mistake has already cost this investigation three wrong answers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
520 lines
11 KiB
C++
520 lines
11 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;
|
|
|
|
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();
|