// 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 ) ); }