Files
sbox-public/engine/Sandbox.Engine/Scene/Networking/NetworkObject.cs
Lorenz Junglas 238425e6d3 Omit nulls when serializing for network (#4376)
When serializing and deserializing to/from JSON we need to explicitly write nulls.
Because for development/editing we need to differentiate the following cases:
1. JSON is of an older version (field missing) -> initialize with code defined default
2. JSON is of the current version and field is actually null -> initialize with null

For networking we don't need to differentiate between those cases since client and server have the same version of the code at runtime.

So in networking case we can omit nulls from the JSON and skip all of these:

```
          "OnComponentDestroy": null,
          "OnComponentDisabled": null,
          "OnComponentEnabled": null,
          "OnComponentFixedUpdate": null,
          "OnComponentStart": null,
          "OnComponentUpdate": null,
          "OnPropBreak": null,
          "OnPropTakeDamage": null,
```

However, this wont reduce the amount of data that ends up on the wire, because compression already does a good job at deduplicating those strings.

```
Raw JSON    with nulls :      41Kb
Raw JSON    skip nulls :      27Kb  (13Kb saved, 33.6% smaller, 1.51x ratio)
LZ4 fast    with nulls :       7Kb  (34Kb saved, 81.9% smaller, 5.54x ratio)
LZ4 fast    skip nulls :       7Kb  (34Kb saved, 82.6% smaller, 5.76x ratio)
```

The main benefit instead comes from saving CPU resources as this makes compression/decompression faster, because there is less data to process.

```
LZ4  with nulls          compress :    0.210 ms/op
LZ4  with nulls          decomp   :    0.232 ms/op
 
LZ4  skip nulls          compress :    0.165 ms/op
LZ4  skip nulls          decomp   :    0.074 ms/op
```
2026-03-27 10:59:06 +01:00

888 lines
23 KiB
C#

using System.Runtime.CompilerServices;
using Sandbox.Network;
using System.Runtime.InteropServices;
using System.Text.Json.Nodes;
namespace Sandbox;
internal sealed partial class NetworkObject : IValid, IDeltaSnapshot
{
internal NetworkObject RootNetworkObject => GameObject.RootNetwork.RootGameObject._net;
internal GameObject GameObject { get; set; }
Guid IDeltaSnapshot.Id => Id;
/// <summary>
/// The unique <see cref="Guid"/> of the underlying <see cref="GameObject"/>.
/// </summary>
internal Guid Id => GameObject.Id;
/// <summary>
/// The <see cref="Guid"/> of the connection that created this.
/// </summary>
public Guid Creator { get; private set; }
public bool IsValid => GameObject.IsValid();
/// <summary>
/// If true, then this object is spawning on the host, on behalf of another client. While it's
/// doing this, we're going to act like the host is the owner... so that anything called in
/// OnAwake will think we're not a proxy - until we've fully handed it off.
/// </summary>
bool _isNetworkSpawning;
/// <summary>
/// The <see cref="Guid"/> of the connection that owns this.
/// </summary>
public Guid Owner
{
get;
set
{
if ( field == value )
{
UpdateIsOwner();
UpdateIsProxy();
return;
}
var oldOwner = field;
field = value;
UpdateIsOwner();
UpdateIsProxy();
OnOwnerChanged( field, oldOwner );
}
}
/// <summary>
/// Are we the owner of this networked object?
/// </summary>
public bool IsOwner { get; private set; }
/// <summary>
/// Is this networked object unowned?
/// </summary>
public bool IsUnowned { get; private set; } = true;
/// <summary>
/// This is a proxy if we don't own this networked object.
/// </summary>
public bool IsProxy { get; private set; }
private void UpdateIsProxy()
{
if ( _isNetworkSpawning || IsOwner || (IsUnowned && Networking.IsHost) )
{
IsProxy = false;
return;
}
IsProxy = true;
}
private void UpdateIsOwner()
{
IsUnowned = Owner == Guid.Empty;
IsOwner = Owner == Connection.Local.Id;
}
/// <summary>
/// Current snapshot version for this networked object.
/// </summary>
public ushort SnapshotVersion => LocalSnapshotState.Version;
bool _clearInterpolationFlag;
bool _hasNetworkDestroyed;
bool _initialized;
internal void OnHotload()
{
// Build the network table again as properties may have changed.
CreateDataTable();
}
internal NetworkObject( GameObject source )
{
GameObject = source;
}
/// <summary>
/// Initialize and spawn this networked object with the specified owner <see cref="Connection"/>.
/// </summary>
internal void InitializeForConnection( Connection owner, bool enable )
{
if ( _initialized )
throw new( "NetworkObject already initialized" );
using var _ = PerformanceStats.Timings.Network.Scope();
_initialized = true;
if ( owner is not null )
{
Creator = owner.Id;
Owner = owner.Id;
}
else
{
Creator = Guid.Empty;
Owner = Guid.Empty;
}
CreateDataTable();
// Keep track of us
GameObject.Scene.RegisterNetworkedObject( this );
// Call OnAwake on everything. We allow you to enable here because if you're the
// host spawning this object for other connections, then you probably want to act
// like the owner of it while OnAwake/OnEnabled is being called.
_isNetworkSpawning = true;
UpdateIsProxy();
GameObject.Enabled = enable;
CallNetworkSpawn( owner );
_isNetworkSpawning = false;
UpdateIsProxy();
// Tell the world that we're here
BroadcastNetworkSpawn( owner );
}
/// <summary>
/// Call INetworkSpawn hooks
/// </summary>
private void CallNetworkSpawn( Connection owner )
{
foreach ( var target in GameObject.Components.GetAll<Component.INetworkSpawn>( FindMode.EverythingInSelfAndDescendants ).ToArray() )
{
try
{
target.OnNetworkSpawn( owner );
}
catch ( Exception e )
{
Log.Error( e );
}
}
}
/// <summary>
/// Tell everyone that we exist, and spawn any child network objects with the same owner.
/// </summary>
void BroadcastNetworkSpawn( Connection owner )
{
// Tell everyone that we exist
SceneNetworkSystem.Instance?.NetworkSpawnBroadcast( this );
// If we have any child network objects, then spawn them with the same owner
GameObject.NetworkSpawnRecursive( owner );
}
/// <summary>
/// Initialize this networked object from a create message.
/// </summary>
internal void Initialize( ObjectCreateMsg msg )
{
if ( _initialized )
throw new( "NetworkObject already initialized" );
using var _ = PerformanceStats.Timings.Network.Scope();
_initialized = true;
Creator = msg.Creator;
Owner = msg.Owner;
CreateDataTable();
OnCreateMessage( msg );
GameObject.Scene.RegisterNetworkedObject( this );
}
internal void Dispose()
{
GameObject.Scene.UnregisterNetworkObject( this );
GameObject = default;
}
internal void ClearInterpolation()
{
if ( IsProxy ) return;
_clearInterpolationFlag = true;
}
internal bool CanDropOwnership( Connection source )
{
// Conna: accept all requests for now. In future, maybe we can do something with INetworkListener?
return true;
}
internal bool CanAssignOwnership( Connection source, Guid target )
{
// Conna: accept all requests for now. In future, maybe we can do something with INetworkListener?
return true;
}
internal bool CanTakeOwnership( Connection source )
{
// Conna: accept all requests for now. In future, maybe we can do something with INetworkListener?
return true;
}
internal void OnNetworkDestroy()
{
_hasNetworkDestroyed = true;
GameObject.Destroy();
}
/// <summary>
/// Send a detach message to all other clients. Only the host can detach
/// a networked object.
/// </summary>
internal void SendNetworkDetach()
{
if ( SceneNetworkSystem.Instance is null ) return;
if ( Networking.IsDisconnecting ) return;
if ( !Networking.IsHost ) return;
SceneNetworkSystem.Instance.NetworkDetachBroadcast( this );
}
internal void SendNetworkDestroy()
{
if ( SceneNetworkSystem.Instance is null ) return;
if ( Networking.IsDisconnecting ) return;
if ( _hasNetworkDestroyed ) return;
if ( IsProxy && !Networking.IsHost ) return;
SceneNetworkSystem.Instance.NetworkDestroyBroadcast( this );
}
private static readonly GameObject.SerializeOptions _refreshSerializeOptions = new() { SingleNetworkObject = true, SkipNulls = true };
internal ObjectRefreshMsg GetRefreshMessage()
{
var system = SceneNetworkSystem.Instance;
if ( system is null )
throw new Exception( "SceneNetworkSystem is null" );
var snapshot = ((IDeltaSnapshot)this).WriteSnapshotState();
using var blobs = BlobDataSerializer.Capture();
var msg = new ObjectRefreshMsg
{
Guid = GameObject.Id,
Parent = GameObject.Parent.Id,
JsonData = GameObject.Serialize( _refreshSerializeOptions ).ToJsonString(),
BlobData = blobs.ToByteArray(),
TableData = WriteReliableData(),
Snapshot = system.DeltaSnapshots.GetFullSnapshotData( snapshot )
};
return msg;
}
private static readonly GameObject.SerializeOptions _refreshDescendantSerializeOptions = new() { IgnoreChildren = true, SkipNulls = true };
private static readonly GameObject.SerializeOptions _componentRefreshSerializeOptions = new() { SkipNulls = true };
internal void SendNetworkRefresh( GameObject go )
{
var system = SceneNetworkSystem.Instance;
if ( system is null ) return;
if ( go is null ) return;
if ( go == GameObject )
{
// If the object passed isn't actually a descendant, then refresh
// the entire tree.
SendNetworkRefresh();
return;
}
if ( go.IsDestroyed )
{
// We want to tell clients that this component has been destroyed...
var msg = new ObjectDestroyDescendantMsg
{
Guid = go.Id
};
system.Broadcast( msg );
return;
}
if ( !go.IsAncestor( GameObject ) )
return;
{
var snapshot = ((IDeltaSnapshot)this).WriteSnapshotState();
using var blobs = BlobDataSerializer.Capture();
var msg = new ObjectRefreshDescendantMsg
{
GameObjectId = GameObject.Id,
ParentId = go.Parent.Id,
JsonData = go.Serialize( _refreshDescendantSerializeOptions ).ToJsonString(),
BlobData = blobs.ToByteArray(),
TableData = WriteReliableData(),
Snapshot = system.DeltaSnapshots.GetFullSnapshotData( snapshot )
};
system.Broadcast( msg );
}
}
internal void SendNetworkRefresh( Component component )
{
var system = SceneNetworkSystem.Instance;
if ( system is null ) return;
if ( component is null ) return;
if ( !component.IsValid() )
{
// We want to tell clients that this component has been destroyed...
var msg = new ObjectDestroyComponentMsg
{
Guid = component.Id
};
system.Broadcast( msg );
return;
}
{
var snapshot = ((IDeltaSnapshot)this).WriteSnapshotState();
using var blobs = BlobDataSerializer.Capture();
var msg = new ObjectRefreshComponentMsg
{
JsonData = component.Serialize( _componentRefreshSerializeOptions ).ToJsonString(),
BlobData = blobs.ToByteArray(),
GameObjectId = component.GameObject.Id,
TableData = WriteReliableData(),
Snapshot = system.DeltaSnapshots.GetFullSnapshotData( snapshot )
};
system.Broadcast( msg );
}
}
internal void SendNetworkRefresh()
{
var system = SceneNetworkSystem.Instance;
if ( system is null ) return;
var msg = GetRefreshMessage();
system.Broadcast( msg );
}
private struct CullState
{
public bool Culled;
public float LastVisibleAt;
}
internal readonly LocalSnapshotState LocalSnapshotState = new();
private readonly HashSet<Guid> _culledConnections = [];
private readonly Dictionary<Guid, CullState> _cullStates = new();
private readonly SnapshotValueCache _snapshotCache = new();
private TimeUntil _nextUpdateCachedBounds;
private BBox _cachedLocalBounds;
/// <summary>
/// Only cull this object if we've been invisible for this long.
/// </summary>
private const float CullDelay = 2f;
/// <summary>
/// Remove a connection id from any internal data structures.
/// </summary>
/// <param name="id"></param>
internal void RemoveConnection( Guid id )
{
LocalSnapshotState.RemoveConnection( id );
_culledConnections.Remove( id );
_cullStates.Remove( id );
}
/// <summary>
/// Called when the host has changed.
/// </summary>
internal void OnHostChanged( Connection previousHost, Connection newHost )
{
ClearConnections();
UpdateIsProxy();
}
/// <summary>
/// Clear all connections associated with the local snapshot state.
/// </summary>
private void ClearConnections()
{
LocalSnapshotState.ClearConnections();
}
bool IDeltaSnapshot.ShouldTransmit( Connection target )
{
return GameObject.Network.AlwaysTransmit || !_culledConnections.Contains( target.Id );
}
bool IDeltaSnapshot.UpdateTransmitState( Connection[] targets )
{
if ( GameObject.Network.AlwaysTransmit )
{
for ( var i = 0; i < targets.Length; i++ )
{
var target = targets[i];
if ( !_culledConnections.Remove( target.Id ) )
continue;
GameObject.Network.SetCullState( target, false );
}
return true;
}
if ( _nextUpdateCachedBounds )
{
// Let's update the cached local bounds every half a second.
_nextUpdateCachedBounds = 0.5f;
_cachedLocalBounds = GameObject.GetLocalBounds();
}
var shouldTransmitToAny = false;
var worldBounds = _cachedLocalBounds + GameObject.WorldPosition;
var timeNow = Time.Now;
var rootNetworkObject = RootNetworkObject;
IDeltaSnapshot root = rootNetworkObject != this ? rootNetworkObject : null;
for ( var i = 0; i < targets.Length; i++ )
{
var target = targets[i];
ref var state = ref CollectionsMarshal.GetValueRefOrAddDefault( _cullStates, target.Id, out var exists );
if ( !exists )
{
state = new CullState
{
Culled = false,
LastVisibleAt = timeNow
};
}
if ( !state.Culled )
shouldTransmitToAny = true;
if ( (root?.ShouldTransmit( target ) ?? false) || IsVisible( target, worldBounds ) )
{
state.LastVisibleAt = timeNow;
if ( !state.Culled )
continue;
if ( !_culledConnections.Remove( target.Id ) )
continue;
LocalSnapshotState.RemoveConnection( target.Id );
GameObject.Network.SetCullState( target, false );
shouldTransmitToAny = true;
state.Culled = false;
}
else
{
var timeSinceVisible = timeNow - state.LastVisibleAt;
if ( state.Culled || timeSinceVisible < CullDelay )
continue;
if ( !_culledConnections.Add( target.Id ) )
continue;
GameObject.Network.SetCullState( target, true );
state.Culled = true;
}
}
return shouldTransmitToAny;
}
/// <summary>
/// Is this network object visible to the provided <see cref="Connection"/>. We'll check if we
/// have a culler component and use that, but we'll also use our bounds to determine if we're
/// visible.
/// </summary>
private bool IsVisible( Connection target, BBox worldBounds )
{
// Do we have a INetworkVisible? We're going to let that take priority.
var go = GameObject;
if ( go.IsValid() && go.Enabled && go.NetworkVisibility is not null )
{
return go.NetworkVisibility.IsVisibleToConnection( target, worldBounds );
}
// Global culling
return GameObject.Scene.IsBBoxVisibleToConnection( target, worldBounds );
}
void IDeltaSnapshot.OnSnapshotAck( Connection source, DeltaSnapshot snapshot, RemoteSnapshotState state )
{
IDeltaSnapshot snapshotter = this;
if ( !snapshotter.ShouldTransmit( source ) )
return;
var hasFullSnapshotState = true;
foreach ( var entry in LocalSnapshotState.Entries )
{
if ( state.IsValueHashEqual( entry.Slot, entry.Hash, snapshot.SnapshotId ) )
{
entry.Connections.Add( source.Id );
}
else
{
entry.Connections.Remove( source.Id );
hasFullSnapshotState = false;
}
}
if ( hasFullSnapshotState )
LocalSnapshotState.UpdatedConnections.Add( source.Id );
else
LocalSnapshotState.UpdatedConnections.Remove( source.Id );
}
private const int SnapshotPositionSlot = 1;
private const int SnapshotRotationSlot = 2;
private const int SnapshotScaleSlot = 3;
private const int SnapshotInterpolationSlot = 4;
private const int SnapshotEnabledSlot = 5;
LocalSnapshotState IDeltaSnapshot.WriteSnapshotState()
{
var system = SceneNetworkSystem.Instance;
if ( system is null ) return null;
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;
LocalSnapshotState.Flags = flags;
if ( !IsProxy )
{
var tx = GameObject.Transform.TargetLocal;
if ( (flags & NetworkFlags.NoPositionSync) == 0 )
LocalSnapshotState.AddCached( _snapshotCache, SnapshotPositionSlot, tx.Position, LocalSnapshotState.HashFlags.All );
else
LocalSnapshotState.Remove( SnapshotPositionSlot );
if ( (flags & NetworkFlags.NoRotationSync) == 0 )
LocalSnapshotState.AddCached( _snapshotCache, SnapshotRotationSlot, tx.Rotation, LocalSnapshotState.HashFlags.All );
else
LocalSnapshotState.Remove( SnapshotRotationSlot );
if ( (flags & NetworkFlags.NoScaleSync) == 0 )
LocalSnapshotState.AddCached( _snapshotCache, SnapshotScaleSlot, tx.Scale, LocalSnapshotState.HashFlags.All );
else
LocalSnapshotState.Remove( SnapshotScaleSlot );
LocalSnapshotState.AddCached( _snapshotCache, SnapshotInterpolationSlot, _clearInterpolationFlag );
LocalSnapshotState.AddCached( _snapshotCache, SnapshotEnabledSlot, GameObject.Enabled );
}
dataTable.QueryValues();
dataTable.WriteSnapshotState( LocalSnapshotState );
_clearInterpolationFlag = false;
return LocalSnapshotState;
}
void IDeltaSnapshot.SendNetworkUpdate( bool queryValues )
{
if ( queryValues )
dataTable.QueryValues( true );
if ( !dataTable.HasReliableChanges() )
return;
var msg = new ObjectNetworkTableMsg { Guid = GameObject.Id };
var data = ByteStream.Create( 4096 );
dataTable.WriteReliableChanged( ref data );
msg.TableData = data.ToArray();
data.Dispose();
SceneNetworkSystem.Instance.Broadcast( msg );
}
internal void TransmitStateChanged()
{
LocalSnapshotState.ClearConnections();
}
private static readonly GameObject.SerializeOptions _createSerializeOptions = new() { SingleNetworkObject = true, SkipNulls = true };
internal ObjectCreateMsg GetCreateMessage()
{
if ( GameObject.Parent is null )
{
throw new( $"GameObject {GameObject.Id} ({GameObject.Name} has invalid parent" );
}
using var blobs = BlobDataSerializer.Capture();
var jsonData = GameObject.Serialize( _createSerializeOptions );
if ( jsonData is null )
{
throw new( $"Unable to serialize {GameObject.Id} ({GameObject.Name})" );
}
var create = new ObjectCreateMsg
{
Guid = GameObject.Id,
SnapshotVersion = GameObject._net.LocalSnapshotState.Version,
Transform = GameObject.Transform.TargetLocal,
JsonData = jsonData.ToJsonString(),
BlobData = blobs.ToByteArray(),
Creator = Creator,
Parent = GameObject.Parent.Id,
Owner = Owner,
TableData = WriteDataTable( true ),
Enabled = GameObject.Enabled
};
return create;
}
internal void DoOrphanedAction()
{
var action = GameObject.Network.NetworkOrphaned;
if ( action == NetworkOrphaned.Destroy )
{
GameObject.Destroy();
}
else if ( action == NetworkOrphaned.ClearOwner )
{
if ( Networking.IsHost )
GameObject.Network.AssignOwnership( Guid.Empty );
else
Owner = Guid.Empty;
}
else if ( action == NetworkOrphaned.Random )
{
// Only the host can assign ownership to a random connection. Because they'll need to broadcast
// the random selection to everyone else.
if ( Networking.IsHost )
{
var connections = Connection.All.ToArray();
var randomIndex = Game.Random.Int( 0, connections.Length - 1 );
var connection = connections[randomIndex];
GameObject.Network.AssignOwnership( connection );
}
else
{
// We're not the host, so let's just clear the owner until we get the new randomly
// selected owner from the host.
Owner = Guid.Empty;
}
}
else if ( action == NetworkOrphaned.Host )
{
if ( Networking.IsHost )
GameObject.Network.AssignOwnership( Connection.Host?.Id ?? Guid.Empty );
else
Owner = Guid.Empty;
}
}
internal void OnNetworkTableMessage( ObjectNetworkTableMsg message, Connection source = null )
{
ReadDataTable( message.TableData, source: source );
}
internal void OnRefreshMessage( Connection source, ObjectRefreshMsg message )
{
var scene = Game.ActiveScene;
if ( !scene.IsValid() ) return;
var oldTransform = GameObject.Transform.TargetLocal;
using ( BlobDataSerializer.LoadFromMemory( message.BlobData ) )
{
var jsonObj = JsonNode.Parse( message.JsonData ).AsObject();
// Only the host can modify network flags after the object has been spawned.
if ( !source.IsHost )
jsonObj.Remove( GameObject.JsonKeys.NetworkFlags );
GameObject.SetParentFromNetwork( scene.Directory.FindByGuid( message.Parent ) );
GameObject.NetworkRefresh( jsonObj );
}
UpdateFromRefresh( source, message.TableData, message.Snapshot );
// This is making sure that components of a transform that shouldn't
// be synchronized are not updated from this message.
var newTransform = GameObject.Transform.TargetLocal;
var copyTransform = newTransform;
var flags = GameObject.Network.Flags;
if ( (flags & NetworkFlags.NoPositionSync) != 0 )
copyTransform.Position = oldTransform.Position;
if ( (flags & NetworkFlags.NoRotationSync) != 0 )
copyTransform.Rotation = oldTransform.Rotation;
if ( (flags & NetworkFlags.NoScaleSync) != 0 )
copyTransform.Scale = oldTransform.Scale;
if ( copyTransform != newTransform )
GameObject.Transform.Local = copyTransform;
}
internal void UpdateFromRefresh( Connection source, byte[] tableData, byte[] snapshotData )
{
RegisterPropertiesRecursive();
var system = SceneNetworkSystem.Instance;
if ( system is null ) return;
ReadDataTable( tableData );
var bs = ByteStream.CreateReader( snapshotData );
system.DeltaSnapshots.OnDeltaSnapshot( source, bs );
bs.Dispose();
}
internal void OnCreateMessage( ObjectCreateMsg msg )
{
LocalSnapshotState.Version = msg.SnapshotVersion;
var parent = GameObject.Scene.Directory.FindByGuid( msg.Parent );
GameObject.Transform.SetLocalTransformFast( msg.Transform );
GameObject.SetParentFromNetwork( parent );
GameObject.Enabled = msg.Enabled;
ReadDataTable( msg.TableData );
}
bool IDeltaSnapshot.OnSnapshot( Connection source, DeltaSnapshot snapshot )
{
// Don't process this if the source connection does not have control, and they
// are not the host.
if ( !HasControl( source ) && !source.IsHost )
return false;
// Conna: only what we regard as the owner can modify this shit.
if ( HasControl( source ) )
{
snapshot.TryGetValue<bool>( SnapshotInterpolationSlot, out var clearInterpolation );
var didTransformChange = false;
var transform = GameObject.Transform.TargetLocal;
if ( snapshot.TryGetValue<Vector3>( SnapshotPositionSlot, out var position ) )
{
didTransformChange = true;
transform.Position = position;
}
if ( snapshot.TryGetValue<Rotation>( SnapshotRotationSlot, out var rotation ) )
{
didTransformChange = true;
transform.Rotation = rotation;
}
if ( snapshot.TryGetValue<Vector3>( SnapshotScaleSlot, out var scale ) )
{
didTransformChange = true;
transform.Scale = scale;
}
if ( didTransformChange )
{
GameObject.Transform.FromNetwork( transform, clearInterpolation );
}
else if ( clearInterpolation )
{
GameObject.Transform.ClearLocalInterpolation();
}
if ( snapshot.TryGetValue<bool>( SnapshotEnabledSlot, out var enabled ) )
{
GameObject.Enabled = enabled;
}
}
dataTable.ReadSnapshot( source, snapshot );
return true;
}
/// <summary>
/// Whether the specified <see cref="Connection"/> has control over this networked object. A connection
/// has control if the object is unowned, and they are the host, or if they own it directly.
/// </summary>
internal bool HasControl( Connection c )
{
if ( IsUnowned )
return c.IsHost;
if ( c == Connection.Local )
return IsOwner;
return c.Id == Owner;
}
void OnOwnerChanged( Guid newOwner, Guid prevOwner )
{
var wasOwner = (prevOwner == Connection.Local.Id) || (prevOwner == Guid.Empty && Networking.IsHost);
var isOwner = (newOwner == Connection.Local.Id) || (newOwner == Guid.Empty && Networking.IsHost);
var newConnection = Connection.Find( newOwner );
var oldConnection = Connection.Find( prevOwner );
// Conna: clear interpolation when ownership changes.
GameObject.Transform.ClearLocalInterpolation();
IGameObjectNetworkEvents.PostToGameObject( GameObject, x => x.NetworkOwnerChanged( newConnection, oldConnection ) );
if ( wasOwner && !isOwner )
{
IGameObjectNetworkEvents.PostToGameObject( GameObject, x => x.StopControl() );
}
if ( isOwner && !wasOwner )
{
IGameObjectNetworkEvents.PostToGameObject( GameObject, x => x.StartControl() );
}
var system = SceneNetworkSystem.Instance;
system?.DeltaSnapshots.ClearNetworkObject( this );
LocalSnapshotState.ClearConnections();
if ( !isOwner )
return;
GameObject.IsNetworkCulled = false;
GameObject.UpdateNetworkCulledState();
}
}