etqw-sdk/source/game/PredictionErrorDecay.cpp
2008-05-29 00:00:00 +00:00

1248 lines
41 KiB
C++

// Copyright (C) 2007 Id Software, Inc.
//
#include "precompiled.h"
#pragma hdrstop
#if defined( _DEBUG ) && !defined( ID_REDIRECT_NEWDELETE )
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
#include "PredictionErrorDecay.h"
#ifdef PREDICTION_ERROR_DECAY_DEBUG
idCVar net_debugPredictionError( "net_debugPredictionError", "0", CVAR_BOOL | CVAR_GAME | CVAR_CHEAT, "dump debugging information about the prediction error decay" );
int sdPredictionErrorDecay_Origin::pedDebugCount = 0;
int sdPredictionErrorDecay_Angles::pedDebugCount = 0;
#endif
idCVar net_predictionErrorDecay( "net_predictionErrorDecay", "1", CVAR_BOOL | CVAR_GAME | CVAR_NOCHEAT, "Enable/disable prediction error decay" );
idCVar net_limitApparentVelocity( "net_limitApparentVelocity", "1", CVAR_BOOL | CVAR_GAME | CVAR_NOCHEAT, "limit the apparent velocity of objects in prediction to realistic levels" );
idCVar net_limitApparentMaxLagAllowance( "net_limitApparentMaxLagAllowance", "0.25", CVAR_FLOAT | CVAR_GAME | CVAR_NOCHEAT, "fraction of the current physics speed added to the maximum apparent speed due to client lag" );
idCVar net_limitApparentMaxErrorAllowance( "net_limitApparentMaxErrorAllowance", "0.25", CVAR_FLOAT | CVAR_GAME | CVAR_NOCHEAT, "fraction of the current physics speed added to the maximum apparent speed due to client prediction error" );
idCVar net_limitApparentMinSpeed( "net_limitApparentMinSpeed", "200", CVAR_FLOAT | CVAR_GAME | CVAR_NOCHEAT, "minimum value for maximum apparent speed to reach" );
#define ENABLE_JP_FLOAT_CHECKS
#if defined( ENABLE_JP_FLOAT_CHECKS )
#undef FLOAT_CHECK_BAD
#undef VEC_CHECK_BAD
#define FLOAT_CHECK_BAD( x ) \
( FLOAT_IS_NAN( x ) || FLOAT_IS_INF( x ) || FLOAT_IS_IND( x ) || FLOAT_IS_DENORMAL( x ) )
// if ( FLOAT_IS_NAN( x ) || FLOAT_IS_INF( x ) || FLOAT_IS_IND( x ) || FLOAT_IS_DENORMAL( x ) ) \
// { \
// gameLocal.Warning( "Bad floating point number in %s: %i", __FILE__, __LINE__ ); \
// }
#define VEC_CHECK_BAD( vec ) ( FLOAT_CHECK_BAD( ( vec ).x ) || FLOAT_CHECK_BAD( ( vec ).y ) || FLOAT_CHECK_BAD( ( vec ).z ) )
#define MAT_CHECK_BAD( m ) ( VEC_CHECK_BAD( m[ 0 ] ) || VEC_CHECK_BAD( m[ 1 ] ) || VEC_CHECK_BAD( m[ 2 ] ) )
#else
#define MAT_CHECK_BAD( m )
#endif
const float MAX_PREDICTION_TIME_OFFSET = 3.f;
/*
===============================================================================
Quartic spline utility classes
===============================================================================
*/
class sdQuarticFunction {
public:
float A, B, C, D, E;
inline void Construct( float x0, float dx0, float ddx0, float x1, float dx1 ) {
E = x0;
D = dx0;
C = ddx0 * 0.5f;
B = 4.0f*x1 - 2.0f*C - 3.0f*D - 4.0f*E - dx1;
A = x1 - B - C - D - E;
}
inline float EvaluateX( float t, float t2, float t3, float t4 ) const {
return A*t4 + B*t3 + C*t2 + D*t + E;
}
inline float EvaluateDX( float t, float t2, float t3 ) const {
return 4.0f*A*t3 + 3.0f*B*t2 + 2.0f*C*t + D;
}
inline float EvaluateDDX( float t, float t2 ) const {
return 12.0f*A*t2 + 6.0f*B*t + 2.0f*C;
}
};
class sdQuarticSpline {
public:
sdQuarticFunction x, y, z;
inline void Construct( const idVec3& x0, const idVec3& dx0, const idVec3& ddx0, const idVec3& x1, const idVec3& dx1 ) {
x.Construct( x0.x, dx0.x, ddx0.x, x1.x, dx1.x );
y.Construct( x0.y, dx0.y, ddx0.y, x1.y, dx1.y );
z.Construct( x0.z, dx0.z, ddx0.z, x1.z, dx1.z );
}
inline idVec3 EvaluateX( float t, float t2, float t3, float t4 ) const {
return idVec3( x.EvaluateX( t, t2, t3, t4 ), y.EvaluateX( t, t2, t3, t4 ), z.EvaluateX( t, t2, t3, t4 ) );
}
inline idVec3 EvaluateDX( float t, float t2, float t3 ) const {
return idVec3( x.EvaluateDX( t, t2, t3 ), y.EvaluateDX( t, t2, t3 ), z.EvaluateDX( t, t2, t3 ) );
}
inline idVec3 EvaluateDDX( float t, float t2 ) const {
return idVec3( x.EvaluateDDX( t, t2 ), y.EvaluateDDX( t, t2 ), z.EvaluateDDX( t, t2 ) );
}
};
class sdQuarticAngleSpline {
public:
sdQuarticFunction pitch, yaw, roll;
idAngles baseAngles;
inline void Construct( const idAngles& x0, const idAngles& dx0, const idAngles& ddx0, const idAngles& x1, const idAngles& dx1 ) {
baseAngles = x0;
// angles wrap around
idAngles diffAngles = x1 - x0;
//diffAngles.Normalize180();
pitch.Construct( ang_zero.pitch, dx0.pitch, ddx0.pitch, diffAngles.pitch, dx1.pitch );
yaw.Construct( ang_zero.yaw, dx0.yaw, ddx0.yaw, diffAngles.yaw, dx1.yaw );
roll.Construct( ang_zero.roll, dx0.roll, ddx0.roll, diffAngles.roll, dx1.roll );
}
inline idAngles EvaluateX( float t, float t2, float t3, float t4 ) const {
return idAngles( pitch.EvaluateX( t, t2, t3, t4 ), yaw.EvaluateX( t, t2, t3, t4 ), roll.EvaluateX( t, t2, t3, t4 ) ) + baseAngles;
}
inline idAngles EvaluateDX( float t, float t2, float t3 ) const {
return idAngles( pitch.EvaluateDX( t, t2, t3 ), yaw.EvaluateDX( t, t2, t3 ), roll.EvaluateDX( t, t2, t3 ) );
}
inline idAngles EvaluateDDX( float t, float t2 ) const {
return idAngles( pitch.EvaluateDDX( t, t2 ), yaw.EvaluateDDX( t, t2 ), roll.EvaluateDDX( t, t2 ) );
}
};
/*
===============================================================================
sdPredictionErrorDecay_Origin
===============================================================================
*/
/*
===============
sdPredictionErrorDecay_Origin::sdPredictionErrorDecay_Origin
===============
*/
sdPredictionErrorDecay_Origin::sdPredictionErrorDecay_Origin( void ) {
lastReturnedOrigin.Zero();
lastReturnedVelocity.Zero();
lastReturnedAcceleration.Zero();
networkTime = 0;
networkOrigin.Zero();
lastNetworkTime = 0;
lastNetworkOrigin.Zero();
lastLastNetworkTime = 0;
lastLastNetworkOrigin.Zero();
networkPacketDelay = 0;
lastUpdateTime = 0;
lastDecayTime = 0;
isNew = true;
owner = NULL;
numDuplicates = 0;
lastDuplicateTime = 0;
isStationary = true;
#ifdef PREDICTION_ERROR_DECAY_DEBUG
debugFile = NULL;
#endif
}
/*
===============
sdPredictionErrorDecay_Origin::~sdPredictionErrorDecay_Origin
===============
*/
sdPredictionErrorDecay_Origin::~sdPredictionErrorDecay_Origin( void ) {
#ifdef PREDICTION_ERROR_DECAY_DEBUG
if ( debugFile != NULL ) {
fileSystem->CloseFile( debugFile );
}
#endif
}
/*
===============
sdPredictionErrorDecay_Origin::Init
===============
*/
void sdPredictionErrorDecay_Origin::Init( idEntity* _owner ) {
owner = _owner;
#ifdef PREDICTION_ERROR_DECAY_DEBUG
myIndex = -1;
if ( net_debugPredictionError.GetBool() && debugFile == NULL && _owner->entityNumber == 0 ) {
debugFile = fileSystem->OpenFileWrite( va( "ped_origin_%i.csv", pedDebugCount++ ) );
if ( debugFile != NULL ) {
myIndex = pedDebugCount - 1;
debugFile->Printf( "entNum,currentTime,prevTime,networkPacketDelay,timeSinceNewPacket," );
debugFile->Printf( "matchingTime," );
debugFile->Printf( "netOrigin.x,netOrigin.y,netOrigin.z,renderOrigin.x,renderOrigin.y,renderOrigin.z," );
debugFile->Printf( "resultOrigin.x,resultOrigin.y,resultOrigin.z," );
debugFile->Printf( "futureSpot.x,futureSpot.y,futureSpot.z," );
debugFile->Printf( "futureVelocity.x,futureVelocity.y,futureVelocity.z," );
debugFile->Printf( "lastNetworkTime,lastNetworkOrigin.x,lastNetworkOrigin.y,lastNetworkOrigin.z," );
debugFile->Printf( "lastLastNetworkTime,lastLastNetworkOrigin.x,lastLastNetworkOrigin.y,lastLastNetworkOrigin.z," );
debugFile->Printf( "\n" );
}
}
#endif
}
/*
===============
sdPredictionErrorDecay_Origin::Reset
===============
*/
void sdPredictionErrorDecay_Origin::Reset( const idVec3& newOrigin ) {
lastReturnedOrigin = newOrigin;
lastReturnedVelocity.Zero();
lastReturnedAcceleration.Zero();
networkTime = gameLocal.time;
networkOrigin = newOrigin;
lastNetworkTime = networkTime;
lastNetworkOrigin = networkOrigin;
lastLastNetworkTime = lastNetworkTime;
lastLastNetworkOrigin = lastNetworkOrigin;
numDuplicates = 0;
lastDuplicateTime = 0;
isStationary = true;
lastDecayTime = gameLocal.time - gameLocal.msec;
}
/*
===============
sdPredictionErrorDecay_Origin::Update
===============
*/
void sdPredictionErrorDecay_Origin::Update( const sdPredictionErrorDecay_Origin::CustomDecayInfo* customInfo ) {
lastUpdateTime = gameLocal.time;
bool hasLocalPhysics;
int prediction;
int packetSpread;
idVec3 origin;
if ( customInfo == NULL ) {
hasLocalPhysics = !owner->IsPhysicsInhibited();
prediction = networkSystem->ClientGetPrediction();
packetSpread = owner->aorPacketSpread;
origin = owner->GetPhysics()->GetOrigin();
} else {
hasLocalPhysics = customInfo->hasLocalPhysics;
prediction = customInfo->currentPrediction;
packetSpread = customInfo->packetSpread;
origin = customInfo->origin;
}
if ( hasLocalPhysics ) {
int timeSinceNewPacket = gameLocal.time - networkTime;
timeSinceNewPacket -= gameLocal.msec;
timeSinceNewPacket -= prediction;
int reAddTime = packetSpread + packetSpread / 5;
if ( reAddTime < gameLocal.msec * 4 ) {
reAddTime = gameLocal.msec * 4;
}
if ( timeSinceNewPacket > reAddTime ) {
// insert a new "packet" if its been too long and we're running physics locally
// its generally close enough that this all averages out
OnNewInfo( origin, gameLocal.time, customInfo );
}
}
#ifdef PREDICTION_ERROR_DECAY_DEBUG
if ( net_debugPredictionError.GetBool() && debugFile == NULL && owner->entityNumber == 0 ) {
debugFile = fileSystem->OpenFileWrite( va( "ped_origin_%i.csv", pedDebugCount++ ) );
if ( debugFile != NULL ) {
myIndex = pedDebugCount - 1;
debugFile->Printf( "entNum,currentTime,prevTime,networkPacketDelay,timeSinceNewPacket," );
debugFile->Printf( "matchingTime," );
debugFile->Printf( "netOrigin.x,netOrigin.y,netOrigin.z,renderOrigin.x,renderOrigin.y,renderOrigin.z," );
debugFile->Printf( "resultOrigin.x,resultOrigin.y,resultOrigin.z," );
debugFile->Printf( "futureSpot.x,futureSpot.y,futureSpot.z," );
debugFile->Printf( "futureVelocity.x,futureVelocity.y,futureVelocity.z," );
debugFile->Printf( "lastNetworkTime,lastNetworkOrigin.x,lastNetworkOrigin.y,lastNetworkOrigin.z," );
debugFile->Printf( "lastLastNetworkTime,lastLastNetworkOrigin.x,lastLastNetworkOrigin.y,lastLastNetworkOrigin.z," );
debugFile->Printf( "\n" );
}
}
if ( !net_debugPredictionError.GetBool() && debugFile != NULL ) {
fileSystem->CloseFile( debugFile );
debugFile = NULL;
}
#endif
}
/*
===============
sdPredictionErrorDecay_Origin::LimitApparentVelocity
===============
*/
float sdPredictionErrorDecay_Origin::LimitApparentVelocity( const idVec3& oldOrigin, const idVec3& idealNewOrigin, idVec3& newOrigin, const idVec3& physicsVelocity, int currentPrediction ) {
idVec3 apparentVelocity = ( newOrigin - oldOrigin ) / MS2SEC( gameLocal.msec );
float apparentSpeedSqr = apparentVelocity.LengthSqr();
float error = ( idealNewOrigin - oldOrigin ).Length();
float localSpeed = physicsVelocity.Length();
float speedAllowanceDueToLag = Min( localSpeed * net_limitApparentMaxLagAllowance.GetFloat(), 0.5f * currentPrediction * localSpeed / 350.0f );
float speedAllowanceDueToError = Min( localSpeed * net_limitApparentMaxErrorAllowance.GetFloat(), 2.0f * error * localSpeed / 350.0f );
float maxSpeed = localSpeed + speedAllowanceDueToLag + speedAllowanceDueToError;
maxSpeed = Max( net_limitApparentMinSpeed.GetFloat(), Min( localSpeed * 1.25f, maxSpeed ) );
if ( apparentSpeedSqr > Square( maxSpeed ) && apparentSpeedSqr > idMath::FLT_EPSILON ) {
// apparent velocity is too high
// try to tone that back linearly
float apparentSpeed = idMath::Sqrt( apparentSpeedSqr );
float lerpValue = maxSpeed / apparentSpeed;
newOrigin = Lerp( oldOrigin, newOrigin, lerpValue );
return lerpValue;
}
return 1.0f;
}
/*
===============
sdPredictionErrorDecay_Origin::Decay
===============
*/
void sdPredictionErrorDecay_Origin::Decay( idVec3& renderOrigin, const sdPredictionErrorDecay_Origin::CustomDecayInfo* customInfo ) {
//
// TWTODO: This whole function needs a good old cleanup!!
//
if ( IsNew() ) {
return;
}
assert( owner );
if ( !net_predictionErrorDecay.GetBool() ) {
Reset( renderOrigin );
return;
}
if ( gameLocal.time <= lastDecayTime || !gameLocal.isNewFrame || gameLocal.msec == 0 || !owner->fl.allowPredictionErrorDecay ) {
renderOrigin = lastReturnedOrigin;
return;
}
idVec3 predictedOrigin = renderOrigin;
//
// Collect information to be used
//
bool hasLocalPhysics;
bool isPlayer;
int currentPrediction;
int packetSpread;
bool boxDecayClip;
bool pointDecayClip;
bool heightMapDecayClip;
idVec3 limitVelocity;
bool hasGroundContacts;
float physicsCutoffSqr;
float ownerAorDistSqr;
if ( customInfo == NULL ) {
hasLocalPhysics = !owner->IsPhysicsInhibited();
isPlayer = owner->IsType( idPlayer::Type );
currentPrediction = networkSystem->ClientGetPrediction();
packetSpread = owner->aorPacketSpread;
boxDecayClip = ( owner->aorFlags & AOR_BOX_DECAY_CLIP ) != 0;
pointDecayClip = ( owner->aorFlags & AOR_POINT_DECAY_CLIP ) != 0;
heightMapDecayClip = ( owner->aorFlags & AOR_HEIGHTMAP_DECAY_CLIP ) != 0;
limitVelocity = owner->GetPhysics()->GetLinearVelocity();
hasGroundContacts = owner->GetPhysics()->HasGroundContacts();
physicsCutoffSqr = 1.0f;
ownerAorDistSqr = 0.0f;
if ( owner->aorLayout != NULL ) {
physicsCutoffSqr = owner->aorLayout->GetPhysicsCutoffSqr();
ownerAorDistSqr = owner->aorDistanceSqr;
}
} else {
// using custom info
hasLocalPhysics = customInfo->hasLocalPhysics;
isPlayer = customInfo->isPlayer;
currentPrediction = customInfo->currentPrediction;
packetSpread = customInfo->packetSpread;
boxDecayClip = customInfo->boxDecayClip;
pointDecayClip = customInfo->pointDecayClip;
heightMapDecayClip = customInfo->heightMapDecayClip;
limitVelocity = customInfo->limitVelocity;
hasGroundContacts = customInfo->hasGroundContacts;
physicsCutoffSqr = customInfo->physicsCutoffSqr;
ownerAorDistSqr = customInfo->ownerAorDistSqr;
}
if ( networkPacketDelay > 0 && networkTime != lastNetworkTime ) {
// TODO: clean up this timing stuff!
int timeSinceNewPacket = gameLocal.time - networkTime;
if ( timeSinceNewPacket < gameLocal.msec ) {
timeSinceNewPacket = gameLocal.msec;
}
int lerpDuration = packetSpread*2;
if ( lerpDuration < gameLocal.msec ) {
lerpDuration = gameLocal.msec;
}
int matchingTimeInt = gameLocal.msec * 3;
if ( matchingTimeInt < lerpDuration ) {
matchingTimeInt = lerpDuration;
}
float matchingTime = MS2SEC( matchingTimeInt );
//
// Predict a position and velocity for the future
//
idVec3 futureVelocity;
int timeOffset = timeSinceNewPacket;
idVec3 hasPhysicsVelocity = vec3_origin;
idVec3 noPhysicsVelocity = vec3_origin;
//
// estimate velocity from local physics
if ( hasLocalPhysics ) {
if ( !isStationary && timeSinceNewPacket + ( networkTime - lastNetworkTime ) > 0 ) {
if ( isPlayer && timeSinceNewPacket > 0 ) {
hasPhysicsVelocity = ( predictedOrigin - networkOrigin ) / MS2SEC( timeSinceNewPacket );
} else {
// velocity averaged over last couple of packets plus the prediction so far
hasPhysicsVelocity = ( predictedOrigin - lastNetworkOrigin ) / MS2SEC( timeSinceNewPacket + ( networkTime - lastNetworkTime ) );
}
} else {
hasPhysicsVelocity = ( predictedOrigin - networkOrigin ) / MS2SEC( gameLocal.msec * 4 );
timeOffset = gameLocal.msec * 5;
}
}
//
// estimate velocity from what has been sent on the network
{
idVec3 networkVelocity = ( networkOrigin - lastNetworkOrigin ) / MS2SEC( networkTime - lastNetworkTime );
if ( lastNetworkTime != lastLastNetworkTime ) {
idVec3 lastNetworkVelocity = ( lastNetworkOrigin - lastLastNetworkOrigin ) / MS2SEC( lastNetworkTime - lastLastNetworkTime );
float recentBiasValue = idMath::ClampFloat( 0.0f, 1.0f, timeSinceNewPacket / ( lerpDuration * 0.5f ) );
noPhysicsVelocity = Lerp( lastNetworkVelocity, networkVelocity, 0.5f + 0.5f * recentBiasValue );
idVec3 networkAcceleration = ( networkVelocity - lastNetworkVelocity ) / MS2SEC( 0.5f * ( ( networkTime + lastNetworkTime ) - ( lastNetworkTime + lastLastNetworkTime ) ) );
// decay networkAcceleration based on how long its been since the acceleration took place
networkAcceleration *= 1.0f - recentBiasValue;
noPhysicsVelocity += networkAcceleration * MS2SEC( timeSinceNewPacket );
} else {
noPhysicsVelocity = networkVelocity;
}
// scale back a bit for less overshoot
noPhysicsVelocity *= 0.6f;
// decay velocity down if timeSinceNewPacket is getting big
int velocityDecayStart = lerpDuration + lerpDuration / 2 + currentPrediction;
if ( timeSinceNewPacket > velocityDecayStart ) {
float scale = 1.0f - idMath::ClampFloat( 0.0f, 1.0f, ( timeSinceNewPacket - velocityDecayStart ) / ( lerpDuration * 0.5f ) );
noPhysicsVelocity *= scale;
}
}
// blend the two together based on distance to the physics cutoff point
// to smooth the change between prediction modes
float lerpToPhysicsCutoff;
if ( owner->aorLayout != NULL ) {
if ( physicsCutoffSqr > idMath::FLT_EPSILON ) {
lerpToPhysicsCutoff = idMath::ClampFloat( 0.0f, 1.0f, ownerAorDistSqr / physicsCutoffSqr );
futureVelocity = Lerp( hasPhysicsVelocity, noPhysicsVelocity, lerpToPhysicsCutoff );
} else {
lerpToPhysicsCutoff = 1.0f;
futureVelocity = noPhysicsVelocity;
}
} else {
if ( hasLocalPhysics ) {
lerpToPhysicsCutoff = 0.0f;
futureVelocity = hasPhysicsVelocity;
} else {
lerpToPhysicsCutoff = 1.0f;
futureVelocity = noPhysicsVelocity;
}
}
idVec3 futureOrigin = networkOrigin + futureVelocity * ( matchingTime + MS2SEC( timeOffset ) );
if ( !hasLocalPhysics && ( boxDecayClip || pointDecayClip || heightMapDecayClip ) ) {
// crude clipping to try to stop it dipping through the ground
idBounds bounds = owner->GetPhysics()->GetBounds();
idVec3 center = bounds.GetCenter();
idVec3 size = bounds.GetSize();
for ( int i = 0; i < 3; i++ ) {
if ( size[ i ] > 48.0f ) {
size[ i ] -= 16.0f;
}
}
bounds = idBounds( center - size * 0.5f, center + size * 0.5f );
idVec3 direction = futureOrigin - predictedOrigin;
float distance = direction.Normalize();
float sizeInDirection = idMath::Fabs( size * direction );
// limit the length of the trace
if ( distance > 512.0f ) {
distance = 512.0f;
}
idVec3 rotatedCenter = center * owner->GetPhysics()->GetAxis();
idVec3 start = rotatedCenter + predictedOrigin;
idVec3 end = start + distance * direction;
// TWTODO: use proper contents masks requested from the physics
trace_t trace;
if ( boxDecayClip ) {
gameLocal.clip.TraceBounds( CLIP_DEBUG_PARMS trace, start, end, bounds, owner->GetPhysics()->GetAxis(), CONTENTS_SOLID | CONTENTS_VEHICLECLIP, owner );
} else if ( pointDecayClip ) {
end += direction * sizeInDirection * 2.0f;
gameLocal.clip.TracePoint( CLIP_DEBUG_PARMS trace, start, end, CONTENTS_SOLID | CONTENTS_VEHICLECLIP, owner );
} else {
end += direction * sizeInDirection * 2.0f;
trace.fraction = 1.0f;
trace.endpos = end;
const sdPlayZone* playZoneHeight = gameLocal.GetPlayZone( start, sdPlayZone::PZF_HEIGHTMAP );
if ( playZoneHeight != NULL ) {
const sdHeightMapInstance& heightMap = playZoneHeight->GetHeightMap();
if ( heightMap.IsValid() ) {
trace.fraction = heightMap.TracePoint( start, end, trace.endpos );
trace.c.normal.Set( 0.0f, 0.0f, 1.0f );
}
}
}
if ( trace.fraction < 1.0f ) {
// scale back the velocity in the direction of the bump too
futureVelocity -= ( futureVelocity * trace.c.normal * 0.95f ) * trace.c.normal;
futureOrigin = networkOrigin + futureVelocity * ( matchingTime + MS2SEC( timeOffset ) );
}
}
//
// Construct & evaluate splines for output
//
float t = MS2SEC( gameLocal.msec ) / matchingTime;
float spreadBasedScaleUp = Lerp( 1.0f, 1.5f, ( 1000 - lerpDuration + 150 ) / 1000.0f );
t *= spreadBasedScaleUp;
if ( t > 0.5f ) {
t = 0.5f;
}
float t2 = t*t;
float t3 = t2*t;
float t4 = t2*t2;
sdQuarticSpline spline;
spline.Construct( lastReturnedOrigin, lastReturnedVelocity, lastReturnedAcceleration, futureOrigin, futureVelocity );
idVec3 tweenOrigin = spline.EvaluateX( t, t2, t3, t4 );
idVec3 tweenVelocity = spline.EvaluateDX( t, t2, t3 );
idVec3 tweenAcceleration = spline.EvaluateDDX( t, t2 );
//
// Try to prevent the apparent velocity being unrealistic
//
if ( net_limitApparentVelocity.GetBool() ) {
float originalTweenOriginZ = tweenOrigin.z;
float originalTweenVelocityZ = tweenVelocity.z;
float originalTweenAccelerationZ = tweenAcceleration.z;
// since this does a bunch of masking to cover up jerking, can blend in more of what is REALLY happening
tweenOrigin = Lerp( predictedOrigin, tweenOrigin, lerpToPhysicsCutoff );
float lerpValue = LimitApparentVelocity( lastReturnedOrigin, predictedOrigin, tweenOrigin,
limitVelocity, currentPrediction );
tweenVelocity = Lerp( lastReturnedVelocity, tweenVelocity, lerpValue );
tweenAcceleration = Lerp( lastReturnedAcceleration, tweenAcceleration, lerpValue );
if ( isPlayer ) {
// HACK: players don't limit z velocity as much - looks really weird when they jump
tweenOrigin.z = Lerp( tweenOrigin.z, originalTweenOriginZ, 0.75f );
tweenVelocity.z = Lerp( tweenVelocity.z, originalTweenVelocityZ, 0.75f );
tweenAcceleration.z = Lerp( tweenAcceleration.z, originalTweenAccelerationZ, 0.75f );
}
}
if ( hasLocalPhysics ) {
if ( hasGroundContacts ) {
if ( tweenOrigin.z < predictedOrigin.z ) {
tweenOrigin.z = predictedOrigin.z;
}
}
}
tweenOrigin.FixDenormals();
tweenVelocity.FixDenormals();
tweenAcceleration.FixDenormals();
// cache output
renderOrigin = tweenOrigin;
lastReturnedOrigin = tweenOrigin;
lastReturnedVelocity = tweenVelocity;
lastReturnedAcceleration = tweenAcceleration;
//
// Debugging info
//
#ifdef PREDICTION_ERROR_DECAY_DEBUG
if ( debugFile != NULL ) {
if ( owner != NULL ) {
debugFile->Printf( "%i,", owner->entityNumber );
} else {
debugFile->Printf( "-1," );
}
// timing
debugFile->Printf( "%i,%i,%i,%i,", gameLocal.time, networkTime, networkPacketDelay, timeSinceNewPacket );
debugFile->Printf( "%.2f,", matchingTime );
// origin
debugFile->Printf( "%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,", networkOrigin.x, networkOrigin.y, networkOrigin.z,
predictedOrigin.x, predictedOrigin.y, predictedOrigin.z );
debugFile->Printf( "%.2f,%.2f,%.2f,", renderOrigin.x, renderOrigin.y, renderOrigin.z );
debugFile->Printf( "%.2f,%.2f,%.2f,", futureOrigin.x, futureOrigin.y, futureOrigin.z );
debugFile->Printf( "%.2f,%.2f,%.2f,", futureVelocity.x, futureVelocity.y, futureVelocity.z );
debugFile->Printf( "%i,%.2f,%.2f,%.2f,", lastNetworkTime, lastNetworkOrigin.x, lastNetworkOrigin.y, lastNetworkOrigin.z );
debugFile->Printf( "%i,%.2f,%.2f,%.2f,", lastLastNetworkTime, lastLastNetworkOrigin.x, lastLastNetworkOrigin.y, lastLastNetworkOrigin.z );
debugFile->Printf( "\n" );
}
#endif
} else {
// do nothing - it hasn't calibrated itself appropriately yet
lastReturnedOrigin = renderOrigin;
}
lastDecayTime = gameLocal.time;
// gracefully handle bad values
// TWTODO: Proper fix!
if ( VEC_CHECK_BAD( renderOrigin ) ) {
gameLocal.Warning( "sdPredictionErrorDecay_Origin::Decay - Bad float!" );
assert( false );
renderOrigin = predictedOrigin;
Reset( renderOrigin );
}
}
/*
===============
sdPredictionErrorDecay_Origin::OnNewInfo
===============
*/
void sdPredictionErrorDecay_Origin::OnNewInfo( const idVec3& serverOrigin, int& time, const CustomDecayInfo* customInfo ) {
assert( owner );
if ( !net_predictionErrorDecay.GetBool() ) {
return;
}
int newDelay = gameLocal.time - networkTime;
int packetSpread = customInfo == NULL ? owner->aorPacketSpread : customInfo->packetSpread;
bool physicsInhibited = customInfo == NULL ? owner->IsPhysicsInhibited() : !customInfo->hasLocalPhysics;
if ( newDelay < packetSpread ) {
if ( physicsInhibited ) {
networkOrigin = serverOrigin;
networkTime = time;
}
return;
}
// see if it falls beneath the interesting threshold
float diffSqr = ( serverOrigin - networkOrigin ).LengthSqr();
if ( diffSqr < Square( 0.5f ) ) {
if ( diffSqr < idMath::FLT_EPSILON && time > lastDuplicateTime ) {
// its basically identical to the last origin!
numDuplicates++;
lastDuplicateTime = time;
if ( numDuplicates >= 2 ) {
// 2 duplicates - we must be stationary
isStationary = true;
}
}
networkOrigin = serverOrigin;
networkTime = time;
return;
}
// see if the distance is too far - ie, looks like its a respawn or teleport that hasn't been
// caught properly by a reset
idVec3 velocity = owner->GetPhysics()->GetLinearVelocity();
if ( customInfo != NULL ) {
velocity = customInfo->limitVelocity;
}
float thresholdDistanceSqr = velocity.LengthSqr() * Square( 4.0f );
thresholdDistanceSqr = Max( thresholdDistanceSqr, Square( 1024.0f ) );
float returnedDiffSqr = ( serverOrigin - lastReturnedOrigin ).LengthSqr();
if ( returnedDiffSqr > thresholdDistanceSqr ) {
Reset( serverOrigin );
return;
}
if ( isStationary ) {
newDelay = packetSpread;
isStationary = false;
numDuplicates = 0;
}
if ( newDelay > 0 ) {
// networkPacketDelay = ( newDelay + networkPacketDelay ) * 0.5f;
networkPacketDelay = newDelay;
}
// don't let the delay get too big
if ( networkPacketDelay > 1000 ) {
networkPacketDelay = 1000;
}
if ( isNew ) {
networkPacketDelay = 0;
isNew = false;
networkTime = time;
networkOrigin = serverOrigin;
lastNetworkTime = networkTime;
lastNetworkOrigin = networkOrigin;
lastLastNetworkTime = lastNetworkTime;
lastLastNetworkOrigin = lastNetworkOrigin;
} else {
lastLastNetworkTime = lastNetworkTime;
lastLastNetworkOrigin = lastNetworkOrigin;
lastNetworkTime = networkTime;
lastNetworkOrigin = networkOrigin;
networkTime = time;
networkOrigin = serverOrigin;
}
}
/*
===============
sdPredictionErrorDecay_Origin::NeedsUpdate
===============
*/
bool sdPredictionErrorDecay_Origin::NeedsUpdate( void ) {
return gameLocal.time >= lastDecayTime && !IsNew() && ( ( gameLocal.time - networkTime ) < SEC2MS( MAX_PREDICTION_TIME_OFFSET ) );
}
/*
===============================================================================
sdPredictionErrorDecay_Angles
===============================================================================
*/
/*
===============
sdPredictionErrorDecay_Angles::sdPredictionErrorDecay_Angles
===============
*/
sdPredictionErrorDecay_Angles::sdPredictionErrorDecay_Angles( void ) {
lastReturnedAngles.Zero();
lastReturnedAngVelocity.Zero();
lastReturnedAngAcceleration.Zero();
networkTime = 0;
networkAngles.Zero();
lastNetworkTime = 0;
lastNetworkAngles.Zero();
lastLastNetworkTime = 0;
lastLastNetworkAngles.Zero();
networkPacketDelay = 0;
lastUpdateTime = 0;
lastDecayTime = 0;
isNew = true;
owner = NULL;
numDuplicates = 0;
lastDuplicateTime = 0;
isStationary = true;
#ifdef PREDICTION_ERROR_DECAY_DEBUG
debugFile = NULL;
#endif
}
/*
===============
sdPredictionErrorDecay_Angles::~sdPredictionErrorDecay_Angles
===============
*/
sdPredictionErrorDecay_Angles::~sdPredictionErrorDecay_Angles( void ) {
#ifdef PREDICTION_ERROR_DECAY_DEBUG
if ( debugFile != NULL ) {
fileSystem->CloseFile( debugFile );
}
#endif
}
/*
===============
sdPredictionErrorDecay_Angles::Init
===============
*/
void sdPredictionErrorDecay_Angles::Init( idEntity* _owner ) {
owner = _owner;
#ifdef PREDICTION_ERROR_DECAY_DEBUG
debugFile = NULL;
if ( owner->IsType( idPlayer::Type ) && net_debugPredictionError.GetBool() ) {
debugFile = fileSystem->OpenFileWrite( va( "ped_angles_%i.csv", pedDebugCount++ ) );
}
if ( debugFile != NULL ) {
debugFile->Printf( "entNum,currentTime,prevTime,networkPacketDelay,timeSinceNewPacket," );
debugFile->Printf( "netAngles.pitch,netAngles.yaw,netAngles.roll,renderAngles.pitch,renderAngles.yaw,renderAngles.roll," );
debugFile->Printf( "resultAngles.pitch,resultAngles.yaw,resultAngles.roll," );
debugFile->Printf( "\n" );
}
#endif
}
/*
===============
sdPredictionErrorDecay_Angles::Reset
===============
*/
void sdPredictionErrorDecay_Angles::Reset( const idMat3& newAxis ) {
lastReturnedAngles = newAxis.ToAngles();
lastReturnedAngVelocity.Zero();
lastReturnedAngAcceleration.Zero();
networkTime = gameLocal.time;
networkAngles = newAxis.ToAngles();
lastNetworkTime = networkTime;
lastNetworkAngles = networkAngles;
lastLastNetworkTime = lastNetworkTime;
lastLastNetworkAngles = lastNetworkAngles;
numDuplicates = 0;
lastDuplicateTime = 0;
isStationary = true;
lastDecayTime = gameLocal.time - gameLocal.msec;
}
/*
===============
sdPredictionErrorDecay_Angles::Update
===============
*/
void sdPredictionErrorDecay_Angles::Update() {
lastUpdateTime = gameLocal.time;
/* bool hasLocalPhysics = !owner->IsPhysicsInhibited();
if ( hasLocalPhysics ) {
int timeSinceNewPacket = gameLocal.time - networkTime;
timeSinceNewPacket -= gameLocal.msec;
if ( timeSinceNewPacket > owner->aorPacketSpread + owner->aorPacketSpread / 5 ) {
// insert a new "packet" if its been too long and we're running physics locally
// its generally close enough that this all averages out
OnNewInfo( owner->GetPhysics()->GetAxis() );
}
}
*/
#ifdef PREDICTION_ERROR_DECAY_DEBUG
if ( net_debugPredictionError.GetBool() && debugFile == NULL ) {
debugFile = fileSystem->OpenFileWrite( va( "ped_angles_%i.csv", pedDebugCount++ ) );
if ( debugFile != NULL ) {
debugFile->Printf( "entNum,currentTime,prevTime,networkPacketDelay,timeSinceNewPacket," );
debugFile->Printf( "netAngles.pitch,netAngles.yaw,netAngles.roll,renderAngles.pitch,renderAngles.yaw,renderAngles.roll," );
debugFile->Printf( "resultAngles.pitch,resultAngles.yaw,resultAngles.roll," );
debugFile->Printf( "\n" );
}
}
if ( !net_debugPredictionError.GetBool() && debugFile != NULL ) {
fileSystem->CloseFile( debugFile );
debugFile = NULL;
}
#endif
}
/*
===============
sdPredictionErrorDecay_Angles::Decay
===============
*/
void sdPredictionErrorDecay_Angles::Decay( idMat3& renderAxis, bool yawOnly ) {
//
// TWTODO: This whole function needs a good old cleanup!!
//
if ( IsNew() ) {
return;
}
assert( owner );
if ( !net_predictionErrorDecay.GetBool() ) {
Reset( renderAxis );
return;
}
if ( gameLocal.time <= lastDecayTime || !gameLocal.isNewFrame || gameLocal.msec == 0 || !owner->fl.allowPredictionErrorDecay ) {
renderAxis = lastReturnedAngles.ToMat3();
return;
}
idMat3 predictedAxes = renderAxis;
idAngles predictedAngles = predictedAxes.ToAngles();
predictedAngles = networkAngles - ( networkAngles - predictedAngles ).Normalize180();
bool hasLocalPhysics = !owner->IsPhysicsInhibited();
if ( networkPacketDelay > 0 && networkTime != lastNetworkTime ) {
// TODO: clean up this timing stuff!
int timeSinceNewPacket = gameLocal.time - networkTime;
if ( timeSinceNewPacket < gameLocal.msec ) {
timeSinceNewPacket = gameLocal.msec;
}
int lerpDuration = owner->aorPacketSpread*2;
if ( lerpDuration < gameLocal.msec ) {
lerpDuration = gameLocal.msec;
}
int matchingTimeInt = gameLocal.msec * 3;
if ( matchingTimeInt < lerpDuration ) {
matchingTimeInt = lerpDuration;
}
float matchingTime = MS2SEC( matchingTimeInt );
//
// Predict an angles and angular velocity for the future
//
idAngles futureAngVelocity;
int timeOffset = timeSinceNewPacket;
idAngles hasPhysicsVelocity = ang_zero;
idAngles noPhysicsVelocity = ang_zero;
int currentPrediction = networkSystem->ClientGetPrediction();
//
// estimate velocity from local physics
if ( hasLocalPhysics ) {
if ( !isStationary && timeSinceNewPacket + ( networkTime - lastNetworkTime ) > 0 ) {
if ( owner->IsType( idPlayer::Type ) && timeSinceNewPacket > 0 ) {
hasPhysicsVelocity = ( predictedAngles - networkAngles) / MS2SEC( timeSinceNewPacket );
} else {
// velocity averaged over last couple of packets plus the prediction so far
hasPhysicsVelocity = ( predictedAngles - lastNetworkAngles ) / MS2SEC( timeSinceNewPacket + ( networkTime - lastNetworkTime ) );
}
} else {
hasPhysicsVelocity = ( predictedAngles - networkAngles ) / MS2SEC( gameLocal.msec * 4 );
timeOffset = gameLocal.msec * 5;
}
}
//
// estimate velocity from what has been sent on the network
{
idAngles networkAngVelocity = ( networkAngles - lastNetworkAngles ) / MS2SEC( networkTime - lastNetworkTime );
if ( lastNetworkTime != lastLastNetworkTime ) {
idAngles lastNetworkAngVelocity = ( lastNetworkAngles - lastLastNetworkAngles ) / MS2SEC( lastNetworkTime - lastLastNetworkTime );
float recentBiasValue = idMath::ClampFloat( 0.0f, 1.0f, timeSinceNewPacket / ( lerpDuration * 0.5f ) );
noPhysicsVelocity = Lerp( lastNetworkAngVelocity, networkAngVelocity, 0.5f + 0.5f * recentBiasValue );
idAngles networkAngAcceleration = ( networkAngVelocity - lastNetworkAngVelocity ) / MS2SEC( 0.5f * ( ( networkTime + lastNetworkTime ) - ( lastNetworkTime + lastLastNetworkTime ) ) );
// decay networkAngAcceleration based on how long its been since the acceleration took place
networkAngAcceleration *= 1.0f - recentBiasValue;
noPhysicsVelocity += networkAngAcceleration * MS2SEC( timeSinceNewPacket );
} else {
noPhysicsVelocity = networkAngVelocity;
}
// scale back a bit for less overshoot
noPhysicsVelocity.pitch *= 0.1f;
noPhysicsVelocity.yaw *= 0.4f;
noPhysicsVelocity.roll *= 0.1f;
// decay velocities down if timeSinceNewPacket is getting big
int velocityDecayStart = lerpDuration + lerpDuration / 2 + currentPrediction;
if ( timeSinceNewPacket > velocityDecayStart ) {
float scale = 1.0f - idMath::ClampFloat( 0.0f, 1.0f, ( timeSinceNewPacket - velocityDecayStart ) / ( lerpDuration * 0.5f ) );
noPhysicsVelocity *= scale;
}
}
// blend the two together based on distance to the physics cutoff point
// to smooth the change between prediction modes
if ( owner->aorLayout != NULL ) {
float physicsCutoffSqr = owner->aorLayout->GetPhysicsCutoffSqr();
if ( physicsCutoffSqr > idMath::FLT_EPSILON ) {
float lerpValue = idMath::ClampFloat( 0.0f, 1.0f, owner->aorDistanceSqr / physicsCutoffSqr );
futureAngVelocity = Lerp( hasPhysicsVelocity, noPhysicsVelocity, lerpValue );
} else {
futureAngVelocity = noPhysicsVelocity;
}
} else {
if ( hasLocalPhysics ) {
futureAngVelocity = hasPhysicsVelocity;
} else {
futureAngVelocity = noPhysicsVelocity;
}
}
idAngles futureAngles = networkAngles + futureAngVelocity * ( matchingTime + MS2SEC( timeOffset ) );
//
// Construct & evaluate splines for output
//
float t = MS2SEC( gameLocal.msec ) / matchingTime;
float spreadBasedScaleUp = Lerp( 1.0f, 1.5f, ( 1000 - lerpDuration + 150 ) / 1000.0f );
t *= spreadBasedScaleUp;
if ( t > 0.5f ) {
t = 0.5f;
}
float t2 = t*t;
float t3 = t2*t;
float t4 = t2*t2;
// angles
idAngles tweenAngles = ang_zero;
idAngles tweenAngVelocity = ang_zero;
idAngles tweenAngAcceleration = ang_zero;
if ( !yawOnly ) {
sdQuarticAngleSpline angSpline;
angSpline.Construct( lastReturnedAngles, lastReturnedAngVelocity, lastReturnedAngAcceleration, futureAngles, futureAngVelocity );
tweenAngles = angSpline.EvaluateX( t, t2, t3, t4 );
tweenAngVelocity = angSpline.EvaluateDX( t, t2, t3 );
tweenAngAcceleration = angSpline.EvaluateDDX( t, t2 );
} else {
// player only operates on yaw
float diffYaw = idMath::AngleNormalize180( futureAngles.yaw - lastReturnedAngles.yaw );
sdQuarticFunction yaw;
yaw.Construct( 0.0f, lastReturnedAngVelocity.yaw, lastReturnedAngAcceleration.yaw, diffYaw, futureAngVelocity.yaw );
tweenAngles.yaw = yaw.EvaluateX( t, t2, t3, t4 ) + lastReturnedAngles.yaw;
tweenAngVelocity.yaw = yaw.EvaluateDX( t, t2, t3 );
tweenAngAcceleration.yaw = yaw.EvaluateDDX( t, t2 );
}
tweenAngles.FixDenormals();
tweenAngVelocity.FixDenormals();
tweenAngAcceleration.FixDenormals();
// cache output
renderAxis = tweenAngles.ToMat3();
lastReturnedAngles = tweenAngles;
lastReturnedAngVelocity = tweenAngVelocity;
lastReturnedAngAcceleration = tweenAngAcceleration;
//
// Debugging info
//
#ifdef PREDICTION_ERROR_DECAY_DEBUG
if ( debugFile != NULL ) {
if ( owner != NULL ) {
debugFile->Printf( "%i,", owner->entityNumber );
} else {
debugFile->Printf( "-1," );
}
// timing
debugFile->Printf( "%i,%i,%i,%i,", gameLocal.time, networkTime, networkPacketDelay, timeSinceNewPacket );
// angles
idAngles renderAngles = renderAxis.ToAngles();
debugFile->Printf( "%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,", networkAngles.pitch, networkAngles.yaw, networkAngles.roll,
predictedAngles.pitch, predictedAngles.yaw, predictedAngles.roll );
debugFile->Printf( "%.2f,%.2f,%.2f,", renderAngles.pitch, renderAngles.yaw, renderAngles.roll );
debugFile->Printf( "\n" );
}
#endif
} else {
// do nothing - it hasn't calibrated itself appropriately yet
lastReturnedAngles = renderAxis.ToAngles();
}
lastDecayTime = gameLocal.time;
// gracefully handle bad values
// TWTODO: Proper fix!
if ( MAT_CHECK_BAD( renderAxis ) ) {
gameLocal.Warning( "sdPredictionErrorDecay_Angles::Decay - Bad float!" );
assert( false );
renderAxis = predictedAxes;
Reset( renderAxis );
}
}
/*
===============
sdPredictionErrorDecay_Angles::OnNewInfo
===============
*/
void sdPredictionErrorDecay_Angles::OnNewInfo( const idMat3& serverAxes ) {
assert( owner );
if ( !net_predictionErrorDecay.GetBool() ) {
return;
}
int newDelay = gameLocal.time - networkTime;
if ( newDelay < owner->aorPacketSpread ) {
if ( owner->IsPhysicsInhibited() ) {
networkAngles = serverAxes.ToAngles();
networkTime = gameLocal.time;
FixAngles();
}
return;
}
// see if it falls beneath the interesting threshold
idAngles serverAngles = serverAxes.ToAngles();
idAngles diffAngles = networkAngles - serverAngles;
diffAngles.Normalize180();
idVec3 diffAnglesVec( diffAngles[ 0 ], diffAngles[ 1 ], diffAngles[ 2 ] );
float diffSqr = diffAnglesVec.LengthSqr();
if ( diffSqr < Square( 0.05f ) ) {
if ( diffSqr < idMath::FLT_EPSILON && gameLocal.time > lastDuplicateTime ) {
// its basically identical to the last angles!
numDuplicates++;
lastDuplicateTime = gameLocal.time;
if ( numDuplicates >= 2 ) {
// 2 duplicates - we must be stationary
isStationary = true;
}
}
networkAngles = serverAngles;
networkTime = gameLocal.time;
FixAngles();
return;
}
if ( isStationary ) {
newDelay = owner->aorPacketSpread;
isStationary = false;
numDuplicates = 0;
}
if ( newDelay > 0 ) {
// networkPacketDelay = ( newDelay + networkPacketDelay ) * 0.5f;
networkPacketDelay = newDelay;
}
// don't let the delay get too big
if ( networkPacketDelay > 1000 ) {
networkPacketDelay = 1000;
}
if ( isNew ) {
networkPacketDelay = 0;
isNew = false;
networkTime = gameLocal.time;
networkAngles = serverAxes.ToAngles();
lastNetworkTime = networkTime;
lastNetworkAngles = networkAngles;
lastLastNetworkTime = lastNetworkTime;
lastLastNetworkAngles = lastNetworkAngles;
} else {
lastLastNetworkTime = lastNetworkTime;
lastLastNetworkAngles = lastNetworkAngles;
lastNetworkTime = networkTime;
lastNetworkAngles = networkAngles;
networkTime = gameLocal.time;
networkAngles = serverAxes.ToAngles();
FixAngles();
}
}
/*
===============
sdPredictionErrorDecay_Angles::FixAngles
===============
*/
void sdPredictionErrorDecay_Angles::FixAngles( void ) {
lastNetworkAngles = networkAngles - ( networkAngles - lastNetworkAngles ).Normalize180();
lastLastNetworkAngles = lastNetworkAngles - ( lastNetworkAngles - lastLastNetworkAngles ).Normalize180();
lastReturnedAngles = lastNetworkAngles - ( lastNetworkAngles - lastReturnedAngles ).Normalize180();
}
/*
===============
sdPredictionErrorDecay_Angles::NeedsUpdate
===============
*/
bool sdPredictionErrorDecay_Angles::NeedsUpdate( void ) {
return gameLocal.time >= lastDecayTime && !IsNew() && ( ( gameLocal.time - networkTime ) < SEC2MS( MAX_PREDICTION_TIME_OFFSET ) );
}