From 68884635b29e2aff209242324dbd5a322d4dd12b Mon Sep 17 00:00:00 2001 From: Conna Wiles Date: Fri, 23 Jan 2026 07:21:45 +0000 Subject: [PATCH] More Networking Optimizations --- .../Scene/Components/Collider/Rigidbody.cs | 71 ++++++++++++++++--- .../Scene/Components/Component.Network.cs | 17 ++++- .../Scene/Events/IGameObjectNetworkEvents.cs | 5 ++ .../Scene/GameObject/GameObject.Network.cs | 50 ++++++------- .../GameObjectSystem.Network.cs | 1 + .../DeltaSnapshots/DeltaSnapshotSystem.cs | 15 ++-- .../DeltaSnapshots/LocalSnapshotState.cs | 37 +++++++--- .../DeltaSnapshots/SnapshotValueCache.cs | 34 +++++---- .../Scene/Networking/NetworkObject.cs | 1 + .../Scene/Networking/NetworkTable.cs | 41 +++++------ 10 files changed, 188 insertions(+), 84 deletions(-) diff --git a/engine/Sandbox.Engine/Scene/Components/Collider/Rigidbody.cs b/engine/Sandbox.Engine/Scene/Components/Collider/Rigidbody.cs index fb858629..c775d121 100644 --- a/engine/Sandbox.Engine/Scene/Components/Collider/Rigidbody.cs +++ b/engine/Sandbox.Engine/Scene/Components/Collider/Rigidbody.cs @@ -176,10 +176,37 @@ sealed public partial class Rigidbody : Component, Component.ExecuteInEditor, IG Vector3 _lastVelocity; Vector3 _lastAngularVelocity; - [Sync( SyncFlags.Query )] + [Sync] + private Vector3 NetworkedVelocity + { + get; + set + { + _lastVelocity = value; + field = value; + } + } + + [Sync] + private Vector3 NetworkedAngularVelocity + { + get; + set + { + _lastAngularVelocity = value; + field = value; + } + } + public Vector3 Velocity { - get => _body?.Velocity ?? default; + get + { + if ( IsProxy ) + return NetworkedVelocity; + + return _body?.Velocity ?? default; + } set { if ( _body.IsValid() && !IsProxy ) @@ -191,10 +218,15 @@ sealed public partial class Rigidbody : Component, Component.ExecuteInEditor, IG } } - [Sync( SyncFlags.Query )] public Vector3 AngularVelocity { - get => _body?.AngularVelocity ?? default; + get + { + if ( IsProxy ) + return NetworkedAngularVelocity; + + return _body?.AngularVelocity ?? default; + } set { if ( _body.IsValid() && !IsProxy ) @@ -267,8 +299,8 @@ sealed public partial class Rigidbody : Component, Component.ExecuteInEditor, IG } /// - /// Gets or sets the inertia tensor for this body. - /// By default, the inertia tensor is automatically calculated from the shapes attached to the body. + /// Gets or sets the inertia tensor for this body. + /// By default, the inertia tensor is automatically calculated from the shapes attached to the body. /// Setting this property overrides the automatically calculated inertia tensor until is called. /// public Vector3 InertiaTensor @@ -282,8 +314,8 @@ sealed public partial class Rigidbody : Component, Component.ExecuteInEditor, IG } /// - /// Gets or sets the rotation applied to the inertia tensor. - /// Like , this acts as an override to the automatically calculated inertia tensor rotation + /// Gets or sets the rotation applied to the inertia tensor. + /// Like , this acts as an override to the automatically calculated inertia tensor rotation /// and remains in effect until is called. /// public Rotation InertiaTensorRotation @@ -317,8 +349,23 @@ sealed public partial class Rigidbody : Component, Component.ExecuteInEditor, IG } } + void IGameObjectNetworkEvents.BeforeDropOwnership() + { + // Before we drop ownership, we want to make sure the networked vars + // are fully synchronized with the PhysicsBody. This is because when + // we drop ownership, we'll send a packet about our current state to + // other clients, and we may not have updated these yet in the physics + // step. + + if ( !_body.IsValid() ) + return; + + NetworkedAngularVelocity = _body.AngularVelocity; + NetworkedVelocity = _body.Velocity; + } + /// - /// Resets the inertia tensor and its rotation to the values automatically calculated from the attached colliders. + /// Resets the inertia tensor and its rotation to the values automatically calculated from the attached colliders. /// This removes any custom overrides set via or . /// public void ResetInertiaTensor() @@ -531,6 +578,12 @@ sealed public partial class Rigidbody : Component, Component.ExecuteInEditor, IG // Networked proxy should use velocity to move to world transform. _body.Move( Transform.TargetWorld, Time.Delta ); } + + if ( IsProxy ) + return; + + NetworkedAngularVelocity = _body.AngularVelocity; + NetworkedVelocity = _body.Velocity; } /// diff --git a/engine/Sandbox.Engine/Scene/Components/Component.Network.cs b/engine/Sandbox.Engine/Scene/Components/Component.Network.cs index 5710fdb6..9d77485c 100644 --- a/engine/Sandbox.Engine/Scene/Components/Component.Network.cs +++ b/engine/Sandbox.Engine/Scene/Components/Component.Network.cs @@ -22,13 +22,28 @@ public abstract partial class Component { try { - // If we aren't valid then just set the property value anyway. + // If we aren't valid, then just set the property value anyway. if ( !IsValid ) { p.Setter?.Invoke( p.Value ); return; } + // If it's the same value, just call the original setter because + // we don't want to do all the logic below for the same value. + // Obviously, if we're reading changes from the network, then we + // should just allow all the logic to go through. + if ( !NetworkTable.IsReadingChanges ) + { + var currentValue = p.Getter(); + + if ( Equals( currentValue, p.Value ) ) + { + p.Setter?.Invoke( p.Value ); + return; + } + } + var root = GameObject.FindNetworkRoot(); var slot = NetworkObject.GetPropertySlot( p.MemberIdent, Id ); diff --git a/engine/Sandbox.Engine/Scene/Events/IGameObjectNetworkEvents.cs b/engine/Sandbox.Engine/Scene/Events/IGameObjectNetworkEvents.cs index 4ce23762..339feed3 100644 --- a/engine/Sandbox.Engine/Scene/Events/IGameObjectNetworkEvents.cs +++ b/engine/Sandbox.Engine/Scene/Events/IGameObjectNetworkEvents.cs @@ -5,6 +5,11 @@ /// public interface IGameObjectNetworkEvents : ISceneEvent { + /// + /// Called before we are about to drop ownership of a network GameObject + /// + internal void BeforeDropOwnership() { } + /// /// Called when the owner of a network GameObject is changed /// diff --git a/engine/Sandbox.Engine/Scene/GameObject/GameObject.Network.cs b/engine/Sandbox.Engine/Scene/GameObject/GameObject.Network.cs index e05566ed..f973f0d1 100644 --- a/engine/Sandbox.Engine/Scene/GameObject/GameObject.Network.cs +++ b/engine/Sandbox.Engine/Scene/GameObject/GameObject.Network.cs @@ -218,11 +218,10 @@ public partial class GameObject /// /// Make a request from the host to stop being the network owner of this game object. /// - [Rpc.Broadcast] + [Rpc.Host] void Msg_RequestDropOwnership( ushort snapshotVersion ) { if ( _net is null ) return; - if ( !Networking.IsHost ) return; if ( OwnerTransfer != OwnerTransfer.Request ) return; var caller = Rpc.Caller; @@ -269,11 +268,10 @@ public partial class GameObject /// /// Make a request from the host to become the network owner of this game object. /// - [Rpc.Broadcast] + [Rpc.Host] void Msg_RequestTakeOwnership( ushort snapshotVersion ) { if ( _net is null ) return; - if ( !Networking.IsHost ) return; if ( OwnerTransfer != OwnerTransfer.Request ) return; // Can this caller take ownership? @@ -339,11 +337,10 @@ public partial class GameObject /// /// Make a request from the host to assign ownership of this game object to the specified connection . /// - [Rpc.Broadcast] + [Rpc.Host] void Msg_RequestAssignOwnership( Guid guid, ushort snapshotVersion ) { if ( _net is null ) return; - if ( !Networking.IsHost ) return; if ( OwnerTransfer != OwnerTransfer.Request ) return; // Can this caller assign ownership? @@ -809,12 +806,7 @@ public partial class GameObject if ( !IsProxy ) { - // Clear interpolation and set that flag here. - go.Transform.ClearInterpolation(); - - // Force a delta snapshot for this object since we changed owner. - var system = SceneNetworkSystem.Instance; - system?.DeltaSnapshots?.Send( go._net, NetFlags.Reliable, true ); + UpdateStateBeforeOwnerChange(); } if ( !Networking.IsHost && go.OwnerTransfer == OwnerTransfer.Request ) @@ -839,12 +831,7 @@ public partial class GameObject if ( !IsProxy ) { - // Clear interpolation and set that flag here. - go.Transform.ClearInterpolation(); - - // Force a delta snapshot for this object since we changed owner. - var system = SceneNetworkSystem.Instance; - system?.DeltaSnapshots?.Send( go._net, NetFlags.Reliable, true ); + UpdateStateBeforeOwnerChange(); } go.Msg_AssignOwnership( connectionId, go.Network.SnapshotVersion ); @@ -886,12 +873,7 @@ public partial class GameObject if ( !IsProxy ) { - // Clear interpolation and set that flag here. - go.Transform.ClearInterpolation(); - - // Force a delta snapshot for this object since we changed owner. - var system = SceneNetworkSystem.Instance; - system?.DeltaSnapshots?.Send( go._net, NetFlags.Reliable, true ); + UpdateStateBeforeOwnerChange(); } if ( Networking.IsHost ) @@ -930,5 +912,25 @@ public partial class GameObject { return go.NetworkSpawn( owner ); } + + /// + /// Before we drop ownership (or assign ownership to somebody else), there are a few things + /// we want to do first. We want to let any listeners know so that they can react to it, we + /// want to clear interpolation, and finally, we want to force a full delta snapshot of the + /// object's state. + /// + private void UpdateStateBeforeOwnerChange() + { + // Let any listeners know that we're about to drop ownership and send + // a full state update to everyone + IGameObjectNetworkEvents.PostToGameObject( go, x => x.BeforeDropOwnership() ); + + // Clear interpolation and set that flag here + go.Transform.ClearInterpolation(); + + // Force a delta snapshot for this object since we changed the owner + var system = SceneNetworkSystem.Instance; + system?.DeltaSnapshots?.Send( go._net, NetFlags.Reliable, true ); + } } } diff --git a/engine/Sandbox.Engine/Scene/GameObjectSystem/GameObjectSystem.Network.cs b/engine/Sandbox.Engine/Scene/GameObjectSystem/GameObjectSystem.Network.cs index a32c37c7..38a516c9 100644 --- a/engine/Sandbox.Engine/Scene/GameObjectSystem/GameObjectSystem.Network.cs +++ b/engine/Sandbox.Engine/Scene/GameObjectSystem/GameObjectSystem.Network.cs @@ -208,6 +208,7 @@ public abstract partial class GameObjectSystem : IDeltaSnapshot var system = SceneNetworkSystem.Instance; if ( system is null ) return null; + LocalSnapshotState.Begin(); LocalSnapshotState.SnapshotId = system.DeltaSnapshots.CreateSnapshotId( Id ); LocalSnapshotState.ObjectId = Id; diff --git a/engine/Sandbox.Engine/Scene/Networking/DeltaSnapshots/DeltaSnapshotSystem.cs b/engine/Sandbox.Engine/Scene/Networking/DeltaSnapshots/DeltaSnapshotSystem.cs index 12a3d051..ecb1072a 100644 --- a/engine/Sandbox.Engine/Scene/Networking/DeltaSnapshots/DeltaSnapshotSystem.cs +++ b/engine/Sandbox.Engine/Scene/Networking/DeltaSnapshots/DeltaSnapshotSystem.cs @@ -18,13 +18,13 @@ internal class DeltaSnapshotSystem internal class GuidUlongComparer : IEqualityComparer { [MethodImpl( MethodImplOptions.AggressiveInlining )] - public bool Equals( Guid x, Guid y ) => x.Equals( y ); + public bool Equals( Guid x, Guid y ) => x == y; [MethodImpl( MethodImplOptions.AggressiveInlining )] public int GetHashCode( Guid guid ) { - var lowBits = MemoryMarshal.Read( MemoryMarshal.AsBytes( MemoryMarshal.CreateReadOnlySpan( in guid, 1 ) ) ); - return (int)(lowBits ^ (lowBits >> 32)); + var span = MemoryMarshal.Cast( MemoryMarshal.CreateSpan( ref guid, 1 ) ); + return (int)(span[0] ^ span[1]); } } @@ -674,13 +674,12 @@ internal class DeltaSnapshotSystem /// public ushort CreateSnapshotId( Guid objectId ) { - ushort snapshotId = 0; + ref var id = ref CollectionsMarshal.GetValueRefOrAddDefault( LastSentSnapshotIds, objectId, out bool exists ); - if ( LastSentSnapshotIds.TryGetValue( objectId, out var id ) ) - snapshotId = (ushort)(id + 1); + if ( exists ) + id++; - LastSentSnapshotIds[objectId] = snapshotId; - return snapshotId; + return id; } /// diff --git a/engine/Sandbox.Engine/Scene/Networking/DeltaSnapshots/LocalSnapshotState.cs b/engine/Sandbox.Engine/Scene/Networking/DeltaSnapshots/LocalSnapshotState.cs index db258323..6aa7fce8 100644 --- a/engine/Sandbox.Engine/Scene/Networking/DeltaSnapshots/LocalSnapshotState.cs +++ b/engine/Sandbox.Engine/Scene/Networking/DeltaSnapshots/LocalSnapshotState.cs @@ -26,6 +26,8 @@ internal class LocalSnapshotState public Guid ObjectId { get; set; } public int Size { get; private set; } + private bool _isHashInvalid { get; set; } + [Flags] public enum HashFlags { @@ -59,6 +61,7 @@ internal class LocalSnapshotState return; _parentIdBytes ??= new byte[16]; + _isHashInvalid = true; value.TryWriteBytes( _parentIdBytes ); field = value; @@ -78,6 +81,7 @@ internal class LocalSnapshotState _flagsBytes ??= new byte[1]; _flagsBytes[0] = (byte)value; + _isHashInvalid = true; field = value; } @@ -86,7 +90,15 @@ internal class LocalSnapshotState private byte[] _parentIdBytes; private byte[] _flagsBytes; - private readonly XxHash3 _hasher = new(); + private static readonly XxHash3 Hasher = new(); + + /// + /// Call this every time you begin updating the snapshot state. + /// + public void Begin() + { + _isHashInvalid = false; + } /// /// Remove a connection from stored state acknowledgements. @@ -138,9 +150,9 @@ internal class LocalSnapshotState /// public ulong Hash( byte[] value ) { - _hasher.Reset(); - _hasher.Append( value ); - return _hasher.GetCurrentHashAsUInt64(); + Hasher.Reset(); + Hasher.Append( value ); + return Hasher.GetCurrentHashAsUInt64(); } /// @@ -189,16 +201,16 @@ internal class LocalSnapshotState /// public void AddSerialized( int slot, byte[] value, HashFlags hashFlags = HashFlags.Default ) { - _hasher.Reset(); - _hasher.Append( value ); + Hasher.Reset(); + Hasher.Append( value ); if ( (hashFlags & HashFlags.WithParentId) != 0 && _parentIdBytes is not null ) - _hasher.Append( _parentIdBytes ); + Hasher.Append( _parentIdBytes ); if ( (hashFlags & HashFlags.WithNetworkFlags) != 0 && _flagsBytes is not null ) - _hasher.Append( _flagsBytes ); + Hasher.Append( _flagsBytes ); - var hash = _hasher.GetCurrentHashAsUInt64(); + var hash = Hasher.GetCurrentHashAsUInt64(); AddSerialized( slot, value, hash ); } @@ -209,6 +221,11 @@ internal class LocalSnapshotState /// public void AddCached( SnapshotValueCache cache, int slot, T value, HashFlags hashFlags = HashFlags.Default ) { - AddSerialized( slot, cache.GetCached( slot, value ), hashFlags ); + var cached = cache.GetCached( slot, value, out var isEqual ); + + if ( isEqual && !_isHashInvalid ) + return; + + AddSerialized( slot, cached, hashFlags ); } } diff --git a/engine/Sandbox.Engine/Scene/Networking/DeltaSnapshots/SnapshotValueCache.cs b/engine/Sandbox.Engine/Scene/Networking/DeltaSnapshots/SnapshotValueCache.cs index 0c9cc696..9feedb34 100644 --- a/engine/Sandbox.Engine/Scene/Networking/DeltaSnapshots/SnapshotValueCache.cs +++ b/engine/Sandbox.Engine/Scene/Networking/DeltaSnapshots/SnapshotValueCache.cs @@ -1,37 +1,47 @@ +using System.Runtime.InteropServices; using Sandbox.Engine; namespace Sandbox.Network; internal class SnapshotValueCache { - private Dictionary Serialized { get; } = new(); - private Dictionary Cache { get; } = new(); + private readonly Dictionary _serialized = new(); + private readonly Dictionary _hashCache = new(); /// - /// Get cached bytes from the specified value if they exist. If the value is different + /// Get cached bytes from the specified value if they exist. If the value is different, /// then re-serialize and cache again. /// - public byte[] GetCached( int slot, T value ) + public byte[] GetCached( int slot, T value, out bool isEqual ) { - if ( Cache.TryGetValue( slot, out var cached ) && Equals( cached, value ) ) - return Serialized[slot]; + var hash = value?.GetHashCode() ?? 0; + + ref var cachedHash = ref CollectionsMarshal.GetValueRefOrAddDefault( _hashCache, slot, out bool exists ); + + if ( exists && cachedHash == hash ) + { + isEqual = true; + return _serialized[slot]; + } var bytes = GlobalContext.Current.TypeLibrary.ToBytes( value ); - Serialized[slot] = bytes; - Cache[slot] = value; + _serialized[slot] = bytes; + + cachedHash = hash; + isEqual = false; return bytes; } public void Remove( int slot ) { - Serialized.Remove( slot ); - Cache.Remove( slot ); + _serialized.Remove( slot ); + _hashCache.Remove( slot ); } public void Clear() { - Serialized.Clear(); - Cache.Clear(); + _serialized.Clear(); + _hashCache.Clear(); } } diff --git a/engine/Sandbox.Engine/Scene/Networking/NetworkObject.cs b/engine/Sandbox.Engine/Scene/Networking/NetworkObject.cs index 49ca786a..75cc2579 100644 --- a/engine/Sandbox.Engine/Scene/Networking/NetworkObject.cs +++ b/engine/Sandbox.Engine/Scene/Networking/NetworkObject.cs @@ -533,6 +533,7 @@ internal sealed partial class NetworkObject : IValid, IDeltaSnapshot var flags = GameObject.Network.Flags; + LocalSnapshotState.Begin(); LocalSnapshotState.SnapshotId = system.DeltaSnapshots.CreateSnapshotId( Id ); LocalSnapshotState.ParentId = GameObject.Parent is Scene ? Guid.Empty : GameObject.Parent.Id; LocalSnapshotState.ObjectId = Id; diff --git a/engine/Sandbox.Engine/Scene/Networking/NetworkTable.cs b/engine/Sandbox.Engine/Scene/Networking/NetworkTable.cs index 7fd406da..7bc95490 100644 --- a/engine/Sandbox.Engine/Scene/Networking/NetworkTable.cs +++ b/engine/Sandbox.Engine/Scene/Networking/NetworkTable.cs @@ -12,33 +12,34 @@ internal class NetworkTable : IDisposable public class Entry : INetworkProxy { - public Type TargetType { get; init; } - public string DebugName { get; init; } - public bool NeedsQuery { get; set; } - public Func ControlCondition { get; init; } = c => true; - public Func GetValue { get; init; } - public Action SetValue { get; init; } - public Action OnDirty { get; set; } - public ulong SnapshotHash { get; set; } - public int HashCodeValue { get; set; } - public bool IsSerializerType { get; private set; } - public bool IsDeltaSnapshotType { get; private set; } - public bool IsReliableType { get; set; } - public byte[] Serialized { get; set; } - public bool Initialized { get; set; } - public int Slot { get; private set; } - - private bool InternalIsDirty { get; set; } + // We're making all of these fields because they're accessed on an extremely hot path. + // Being properties, the extra method call stacks up a lot when you have thousands and + // thousands of entries. It doesn't really matter though, this API is not public. + public Type TargetType; + public string DebugName; + public bool NeedsQuery; + public Func ControlCondition = c => true; + public Func GetValue; + public Action SetValue; + public Action OnDirty; + public ulong SnapshotHash; + public int HashCodeValue; + public bool IsSerializerType; + public bool IsDeltaSnapshotType; + public bool IsReliableType; + public byte[] Serialized; + public bool Initialized; + public int Slot; public bool IsDirty { - get => InternalIsDirty; + get; set { - if ( InternalIsDirty == value ) + if ( field == value ) return; - InternalIsDirty = value; + field = value; OnDirty?.Invoke( this ); } }