@@ -1,26 +1,62 @@
// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved.
// Interpolation and prediction logic based on https://www.gabrielgambetta.com/client-server-game-architecture.html
# include "NetworkTransform.h"
# include "Engine/Core/Math/Transform.h"
# include "Engine/Engine/Time.h"
# include "Engine/Level/Actor.h"
# include "Engine/Networking/NetworkManager.h"
# include "Engine/Networking/NetworkPeer.h"
# include "Engine/Networking/NetworkReplicator.h"
# include "Engine/Networking/NetworkStream.h"
# include "Engine/Networking/NetworkStats.h"
# include "Engine/Networking/INetworkDriver.h"
# include "Engine/Networking/NetworkRpc.h"
PACK_STRUCT ( struct Data
{
uint8 LocalSpace : 1 ;
NetworkTransform : : SyncModes SyncMo de : 9 ;
uint8 HasSequenceIn dex : 1 ;
NetworkTransform : : ReplicationComponents Components : 9 ;
} ) ;
static_assert ( ( int32 ) NetworkTransform : : SyncMode s: : All + 1 = = 512 , " Invalid SyncMode s bit count for Data. " ) ;
static_assert ( ( int32 ) NetworkTransform : : ReplicationComponent s: : All + 1 = = 512 , " Invalid ReplicationComponent s bit count for Data. " ) ;
namespace
{
// Percentage of local error that is acceptable (eg. 4 frames error)
constexpr float Precision = 4.0f ;
template < typename T >
FORCE_INLINE bool IsWithinPrecision ( const Vector3Base < T > & currentDelta , const Vector3Base < T > & targetDelta )
{
const T targetDeltaMax = targetDelta . GetAbsolute ( ) . MaxValue ( ) ;
return targetDeltaMax > ( T ) ZeroTolerance & & currentDelta . GetAbsolute ( ) . MaxValue ( ) < targetDeltaMax * ( T ) Precision ;
}
}
NetworkTransform : : NetworkTransform ( const SpawnParams & params )
: Script ( params )
{
// TODO: don't tick when using Default mode or with OwnedAuthoritative role to optimize cpu perf OR introduce TaskGraphSystem to batch NetworkTransform updates over Job System
_tickUpdate = 1 ;
}
void NetworkTransform : : SetSequenceIndex ( uint16 value )
{
NETWORK_RPC_IMPL ( NetworkTransform , SetSequenceIndex , value ) ;
_currentSequenceIndex = value ;
}
void NetworkTransform : : OnEnable ( )
{
// Initialize state
_bufferHasDeltas = false ;
_currentSequenceIndex = 0.0f ;
_lastFrameTransform = GetActor ( ) ? GetActor ( ) - > GetTransform ( ) : Transform : : Identity ;
_buffer . Clear ( ) ;
// Register for replication
NetworkReplicator : : AddObject ( this ) ;
}
@@ -29,6 +65,81 @@ void NetworkTransform::OnDisable()
{
// Unregister from replication
NetworkReplicator : : RemoveObject ( this ) ;
_buffer . Resize ( 0 ) ;
}
void NetworkTransform : : OnUpdate ( )
{
// TODO: cache role in Deserialize to improve cpu perf
const NetworkObjectRole role = NetworkReplicator : : GetObjectRole ( this ) ;
if ( role = = NetworkObjectRole : : OwnedAuthoritative )
return ; // Ignore itself
if ( Mode = = ReplicationModes : : Default )
{
// Transform replicated in Deserialize
}
else if ( role = = NetworkObjectRole : : ReplicatedSimulated & & Mode = = ReplicationModes : : Prediction )
{
// Compute delta of the actor transformation simulated locally
const Transform thisFrameTransform = GetActor ( ) ? GetActor ( ) - > GetTransform ( ) : Transform : : Identity ;
Transform delta = thisFrameTransform - _lastFrameTransform ;
if ( ! delta . IsIdentity ( ) )
{
// Move to the next input sequence number
_currentSequenceIndex + + ;
// Add delta to buffer to re-apply after receiving authoritative transform value
if ( ! _bufferHasDeltas )
{
_buffer . Clear ( ) ;
_bufferHasDeltas = true ;
}
delta . Orientation = thisFrameTransform . Orientation ; // Store absolute orientation value to prevent jittering when blending rotation deltas
_buffer . Add ( { 0.0f , _currentSequenceIndex , delta , } ) ;
// Inform server about sequence number change (add offset to lead before server data)
SetSequenceIndex ( _currentSequenceIndex - 1 ) ;
}
_lastFrameTransform = thisFrameTransform ;
}
else
{
float lag = 0.0f ;
// TODO: use lag from last used NetworkStream context
if ( NetworkManager : : Peer & & NetworkManager : : Peer - > NetworkDriver )
{
// Use lag from the RTT between server and the client
const auto stats = NetworkManager : : Peer - > NetworkDriver - > GetStats ( ) ;
lag = stats . RTT / 2000.0f ;
}
else
{
// Default lag is based on the network manager update rate
const float fps = NetworkManager : : NetworkFPS ;
lag = 1.0f / fps ;
}
// Find the two authoritative positions surrounding the rendering timestamp
const float now = Time : : Update . UnscaledTime . GetTotalSeconds ( ) ;
const float gameTime = now - lag ;
// Drop older positions
while ( _buffer . Count ( ) > = 2 & & _buffer [ 1 ] . Timestamp < = gameTime )
_buffer . RemoveAtKeepOrder ( 0 ) ;
// Interpolate between the two surrounding authoritative positions
if ( _buffer . Count ( ) > = 2 & & _buffer [ 0 ] . Timestamp < = gameTime & & gameTime < = _buffer [ 1 ] . Timestamp )
{
const auto & b0 = _buffer [ 0 ] ;
const auto & b1 = _buffer [ 1 ] ;
Transform transform ;
const float alpha = ( gameTime - b0 . Timestamp ) / ( b1 . Timestamp - b0 . Timestamp ) ;
Transform : : Lerp ( b0 . Value , b1 . Value , alpha , transform ) ;
Set ( transform ) ;
}
}
}
void NetworkTransform : : Serialize ( NetworkStream * stream )
@@ -41,58 +152,62 @@ void NetworkTransform::Serialize(NetworkStream* stream)
transform = Transform : : Identity ;
// Encode data
const NetworkObjectRole role = NetworkReplicator : : GetObjectRole ( this ) ;
Data data ;
data . LocalSpace = LocalSpace ;
data . SyncMode = SyncMode ;
data . HasSequenceIndex = Mode = = ReplicationModes : : Prediction ;
data . Components = Components ;
stream - > Write ( data ) ;
if ( ( data . SyncMode & SyncMode s: : All ) = = ( int ) SyncMode s: : All )
if ( ( data . Components & ReplicationComponent s: : All ) = = ( int ) ReplicationComponent s: : All )
{
stream - > Write ( transform ) ;
}
else
{
if ( ( data . SyncMode & SyncMode s: : Position ) = = ( int ) SyncMode s: : Position )
if ( ( data . Components & ReplicationComponent s: : Position ) = = ( int ) ReplicationComponent s: : Position )
{
stream - > Write ( transform . Translation ) ;
}
else if ( data . SyncMode & SyncMode s: : Position )
else if ( data . Components & ReplicationComponent s: : Position )
{
if ( data . SyncMode & SyncMode s: : PositionX )
if ( data . Components & ReplicationComponent s: : PositionX )
stream - > Write ( transform . Translation . X ) ;
if ( data . SyncMode & SyncMode s: : PositionY )
if ( data . Components & ReplicationComponent s: : PositionY )
stream - > Write ( transform . Translation . X ) ;
if ( data . SyncMode & SyncMode s: : PositionZ )
if ( data . Components & ReplicationComponent s: : PositionZ )
stream - > Write ( transform . Translation . X ) ;
}
if ( ( data . SyncMode & SyncMode s: : Scale ) = = ( int ) SyncMode s: : Scale )
if ( ( data . Components & ReplicationComponent s: : Scale ) = = ( int ) ReplicationComponent s: : Scale )
{
stream - > Write ( transform . Scale ) ;
}
else if ( data . SyncMode & SyncMode s: : Scale )
else if ( data . Components & ReplicationComponent s: : Scale )
{
if ( data . SyncMode & SyncMode s: : ScaleX )
if ( data . Components & ReplicationComponent s: : ScaleX )
stream - > Write ( transform . Scale . X ) ;
if ( data . SyncMode & SyncMode s: : ScaleY )
if ( data . Components & ReplicationComponent s: : ScaleY )
stream - > Write ( transform . Scale . X ) ;
if ( data . SyncMode & SyncMode s: : ScaleZ )
if ( data . Components & ReplicationComponent s: : ScaleZ )
stream - > Write ( transform . Scale . X ) ;
}
if ( ( data . SyncMode & SyncMode s: : Rotation ) = = ( int ) SyncMode s: : Rotation )
if ( ( data . Components & ReplicationComponent s: : Rotation ) = = ( int ) ReplicationComponent s: : Rotation )
{
const Float3 rotation = transform . Orientation . GetEuler ( ) ;
stream - > Write ( rotation ) ;
}
else if ( data . SyncMode & SyncMode s: : Rotation )
else if ( data . Components & ReplicationComponent s: : Rotation )
{
const Float3 rotation = transform . Orientation . GetEuler ( ) ;
if ( data . SyncMode & SyncMode s: : RotationX )
if ( data . Components & ReplicationComponent s: : RotationX )
stream - > Write ( rotation . X ) ;
if ( data . SyncMode & SyncMode s: : RotationY )
if ( data . Components & ReplicationComponent s: : RotationY )
stream - > Write ( rotation . Y ) ;
if ( data . SyncMode & SyncMode s: : RotationZ )
if ( data . Components & ReplicationComponent s: : RotationZ )
stream - > Write ( rotation . Z ) ;
}
}
if ( data . HasSequenceIndex )
stream - > Write ( _currentSequenceIndex ) ;
}
void NetworkTransform : : Deserialize ( NetworkStream * stream )
@@ -103,65 +218,133 @@ void NetworkTransform::Deserialize(NetworkStream* stream)
transform = LocalSpace ? parent - > GetLocalTransform ( ) : parent - > GetTransform ( ) ;
else
transform = Transform : : Identity ;
Transform transformLocal = transform ;
// Decode data
Data data ;
stream - > Read ( data ) ;
if ( ( data . SyncMode & SyncMode s: : All ) = = ( int ) SyncMode s: : All )
if ( ( data . Components & ReplicationComponent s: : All ) = = ( int ) ReplicationComponent s: : All )
{
stream - > Read ( transform ) ;
}
else
{
if ( ( data . SyncMode & SyncMode s: : Position ) = = ( int ) SyncMode s: : Position )
if ( ( data . Components & ReplicationComponent s: : Position ) = = ( int ) ReplicationComponent s: : Position )
{
stream - > Read ( transform . Translation ) ;
}
else if ( data . SyncMode & SyncMode s: : Position )
else if ( data . Components & ReplicationComponent s: : Position )
{
if ( data . SyncMode & SyncMode s: : PositionX )
if ( data . Components & ReplicationComponent s: : PositionX )
stream - > Read ( transform . Translation . X ) ;
if ( data . SyncMode & SyncMode s: : PositionY )
if ( data . Components & ReplicationComponent s: : PositionY )
stream - > Read ( transform . Translation . X ) ;
if ( data . SyncMode & SyncMode s: : PositionZ )
if ( data . Components & ReplicationComponent s: : PositionZ )
stream - > Read ( transform . Translation . X ) ;
}
if ( ( data . SyncMode & SyncMode s: : Scale ) = = ( int ) SyncMode s: : Scale )
if ( ( data . Components & ReplicationComponent s: : Scale ) = = ( int ) ReplicationComponent s: : Scale )
{
stream - > Read ( transform . Scale ) ;
}
else if ( data . SyncMode & SyncMode s: : Scale )
else if ( data . Components & ReplicationComponent s: : Scale )
{
if ( data . SyncMode & SyncMode s: : ScaleX )
if ( data . Components & ReplicationComponent s: : ScaleX )
stream - > Read ( transform . Scale . X ) ;
if ( data . SyncMode & SyncMode s: : ScaleY )
if ( data . Components & ReplicationComponent s: : ScaleY )
stream - > Read ( transform . Scale . X ) ;
if ( data . SyncMode & SyncMode s: : ScaleZ )
if ( data . Components & ReplicationComponent s: : ScaleZ )
stream - > Read ( transform . Scale . X ) ;
}
if ( ( data . SyncMode & SyncMode s: : Rotation ) = = ( int ) SyncMode s: : Rotation )
if ( ( data . Components & ReplicationComponent s: : Rotation ) = = ( int ) ReplicationComponent s: : Rotation )
{
Float3 rotation ;
stream - > Read ( rotation ) ;
transform . Orientation = Quaternion : : Euler ( rotation ) ;
}
else if ( data . SyncMode & SyncMode s: : Rotation )
else if ( data . Components & ReplicationComponent s: : Rotation )
{
Float3 rotation = transform . Orientation . GetEuler ( ) ;
if ( data . SyncMode & SyncMode s: : RotationX )
if ( data . Components & ReplicationComponent s: : RotationX )
stream - > Read ( rotation . X ) ;
if ( data . SyncMode & SyncMode s: : RotationY )
if ( data . Components & ReplicationComponent s: : RotationY )
stream - > Read ( rotation . Y ) ;
if ( data . SyncMode & SyncMode s: : RotationZ )
if ( data . Components & ReplicationComponent s: : RotationZ )
stream - > Read ( rotation . Z ) ;
transform . Orientation = Quaternion : : Euler ( rotation ) ;
}
}
uint16 sequenceIndex = 0 ;
if ( data . HasSequenceIndex )
stream - > Read ( sequenceIndex ) ;
if ( data . LocalSpace ! = LocalSpace )
return ; // TODO: convert transform space if server-client have different values set
// Set transform
const NetworkObjectRole role = NetworkReplicator : : GetObjectRole ( this ) ;
if ( role = = NetworkObjectRole : : OwnedAuthoritative )
return ; // Ignore itself
if ( Mode = = ReplicationModes : : Default )
{
// Immediate set
Set ( transform ) ;
}
else if ( role = = NetworkObjectRole : : ReplicatedSimulated & & Mode = = ReplicationModes : : Prediction )
{
const Transform transformAuthoritative = transform ;
const Transform transformDeltaBefore = transformAuthoritative - transformLocal ;
// Remove any transform deltas from local simulation that happened before the incoming authoritative transform data
if ( ! _bufferHasDeltas )
{
_buffer . Clear ( ) ;
_bufferHasDeltas = true ;
}
// TODO: items are added in order to do batch removal
for ( int32 i = 0 ; i < _buffer . Count ( ) & & _buffer [ i ] . SequenceIndex < sequenceIndex ; i + + )
{
_buffer . RemoveAtKeepOrder ( i ) ;
}
// Use received authoritative actor transformation but re-apply all deltas not yet processed by the server due to lag (reconciliation)
for ( auto & e : _buffer )
{
transform . Translation = transform . Translation + e . Value . Translation ;
transform . Scale = transform . Scale * e . Value . Scale ;
}
// TODO: use euler angles or similar to cache/reapply rotation deltas (Quaternion jitters)
transform . Orientation = transformLocal . Orientation ;
// If local simulation is very close to the authoritative server value then ignore slight error (based relative delta threshold)
const Transform transformDeltaAfter = transformAuthoritative - transformLocal ;
const Transform transformDeltaDelta = transformDeltaAfter - transformDeltaBefore ;
if ( IsWithinPrecision ( transformDeltaDelta . Translation , transformDeltaAfter . Translation ) & &
IsWithinPrecision ( transformDeltaDelta . Scale , transformDeltaAfter . Scale )
)
{
return ;
}
// Set to the incoming value with applied local deltas
Set ( transform ) ;
_lastFrameTransform = transform ;
}
else
{
// Add to the interpolation buffer
const float now = Time : : Update . UnscaledTime . GetTotalSeconds ( ) ;
_buffer . Add ( { now , 0 , transform } ) ;
if ( _bufferHasDeltas )
{
_buffer . Clear ( ) ;
_bufferHasDeltas = false ;
}
}
}
void NetworkTransform : : Set ( const Transform & transform )
{
if ( auto * parent = GetParent ( ) )
{
if ( data . LocalSpace)
if ( LocalSpace )
parent - > SetLocalTransform ( transform ) ;
else
parent - > SetTransform ( transform ) ;