Files
RP412/MUNGA/ENTITY.h
T
CydandClaude Opus 5 449ed5d297 Drawing interpolates across the fixed step
The simulation advances in whole 50 Hz steps and the renderer draws
whenever it can, so the drawn position only changed fifty times a second
and was held for however many frames fell inside a step. That is visible
as stepping, and it got WORSE the faster the machine: at 240 fps each
position is held for nearly five frames, which is why a fast PC looked
like a rabbit on crack while TARGETFPS=50 looked perfect. Matching the two
rates hid it, but locking the frame rate to the physics rate throws away
the entire point of having a fixed step.

So drawing now blends. Entity keeps renderPreviousOrigin - its origin at
the start of the step it is in, snapshotted by Mover::BeginStep - and
renderStepFraction, how far through that step the frame falls, which is
the leftover Entity::PerformAndWatch deliberately does not simulate.
GetRenderToWorld blends the two with Origin::Lerp, which already did
position and shortest-arc quaternion with normalisation.

It is RENDER ONLY. localOrigin and localToWorld are untouched, so physics,
collision, scoring, the nav map's queries and the network update records
all still see exact stepped values.

Three call sites. RootRenderable::Execute, which was the single place a
vehicle's transform reached the matrix stack - the renderable already ran
per frame and simply re-read a value that changed at the physics rate. The
eye needed its gate widened as well: it rebuilt the view only when
localToWorld CHANGED, so the world would have glided while the camera went
on stepping and the judder would have moved rather than gone. And a
teleport must stay a cut - that falls out free, because VTV::BeginStep
applies a scheduled respawn and THEN calls Mover::BeginStep, so the
snapshot lands post-teleport and the blend has nothing to travel.

The picture trails the simulation by up to one step, 20 ms at 50 Hz. That
is the standard price of interpolating rather than extrapolating, and much
the lesser evil: guessing forward overshoots and shimmers every time the
guess is corrected.

RP412INTERP=0 turns it off so the stepping can be seen again without a
rebuild.

Determinism proved rather than asserted: a scripted lap (RP412INPUTSCRIPT,
throttle and steering and pitch) at 240 fps with interpolation on and off,
90 PHYSTRACE samples over 22 seconds of driving, zero differ. Two earlier
attempts at that comparison were invalid and both were my method - the
first did not pin RP412SPAWNZONE so the runs began on different pads, and
the second had no input script, so a joystick sitting on the desk drove
the two runs differently. The template warns about the first of those in
as many words.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 23:14:39 -05:00

666 lines
16 KiB
C++

#pragma once
#include "simulate.h"
#include "origin.h"
#include "linmtrx.h"
class Player;
class NotationFile;
class DamageZone;
class Damage;
class Subsystem;
#define ENTITY_CONTINUATION
#include "entity2.h"
//##########################################################################
//########################### Entity #################################
//##########################################################################
class Entity:
public Simulation
{
friend class Entity__StaticVideoSocketIterator;
friend class Entity__DynamicVideoSocketIterator;
friend class Entity__AudioSocketIterator;
friend class Entity__GaugeSocketIterator;
friend class Entity__AudioLocationIterator;
//##########################################################################
// Shared Data support
//
public:
typedef Entity__SharedData SharedData;
SharedData*
GetSharedData();
static Derivation *GetClassDerivations();
static SharedData DefaultData;
//##########################################################################
// Message Support
//
public:
enum {
MakeMessageID = Simulation::NextMessageID,
MakeReadyMessageID,
RemakeEntityMessageID,
RemakeReadyMessageID,
RemakeCompleteMessageID,
RemakeIncompleteMessageID,
DestroyEntityMessageID,
TransferEntityMessageID,
TransferCompleteMessageID,
TransferIncompleteMessageID,
BecomeInterestingMessageID,
BecomeUninterestingMessageID,
UpdateMessageID,
SubscribeReplicantMessageID,
UnsubscribeReplicantMessageID,
TakeDamageMessageID,
TakeDamageStreamMessageID,
PlayerLinkMessageID,
NextMessageID
};
typedef Entity__Message Message;
typedef Entity__MakeMessage MakeMessage;
typedef Entity__MakeReadyMessage MakeReadyMessage;
typedef Entity__RemakeEntityMessage RemakeEntityMessage;
typedef Entity__RemakeReadyMessage RemakeReadyMessage;
typedef Entity__RemakeCompleteMessage RemakeCompleteMessage;
typedef Entity__RemakeIncompleteMessage RemakeIncompleteMessage;
typedef Entity__DestroyEntityMessage DestroyEntityMessage;
typedef Entity__TransferEntityMessage TransferEntityMessage;
typedef Entity__TransferCompleteMessage TransferCompleteMessage;
typedef Entity__TransferIncompleteMessage TransferIncompleteMessage;
typedef Entity__BecomeInterestingMessage BecomeInterestingMessage;
typedef Entity__BecomeUninterestingMessage BecomeUninterestingMessage;
typedef Entity__UpdateMessage UpdateMessage;
typedef Entity__SubscribeReplicantMessage SubscribeReplicantMessage;
typedef Entity__UnsubscribeReplicantMessage UnsubscribeReplicantMessage;
typedef Entity__TakeDamageMessage TakeDamageMessage;
typedef Entity__TakeDamageStreamMessage TakeDamageStreamMessage;
typedef Entity__PlayerLinkMessage PlayerLinkMessage;
typedef Entity__DynamicMessage DynamicMessage;
typedef Entity__MakeMapMessage MakeMapMessage;
void
Receive(Event *event);
void
LocalDispatch(Message *what)
{Receiver::Receive(Cast_Object(Receiver::Message*,what));}
void
Dispatch(Receiver::Message *what);
void
DispatchToReplicant(
Message *what,
HostID host
);
void
DispatchToReplicants(Message *what);
private:
static const HandlerEntry MessageHandlerEntries[];
protected:
//static MessageHandlerSet MessageHandlers;
static MessageHandlerSet& GetMessageHandlers();
//##########################################################################
// Attribute Support
//
public:
enum {
LocalToWorldAttributeID = Simulation::NextAttributeID,
LocalOriginAttributeID,
DamageZoneCountAttributeID,
DamageZonesAttributeID,
NextAttributeID
};
private:
static const IndexEntry AttributePointers[];
protected:
//static AttributeIndexSet AttributeIndex
static AttributeIndexSet& GetAttributeIndex();
//
// public attribute declarations go here
//
public:
LinearMatrix localToWorld;
Origin localOrigin;
int damageZoneCount;
DamageZone **damageZones;
//######################################################################
//################### Render interpolation #########################
//######################################################################
//
// The simulation advances in whole fixed steps (RP412PHYSICSHZ) and the
// renderer draws whenever it can, so at any frame rate that is not the
// step rate the drawn position only changes 50 times a second and is
// held for however many frames fall inside a step. That is visible as
// stepping, and it gets WORSE the faster the machine: at 240 fps each
// position is held for nearly five frames.
//
// So drawing interpolates. renderPreviousOrigin is this entity's origin
// at the START of the step it is currently in, snapshotted by
// Mover::BeginStep, and renderStepFraction is how far through that step
// the frame being drawn falls. GetRenderToWorld blends the two.
//
// RENDER ONLY. localOrigin and localToWorld are untouched, so physics,
// collision, scoring, the nav map's entity queries and the network
// update records all keep seeing exact stepped values - which is what
// keeps the simulation identical on every machine at every frame rate.
// RP412PHYSTRACE is the proof of that and must not move.
//
// The picture therefore trails the simulation by up to one step (20 ms
// at 50 Hz), which is the standard price and much the lesser evil:
// extrapolating FORWARD instead has to guess, and overshoots into
// shimmer every time the guess is corrected.
//
// A teleport must not be smoothed - sliding a pod 300 metres across the
// map over 20 ms would be far worse than the cut it replaces. That
// falls out for free: VTV::BeginStep applies a scheduled respawn and
// THEN calls Mover::BeginStep, so the snapshot is taken after the
// teleport and the blend has nothing to travel.
//
Origin renderPreviousOrigin;
Scalar renderStepFraction;
//
// The transform to DRAW with. Falls back to localToWorld verbatim when
// interpolation is off, when there is no fixed step to interpolate
// within, or before the first snapshot exists - so the unfixed-step
// path behaves exactly as it always did.
//
void
GetRenderToWorld(LinearMatrix *out);
int
GetDamageZoneIndex(const CString &damage_zone_name) const;
//
// virtual access
//
public:
virtual Vector3D
GetWorldLinearVelocity()
{return Vector3D(0.0f, 0.0f, 0.0f);}
virtual Vector3D
GetWorldLinearAcceleration()
{return Vector3D(0.0f, 0.0f, 0.0f);}
//##########################################################################
// Model Support
//
public:
UpdateMessage*
Execute(const Time &till);
Subsystem*
GetSubsystem(int index)
{
Check(this); Verify((unsigned)index < subsystemCount);
return subsystemArray[index];
}
Simulation*
GetSimulation(int index);
int
GetSubsystemCount()
{Check(this); return subsystemCount;}
Subsystem*
FindSubsystem(const char* name);
typedef void
(Entity::*Performance)(Scalar time_slice);
typedef Entity__UpdateRecord UpdateRecord;
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
enum {
EntitySubsystemID = -1
};
static int
FindSubsytemID(
const char *model_name,
const char *subsystem_name
);
public:
enum {
DamageZoneUpdateModelBit = Simulation::NextUpdateModelBit,
NextUpdateModelBit
};
enum {
DamageZoneUpdateModelFlag = 1 << DamageZoneUpdateModelBit
};
Logical DamageZoneUpdateModelFlagSet()
{
Check(this);
return ((updateModel & DamageZoneUpdateModelFlag) != 0);
}
protected:
void
WriteUpdateRecord(
Simulation::UpdateRecord *message,
int update_model
);
void
UpdateMessageHandler(UpdateMessage *message);
void
ReadUpdateRecord(Simulation::UpdateRecord *message);
void
ReadDamageUpdateRecord(Simulation::UpdateRecord *update_record);
void
WriteDamageUpdateRecord(Simulation::UpdateRecord *update_record);
void
PerformAndWatch(
const Time &till,
MemoryStream *update_stream
);
int
subsystemCount;
Subsystem
**subsystemArray;
Origin
updateOrigin;
void
TakeDamageStreamMessageHandler(TakeDamageStreamMessage *message);
void
TakeDamageMessageHandler(TakeDamageMessage *message);
void
PlayerLinkMessageHandler(PlayerLinkMessage *message);
//##########################################################################
// Renderer Support
//
public:
void
AddStaticVideoComponent(Component *component);
void
AddDynamicVideoComponent(Component *component);
void
AddAudioComponent(Component *component);
typedef Entity__StaticVideoSocketIterator
StaticVideoSocketIterator;
typedef Entity__DynamicVideoSocketIterator
DynamicVideoSocketIterator;
typedef Entity__AudioSocketIterator
AudioSocketIterator;
virtual Enumeration
GetAudioRepresentation(Entity *linked_entity);
private:
SChainOf<Component*>
staticVideoSocket;
SChainOf<Component*>
dynamicVideoSocket;
SChainOf<Component*>
audioSocket;
//##########################################################################
// Flag Support
//
public:
//
// MasterInstance - created on owning host, replicated on remote hosts,
// sends updates to replicants
// ReplicantInstance - created on remote hosts, receives updates from
// MasterInstance
// IndependantInstance - created on owning host, replicated on remote
// hosts, never receives updates from MasterInstance
// HermitInstance - created on owning host, is not replicated, does
// send updates
//
enum {
InstanceBits = Simulation::NextBit,
ValidBit = InstanceBits+2,
TransferableBit,
InterestBit,
InterestLockedBit,
DynamicBit,
TrappedBit,
StatueBit,
MapBit,
PreRunBit,
CondemnedBit,
NextBit
};
enum Instance
{
MasterInstance=0,
ReplicantInstance=1<<InstanceBits,
IndependantInstance=2<<InstanceBits,
HermitInstance=3<<InstanceBits
};
enum {
InstanceMask = HermitInstance,
ValidFlag = 1<<ValidBit,
TransferableFlag = 1<<TransferableBit,
InterestFlag = 1<<InterestBit,
InterestLockedFlag = 1<<InterestLockedBit,
DynamicFlag = 1<<DynamicBit,
TrappedFlag = 1<<TrappedBit,
StatueFlag = 1<<StatueBit,
MapFlag = 1<<MapBit,
PreRunFlag = 1<<PreRunBit,
CondemnedFlag = 1<<CondemnedBit,
TypeMask = DynamicFlag|TrappedFlag|MapFlag,
// DefaultFlags = MasterInstance
DefaultFlags = DynamicFlag|MasterInstance
};
enum Type {
StaticType = 0,
DynamicType = DynamicFlag,
TrappedType = DynamicFlag|TrappedFlag,
StatueType = DynamicFlag|TrappedFlag|StatueFlag,
MapType = DynamicFlag|MapFlag,
TrappedMapType = DynamicFlag|TrappedFlag|MapFlag,
};
//##########################################################################
// Instance Type support
//
public:
void
SetInstance(Instance type)
{Check(this); simulationFlags &= ~InstanceMask; simulationFlags |= type;}
Instance
GetInstance()
{Check(this); return (Instance)(simulationFlags&InstanceMask);}
static LWord
EntityFlagsSetInstance(LWord entity_flags, Instance type)
{
entity_flags &= ~InstanceMask;
entity_flags |= type;
return entity_flags;
}
static Instance
EntityFlagsGetInstance(LWord entity_flags)
{return (Instance)(entity_flags&InstanceMask);}
//##########################################################################
// Validity Support
//
public:
void
SetValidFlag()
{Check(this); simulationFlags |= ValidFlag;}
void
SetInvalidFlag()
{Check(this); simulationFlags &= ~ValidFlag;}
Logical
IsValid()
{Check(this); return (simulationFlags&ValidFlag) != 0;}
void
SetPreRunFlag()
{Check(this); simulationFlags |= PreRunFlag;}
void
SetNoPreRunFlag()
{Check(this); simulationFlags &= ~PreRunFlag;}
Logical
IsPreRunnable()
{Check(this); return (simulationFlags&PreRunFlag) != 0;}
void
SetCondemnedFlag()
{Check(this); simulationFlags |= CondemnedFlag;}
Logical
IsCondemned()
{Check(this); return (simulationFlags&CondemnedFlag) != 0;}
//##########################################################################
// Transference Support
//
public:
void
SetTransferableFlag()
{Check(this); simulationFlags |= TransferableFlag;}
void
SetNontransferableFlag()
{Check(this); simulationFlags &= ~TransferableFlag;}
Logical
IsTransferable()
{Check(this); return (simulationFlags&TransferableFlag) != 0;}
const EntityID&
GetEntityID()
{return entityID;}
HostID
GetOwnerID()
{return ownerID;}
EntityID
entityID;
HostID
ownerID;
Player
*playerLink;
Player*
GetPlayerLink()
{return playerLink;}
//##########################################################################
// Interest Support
//
public:
void
BecomeInteresting();
void
BecomeInterestingMessageHandler(const Receiver::Message*);
void
BecomeUninterestingMessageHandler();
Logical
IsInteresting()
{Check(this); return interestCount != 0;}
void
SetInterestLockedFlag();
void
SetInterestUnlockedFlag();
Logical
IsInterestLocked()
{Check(this); return (simulationFlags&InterestLockedFlag) != 0;}
void
SetInterestZoneID(InterestZoneID interest_zone_ID)
{interestZoneID = interest_zone_ID;}
InterestZoneID
GetInterestZoneID()
{return interestZoneID;}
Logical
IsStatic()
{Check(this); return (simulationFlags&DynamicFlag) == 0;}
Logical
IsDynamic()
{Check(this); return (simulationFlags&DynamicFlag) != 0;}
void
SetTrappedFlag()
{Check(this); simulationFlags |= TrappedFlag;}
Logical
IsTrapped()
{Check(this); return (simulationFlags&TrappedFlag) != 0;}
Logical
IsStatue()
{Check(this); return (simulationFlags&StatueFlag) != 0;}
Logical
IsMap()
{Check(this); return (simulationFlags&MapFlag) != 0;}
static Logical
EntityFlagsIsMap(LWord entity_flags)
{return (entity_flags&MapFlag) != 0;}
Type
GetType()
{Check(this); return (Type)(simulationFlags&TypeMask);}
virtual Enumeration
GetInterestPriority(Entity *linked_entity);
InterestZoneID
interestZoneID;
int
interestCount;
Time
creationTime;
//##########################################################################
// Camera Support
//
protected:
Origin
cameraOffset;
public:
Origin
GetCameraOffset() const
{Check(this); return cameraOffset; }
//##########################################################################
// Scoring support
//
public:
//
// HACK - ECH 7/6/95 - Allow the player vehicle to respond to score
// messages, allows attribute system to be used for scoring
//
virtual void
RespondToScoreMessage(Message *message) {}
//##########################################################################
// Construction and Destruction
//
public:
typedef Entity* (*MakeHandler)(MakeMessage *);
static Entity*
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 Logical
CreateMakeMapMessage(
MakeMapMessage *creation_message,
ResourceDescription::ResourceID resource_id,
NotationFile *model_file,
const ResourceDirectories * //directories
);
static ResourceDescription::ResourceID
CreateDamageZoneStream(
ResourceFile *resource_file,
const char *model_name,
NotationFile *model_file,
const ResourceDirectories *directories
);
static ResourceDescription::ResourceID
CreateExplosionTableStream(
ResourceFile *resource_file,
const char *model_name,
NotationFile *model_file,
const ResourceDirectories *directories
);
ResourceDescription::ResourceID
GetResourceID()
{Check(this); return resourceID;}
Player*
GetOwningPlayer()
{Check(this); return owningPlayer;}
void
SetupNetworkMessage(Entity::Message *message);
protected:
Entity(
MakeMessage *creation_message,
SharedData &virtual_data
);
ResourceDescription::ResourceID
resourceID;
Player
*owningPlayer;
void
DestroyEntityMessageHandler(Message *message);
public:
~Entity();
void
CondemnToDeathRow();
//##########################################################################
// Test Support
//
public:
Logical
TestInstance() const;
static Logical
TestClass();
};
inline void
Entity::SetupNetworkMessage(Entity::Message *message)
{
Check(this);
Check(message);
message->entityID = entityID;
message->interestZoneID = interestZoneID;
message->ownerID = ownerID;
}
#include "entity3.h"
#undef ENTITY_CONTINUATION