Files
RP412/MUNGA/MOVER.h
T
CydandClaude Fable 5 5e47987508 The simulation steps at a fixed rate
RP412PHYSICSHZ names a rate and the simulation advances in whole steps
of exactly that size on every machine, whatever the display does. 0 -
the default, and the shipped behaviour until the play testers have
spoken - is the game as it has always run: the step is however long the
last frame took, which makes the frame rate part of the physics.
Measured over two seconds of free fall, a 30 fps machine's pod fell
three times further than a 144 fps machine's. Two players on the same
track were not in the same gravity.

With a rate set, the same race is bit-identical across frame rates:
30, 60 and 144 fps produce the same trajectory to the last printed
digit, and identical runs reproduce exactly - which was never true of
this engine before, at any frame rate.

It took three pieces, and every one was found by measuring, not by
reading:

- Simulation::PerformTo turns lastPerformance into the accumulator it
  always secretly was: whole steps while time remains, the remainder
  carried to the next frame. Watchers and update records stay once per
  frame - stepping is physics, watching is I/O.

- Entity::PerformAndWatch interleaves subsystems and entity per STEP.
  The frame loop ran all subsystems to the frame boundary and then the
  entity, indistinguishable from correct at one step per frame - which
  is why thirty years of code never noticed - and wrong at two: the
  thrusters raycast twice from a vehicle that had not moved, and the
  hover spring fired twice on one stale height sample. The subsystems
  are also snapped onto their entity's step grid; each Simulation
  anchors its grid at its own creation time, a per-run phase no seed
  could pin.

- Mover::BeginStep clears the force accumulator per step. It was
  cleared once per frame while the thrusters ADD per step, so step two
  of a frame integrated step one's thrust again - and how many steps a
  frame holds rides on wall-clock jitter, which is why identical
  configs measured a quarter-metre apart. The quaternion renormalise
  counts steps now too, for the same reason.

The catch-up clamp is a quarter second of simulation whatever the rate,
so a machine that cannot keep up slows down rather than seizing, and
does so identically everywhere. The engine's clock counts milliseconds,
so rates that do not divide 1000 - 60 among them - quietly run at the
neighbouring millisecond step; the log now says so and names the exact
ones. 25, 50 and 100 are exact, and all three are verified bit-identical
across frame rates and across runs.

Verified for a single vehicle settling under gravity and hover. Driving,
collisions and the network are the next frontiers, in that order: the
collision path writes the victim's state with wall-clock stamps and a
hard-coded 0.1 s bounce, which single-player survives and lockstep will
not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 18:19:43 -05:00

472 lines
9.9 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;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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;
};