This repository has been archived on 2025-04-22. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs
Unity Technologies 143a6cbd34 com.unity.netcode.gameobjects@2.0.0-exp.2
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).

## [2.0.0-exp.2] - 2024-04-02

### Added
- Added updates to all internal messages to account for a distributed authority network session connection.  (#2863)
- Added `NetworkRigidbodyBase` that provides users with a more customizable network rigidbody, handles both `Rigidbody` and `Rigidbody2D`, and provides an option to make `NetworkTransform` use the rigid body for motion.  (#2863)
  - For a customized `NetworkRigidbodyBase` class:
    - `NetworkRigidbodyBase.AutoUpdateKinematicState` provides control on whether the kinematic setting will be automatically set or not when ownership changes.
    - `NetworkRigidbodyBase.AutoSetKinematicOnDespawn` provides control on whether isKinematic will automatically be set to true when the associated `NetworkObject` is despawned.
    - `NetworkRigidbodyBase.Initialize` is a protected method that, when invoked, will initialize the instance. This includes options to:
      - Set whether using a `RigidbodyTypes.Rigidbody` or `RigidbodyTypes.Rigidbody2D`.
      - Includes additional optional parameters to set the `NetworkTransform`, `Rigidbody`, and `Rigidbody2d` to use.
  - Provides additional public methods:
    - `NetworkRigidbodyBase.GetPosition` to return the position of the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting).
    - `NetworkRigidbodyBase.GetRotation` to return the rotation of the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting).
    - `NetworkRigidbodyBase.MovePosition` to move to the position of the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting).
    - `NetworkRigidbodyBase.MoveRotation` to move to the rotation of the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting).
    - `NetworkRigidbodyBase.Move` to move to the position and rotation of the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting).
    - `NetworkRigidbodyBase.Move` to move to the position and rotation of the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting).
    - `NetworkRigidbodyBase.SetPosition` to set the position of the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting).
    - `NetworkRigidbodyBase.SetRotation` to set the rotation of the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting).
    - `NetworkRigidbodyBase.ApplyCurrentTransform` to set the position and rotation of the `Rigidbody` or `Rigidbody2d` based on the associated `GameObject` transform (depending upon its initialized setting).
    - `NetworkRigidbodyBase.WakeIfSleeping` to wake up the rigid body if sleeping.
    - `NetworkRigidbodyBase.SleepRigidbody` to put the rigid body to sleep.
    - `NetworkRigidbodyBase.IsKinematic` to determine if the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting) is currently kinematic.
    - `NetworkRigidbodyBase.SetIsKinematic` to set the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting) current kinematic state.
    - `NetworkRigidbodyBase.ResetInterpolation` to reset the `Rigidbody` or `Rigidbody2d` (depending upon its initialized setting) back to its original interpolation value when initialized.
  - Now includes a `MonoBehaviour.FixedUpdate` implementation that will update the assigned `NetworkTransform` when `NetworkRigidbodyBase.UseRigidBodyForMotion` is true. (#2863)
- Added `RigidbodyContactEventManager` that provides a more optimized way to process collision enter and collision stay events as opposed to the `Monobehaviour` approach. (#2863)
  - Can be used in client-server and distributed authority modes, but is particularly useful in distributed authority.
- Added rigid body motion updates to `NetworkTransform` which allows users to set interolation on rigid bodies. (#2863)
  - Extrapolation is only allowed on authoritative instances, but custom class derived from `NetworkRigidbodyBase` or `NetworkRigidbody` or `NetworkRigidbody2D` automatically switches non-authoritative instances to interpolation if set to extrapolation.
- Added distributed authority mode support to `NetworkAnimator`. (#2863)
- Added session mode selection to `NetworkManager` inspector view. (#2863)
- Added distributed authority permissions feature. (#2863)
- Added distributed authority mode specific `NetworkObject` permissions flags (Distributable, Transferable, and RequestRequired). (#2863)
- Added distributed authority mode specific `NetworkObject.SetOwnershipStatus` method that applies one or more `NetworkObject` instance's ownership flags. If updated when spawned, the ownership permission changes are synchronized with the other connected clients. (#2863)
- Added distributed authority mode specific `NetworkObject.RemoveOwnershipStatus` method that removes one or more `NetworkObject` instance's ownership flags. If updated when spawned, the ownership permission changes are synchronized with the other connected clients. (#2863)
- Added distributed authority mode specific `NetworkObject.HasOwnershipStatus` method that will return (true or false) whether one or more ownership flags is set. (#2863)
- Added distributed authority mode specific `NetworkObject.SetOwnershipLock` method that locks ownership of a `NetworkObject` to prevent ownership from changing until the current owner releases the lock. (#2863)
- Added distributed authority mode specific `NetworkObject.RequestOwnership` method that sends an ownership request to the current owner of a spawned `NetworkObject` instance. (#2863)
- Added distributed authority mode specific `NetworkObject.OnOwnershipRequested` callback handler that is invoked on the owner/authoritative side when a non-owner requests ownership. Depending upon the boolean returned value depends upon whether the request is approved or denied. (#2863)
- Added distributed authority mode specific `NetworkObject.OnOwnershipRequestResponse` callback handler that is invoked when a non-owner's request has been processed. This callback includes a `NetworkObjet.OwnershipRequestResponseStatus` response parameter that describes whether the request was approved or the reason why it was not approved. (#2863)
- Added distributed authority mode specific `NetworkObject.DeferDespawn` method that defers the despawning of `NetworkObject` instances on non-authoritative clients based on the tick offset parameter. (#2863)
- Added distributed authority mode specific `NetworkObject.OnDeferredDespawnComplete` callback handler that can be used to further control when deferring the despawning of a `NetworkObject` on non-authoritative instances. (#2863)
- Added `NetworkClient.SessionModeType` as one way to determine the current session mode of the network session a client is connected to. (#2863)
- Added distributed authority mode specific `NetworkClient.IsSessionOwner` property to determine if the current local client is the current session owner of a distributed authority session. (#2863)
- Added distributed authority mode specific client side spawning capabilities. When running in distributed authority mode, clients can instantiate and spawn `NetworkObject` instances (the local client is authomatically the owner of the spawned object). (#2863)
  - This is useful to better visually synchronize owner authoritative motion models and newly spawned `NetworkObject` instances (i.e. projectiles for example).
- Added distributed authority mode specific client side player spawning capabilities. Clients will automatically spawn their associated player object locally. (#2863)
- Added distributed authority mode specific `NetworkConfig.AutoSpawnPlayerPrefabClientSide` property (default is true) to provide control over the automatic spawning of player prefabs on the local client side. (#2863)
- Added distributed authority mode specific `NetworkManager.OnFetchLocalPlayerPrefabToSpawn` callback that, when assigned, will allow the local client to provide the player prefab to be spawned for the local client. (#2863)
  - This is only invoked if the `NetworkConfig.AutoSpawnPlayerPrefabClientSide` property is set to true.
- Added distributed authority mode specific `NetworkBehaviour.HasAuthority` property that determines if the local client has authority over the associated `NetworkObject` instance (typical use case is within a `NetworkBehaviour` script much like that of `IsServer` or `IsClient`). (#2863)
- Added distributed authority mode specific `NetworkBehaviour.IsSessionOwner` property that determines if the local client is the session owner (typical use case would be to determine if the local client can has scene management authority within a `NetworkBehaviour` script). (#2863)
- Added support for distributed authority mode scene management where the currently assigned session owner can start scene events (i.e. scene loading and scene unloading). (#2863)

### Fixed

- Fixed issue where the host was not invoking `OnClientDisconnectCallback` for its own local client when internally shutting down. (#2822)
- Fixed issue where NetworkTransform could potentially attempt to "unregister" a named message prior to it being registered. (#2807)
- Fixed issue where in-scene placed `NetworkObject`s with complex nested children `NetworkObject`s (more than one child in depth) would not synchronize properly if WorldPositionStays was set to true. (#2796)

### Changed
- Changed client side awareness of other clients is now the same as a server or host. (#2863)
- Changed `NetworkManager.ConnectedClients` can now be accessed by both server and clients. (#2863)
- Changed `NetworkManager.ConnectedClientsList` can now be accessed by both server and clients. (#2863)
- Changed `NetworkTransform` defaults to owner authoritative when connected to a distributed authority session. (#2863)
- Changed `NetworkVariable` defaults to owner write and everyone read permissions when connected to a distributed authority session (even if declared with server read or write permissions).  (#2863)
- Changed `NetworkObject` no longer implements the `MonoBehaviour.Update` method in order to determine whether a `NetworkObject` instance has been migrated to a different scene. Instead, only `NetworkObjects` with the `SceneMigrationSynchronization` property set will be updated internally during the `NetworkUpdateStage.PostLateUpdate` by `NetworkManager`. (#2863)
- Changed `NetworkManager` inspector view layout where properties are now organized by category. (#2863)
- Changed `NetworkTransform` to now use `NetworkTransformMessage` as opposed to named messages for NetworkTransformState updates. (#2810)
- Changed `CustomMessageManager` so it no longer attempts to register or "unregister" a null or empty string and will log an error if this condition occurs. (#2807)
2024-04-02 00:00:00 +00:00

1345 lines
59 KiB
C#

using System;
using System.Collections.Generic;
using Unity.Collections;
using UnityEngine;
namespace Unity.Netcode
{
public class RpcException : Exception
{
public RpcException(string message) : base(message)
{
}
}
/// <summary>
/// The base class to override to write network code. Inherits MonoBehaviour
/// </summary>
public abstract class NetworkBehaviour : MonoBehaviour
{
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `public`
internal delegate void RpcReceiveHandler(NetworkBehaviour behaviour, FastBufferReader reader, __RpcParams parameters);
// RuntimeAccessModifiersILPP will make this `public`
internal static readonly Dictionary<Type, Dictionary<uint, RpcReceiveHandler>> __rpc_func_table = new Dictionary<Type, Dictionary<uint, RpcReceiveHandler>>();
#if DEVELOPMENT_BUILD || UNITY_EDITOR
// RuntimeAccessModifiersILPP will make this `public`
internal static readonly Dictionary<Type, Dictionary<uint, string>> __rpc_name_table = new Dictionary<Type, Dictionary<uint, string>>();
#endif
// RuntimeAccessModifiersILPP will make this `protected`
internal enum __RpcExecStage
{
// Technically will overlap with None and Server
// but it doesn't matter since we don't use None and Server
Send = 0,
Execute = 1,
// Backward compatibility, not used...
None = 0,
Server = 1,
Client = 2,
}
// NetworkBehaviourILPP will override this in derived classes to return the name of the concrete type
internal virtual string __getTypeName() => nameof(NetworkBehaviour);
[NonSerialized]
// RuntimeAccessModifiersILPP will make this `protected`
internal __RpcExecStage __rpc_exec_stage = __RpcExecStage.Send;
#pragma warning restore IDE1006 // restore naming rule violation check
private const int k_RpcMessageDefaultSize = 1024; // 1k
private const int k_RpcMessageMaximumSize = 1024 * 64; // 64k
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `protected`
internal FastBufferWriter __beginSendServerRpc(uint rpcMethodId, ServerRpcParams serverRpcParams, RpcDelivery rpcDelivery)
#pragma warning restore IDE1006 // restore naming rule violation check
{
return new FastBufferWriter(k_RpcMessageDefaultSize, Allocator.Temp, k_RpcMessageMaximumSize);
}
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `protected`
internal void __endSendServerRpc(ref FastBufferWriter bufferWriter, uint rpcMethodId, ServerRpcParams serverRpcParams, RpcDelivery rpcDelivery)
#pragma warning restore IDE1006 // restore naming rule violation check
{
var networkManager = NetworkManager;
var serverRpcMessage = new ServerRpcMessage
{
Metadata = new RpcMetadata
{
NetworkObjectId = NetworkObjectId,
NetworkBehaviourId = NetworkBehaviourId,
NetworkRpcMethodId = rpcMethodId,
},
WriteBuffer = bufferWriter
};
NetworkDelivery networkDelivery;
switch (rpcDelivery)
{
default:
case RpcDelivery.Reliable:
networkDelivery = NetworkDelivery.ReliableFragmentedSequenced;
break;
case RpcDelivery.Unreliable:
if (bufferWriter.Length > networkManager.MessageManager.NonFragmentedMessageMaxSize)
{
throw new OverflowException("RPC parameters are too large for unreliable delivery.");
}
networkDelivery = NetworkDelivery.Unreliable;
break;
}
var rpcWriteSize = 0;
// Authority just no ops and sends to itself
// Client-Server: Only the server-host sends to self
if (IsServer)
{
using var tempBuffer = new FastBufferReader(bufferWriter, Allocator.Temp);
var context = new NetworkContext
{
SenderId = NetworkManager.ServerClientId,
Timestamp = networkManager.RealTimeProvider.RealTimeSinceStartup,
SystemOwner = networkManager,
// header information isn't valid since it's not a real message.
// RpcMessage doesn't access this stuff so it's just left empty.
Header = new NetworkMessageHeader(),
SerializedHeaderSize = 0,
MessageSize = 0
};
serverRpcMessage.ReadBuffer = tempBuffer;
serverRpcMessage.Handle(ref context);
rpcWriteSize = tempBuffer.Length;
}
else
{
rpcWriteSize = NetworkManager.ConnectionManager.SendMessage(ref serverRpcMessage, networkDelivery, NetworkManager.ServerClientId);
}
bufferWriter.Dispose();
#if DEVELOPMENT_BUILD || UNITY_EDITOR
if (__rpc_name_table[GetType()].TryGetValue(rpcMethodId, out var rpcMethodName))
{
NetworkManager.NetworkMetrics.TrackRpcSent(
NetworkManager.ServerClientId,
NetworkObject,
rpcMethodName,
__getTypeName(),
rpcWriteSize);
}
#endif
}
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `protected`
internal FastBufferWriter __beginSendClientRpc(uint rpcMethodId, ClientRpcParams clientRpcParams, RpcDelivery rpcDelivery)
#pragma warning restore IDE1006 // restore naming rule violation check
{
return new FastBufferWriter(k_RpcMessageDefaultSize, Allocator.Temp, k_RpcMessageMaximumSize);
}
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `protected`
internal void __endSendClientRpc(ref FastBufferWriter bufferWriter, uint rpcMethodId, ClientRpcParams clientRpcParams, RpcDelivery rpcDelivery)
#pragma warning restore IDE1006 // restore naming rule violation check
{
var networkManager = NetworkManager;
var clientRpcMessage = new ClientRpcMessage
{
Metadata = new RpcMetadata
{
NetworkObjectId = NetworkObjectId,
NetworkBehaviourId = NetworkBehaviourId,
NetworkRpcMethodId = rpcMethodId,
},
WriteBuffer = bufferWriter
};
NetworkDelivery networkDelivery;
switch (rpcDelivery)
{
default:
case RpcDelivery.Reliable:
networkDelivery = NetworkDelivery.ReliableFragmentedSequenced;
break;
case RpcDelivery.Unreliable:
if (bufferWriter.Length > networkManager.MessageManager.NonFragmentedMessageMaxSize)
{
throw new OverflowException("RPC parameters are too large for unreliable delivery.");
}
networkDelivery = NetworkDelivery.Unreliable;
break;
}
var rpcWriteSize = 0;
// We check to see if we need to shortcut for the case where we are the host/server and we can send a clientRPC
// to ourself. Sadly we have to figure that out from the list of clientIds :(
bool shouldInvokeLocally = false;
if (clientRpcParams.Send.TargetClientIds != null)
{
foreach (var targetClientId in clientRpcParams.Send.TargetClientIds)
{
if (targetClientId == NetworkManager.ServerClientId)
{
shouldInvokeLocally = true;
continue;
}
// Check to make sure we are sending to only observers, if not log an error.
if (networkManager.LogLevel >= LogLevel.Error && !NetworkObject.Observers.Contains(targetClientId))
{
NetworkLog.LogError(GenerateObserverErrorMessage(clientRpcParams, targetClientId));
}
}
rpcWriteSize = NetworkManager.ConnectionManager.SendMessage(ref clientRpcMessage, networkDelivery, in clientRpcParams.Send.TargetClientIds);
}
else if (clientRpcParams.Send.TargetClientIdsNativeArray != null)
{
foreach (var targetClientId in clientRpcParams.Send.TargetClientIdsNativeArray)
{
if (targetClientId == NetworkManager.ServerClientId)
{
shouldInvokeLocally = true;
continue;
}
// Check to make sure we are sending to only observers, if not log an error.
if (networkManager.LogLevel >= LogLevel.Error && !NetworkObject.Observers.Contains(targetClientId))
{
NetworkLog.LogError(GenerateObserverErrorMessage(clientRpcParams, targetClientId));
}
}
rpcWriteSize = NetworkManager.ConnectionManager.SendMessage(ref clientRpcMessage, networkDelivery, clientRpcParams.Send.TargetClientIdsNativeArray.Value);
}
else
{
var observerEnumerator = NetworkObject.Observers.GetEnumerator();
while (observerEnumerator.MoveNext())
{
// Skip over the host
if (IsHost && observerEnumerator.Current == NetworkManager.LocalClientId)
{
shouldInvokeLocally = true;
continue;
}
rpcWriteSize = NetworkManager.ConnectionManager.SendMessage(ref clientRpcMessage, networkDelivery, observerEnumerator.Current);
}
}
// If we are a server/host then we just no op and send to ourself
if (shouldInvokeLocally)
{
using var tempBuffer = new FastBufferReader(bufferWriter, Allocator.Temp);
var context = new NetworkContext
{
SenderId = NetworkManager.ServerClientId,
Timestamp = networkManager.RealTimeProvider.RealTimeSinceStartup,
SystemOwner = networkManager,
// header information isn't valid since it's not a real message.
// RpcMessage doesn't access this stuff so it's just left empty.
Header = new NetworkMessageHeader(),
SerializedHeaderSize = 0,
MessageSize = 0
};
clientRpcMessage.ReadBuffer = tempBuffer;
clientRpcMessage.Handle(ref context);
}
bufferWriter.Dispose();
#if DEVELOPMENT_BUILD || UNITY_EDITOR
if (__rpc_name_table[GetType()].TryGetValue(rpcMethodId, out var rpcMethodName))
{
if (clientRpcParams.Send.TargetClientIds != null)
{
foreach (var targetClientId in clientRpcParams.Send.TargetClientIds)
{
networkManager.NetworkMetrics.TrackRpcSent(
targetClientId,
NetworkObject,
rpcMethodName,
__getTypeName(),
rpcWriteSize);
}
}
else if (clientRpcParams.Send.TargetClientIdsNativeArray != null)
{
foreach (var targetClientId in clientRpcParams.Send.TargetClientIdsNativeArray)
{
networkManager.NetworkMetrics.TrackRpcSent(
targetClientId,
NetworkObject,
rpcMethodName,
__getTypeName(),
rpcWriteSize);
}
}
else
{
var observerEnumerator = NetworkObject.Observers.GetEnumerator();
while (observerEnumerator.MoveNext())
{
networkManager.NetworkMetrics.TrackRpcSent(
observerEnumerator.Current,
NetworkObject,
rpcMethodName,
__getTypeName(),
rpcWriteSize);
}
}
}
#endif
}
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `protected`
internal FastBufferWriter __beginSendRpc(uint rpcMethodId, RpcParams rpcParams, RpcAttribute.RpcAttributeParams attributeParams, SendTo defaultTarget, RpcDelivery rpcDelivery)
#pragma warning restore IDE1006 // restore naming rule violation check
{
if (attributeParams.RequireOwnership && !IsOwner)
{
throw new RpcException("This RPC can only be sent by its owner.");
}
return new FastBufferWriter(k_RpcMessageDefaultSize, Allocator.Temp, k_RpcMessageMaximumSize);
}
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `protected`
internal void __endSendRpc(ref FastBufferWriter bufferWriter, uint rpcMethodId, RpcParams rpcParams, RpcAttribute.RpcAttributeParams attributeParams, SendTo defaultTarget, RpcDelivery rpcDelivery)
#pragma warning restore IDE1006 // restore naming rule violation check
{
var rpcMessage = new RpcMessage
{
Metadata = new RpcMetadata
{
NetworkObjectId = NetworkObjectId,
NetworkBehaviourId = NetworkBehaviourId,
NetworkRpcMethodId = rpcMethodId,
},
SenderClientId = NetworkManager.LocalClientId,
WriteBuffer = bufferWriter
};
NetworkDelivery networkDelivery;
switch (rpcDelivery)
{
default:
case RpcDelivery.Reliable:
networkDelivery = NetworkDelivery.ReliableFragmentedSequenced;
break;
case RpcDelivery.Unreliable:
if (bufferWriter.Length > NetworkManager.MessageManager.NonFragmentedMessageMaxSize)
{
throw new OverflowException("RPC parameters are too large for unreliable delivery.");
}
networkDelivery = NetworkDelivery.Unreliable;
break;
}
if (rpcParams.Send.Target == null)
{
switch (defaultTarget)
{
case SendTo.Everyone:
rpcParams.Send.Target = RpcTarget.Everyone;
break;
case SendTo.Owner:
rpcParams.Send.Target = RpcTarget.Owner;
break;
case SendTo.Server:
rpcParams.Send.Target = RpcTarget.Server;
break;
case SendTo.NotServer:
rpcParams.Send.Target = RpcTarget.NotServer;
break;
case SendTo.NotMe:
rpcParams.Send.Target = RpcTarget.NotMe;
break;
case SendTo.NotOwner:
rpcParams.Send.Target = RpcTarget.NotOwner;
break;
case SendTo.Me:
rpcParams.Send.Target = RpcTarget.Me;
break;
case SendTo.ClientsAndHost:
rpcParams.Send.Target = RpcTarget.ClientsAndHost;
break;
case SendTo.Authority:
rpcParams.Send.Target = RpcTarget.Authority;
break;
case SendTo.NotAuthority:
rpcParams.Send.Target = RpcTarget.NotAuthority;
break;
case SendTo.SpecifiedInParams:
throw new RpcException("This method requires a runtime-specified send target.");
}
}
else if (defaultTarget != SendTo.SpecifiedInParams && !attributeParams.AllowTargetOverride)
{
throw new RpcException("Target override is not allowed for this method.");
}
if (rpcParams.Send.LocalDeferMode == LocalDeferMode.Default)
{
rpcParams.Send.LocalDeferMode = attributeParams.DeferLocal ? LocalDeferMode.Defer : LocalDeferMode.SendImmediate;
}
rpcParams.Send.Target.Send(this, ref rpcMessage, networkDelivery, rpcParams);
bufferWriter.Dispose();
}
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `protected`
internal static NativeList<T> __createNativeList<T>() where T : unmanaged
#pragma warning restore IDE1006 // restore naming rule violation check
{
return new NativeList<T>(Allocator.Temp);
}
internal string GenerateObserverErrorMessage(ClientRpcParams clientRpcParams, ulong targetClientId)
{
var containerNameHoldingId = clientRpcParams.Send.TargetClientIds != null ? nameof(ClientRpcParams.Send.TargetClientIds) : nameof(ClientRpcParams.Send.TargetClientIdsNativeArray);
return $"Sending ClientRpc to non-observer! {containerNameHoldingId} contains clientId {targetClientId} that is not an observer!";
}
/// <summary>
/// Gets the NetworkManager that owns this NetworkBehaviour instance
/// See note around `NetworkObject` for how there is a chicken / egg problem when we are not initialized
/// </summary>
public NetworkManager NetworkManager
{
get
{
if (NetworkObject?.NetworkManager != null)
{
return NetworkObject?.NetworkManager;
}
return NetworkManager.Singleton;
}
}
// This erroneously tries to simplify these method references but the docs do not pick it up correctly
// because they try to resolve it on the field rather than the class of the same name.
#pragma warning disable IDE0001
/// <summary>
/// Provides access to the various <see cref="SendTo"/> targets at runtime, as well as
/// runtime-bound targets like <see cref="Unity.Netcode.RpcTarget.Single"/>,
/// <see cref="Unity.Netcode.RpcTarget.Group(NativeArray{ulong})"/>,
/// <see cref="Unity.Netcode.RpcTarget.Group(NativeList{ulong})"/>,
/// <see cref="Unity.Netcode.RpcTarget.Group(ulong[])"/>,
/// <see cref="Unity.Netcode.RpcTarget.Group{T}(T)"/>, <see cref="Unity.Netcode.RpcTarget.Not(ulong)"/>,
/// <see cref="Unity.Netcode.RpcTarget.Not(NativeArray{ulong})"/>,
/// <see cref="Unity.Netcode.RpcTarget.Not(NativeList{ulong})"/>,
/// <see cref="Unity.Netcode.RpcTarget.Not(ulong[])"/>, and
/// <see cref="Unity.Netcode.RpcTarget.Not{T}(T)"/>
/// </summary>
#pragma warning restore IDE0001
public RpcTarget RpcTarget => NetworkManager.RpcTarget;
/// <summary>
/// If a NetworkObject is assigned, it will return whether or not this NetworkObject
/// is the local player object. If no NetworkObject is assigned it will always return false.
/// </summary>
public bool IsLocalPlayer { get; private set; }
/// <summary>
/// Gets if the object is owned by the local player or if the object is the local player object
/// </summary>
public bool IsOwner { get; internal set; }
/// <summary>
/// Gets if we are executing as server
/// </summary>
public bool IsServer { get; private set; }
/// <summary>
/// Determines if the local client has authority over the associated NetworkObject
/// Client-Server: This will return true if IsServer or IsHost
/// Distributed Authority: This will return true if IsOwner
/// </summary>
public bool HasAuthority { get; internal set; }
internal NetworkClient LocalClient { get; private set; }
/// <summary>
/// Gets if the client is the distributed authority mode session owner
/// </summary>
public bool IsSessionOwner
{
get
{
if (LocalClient == null)
{
return false;
}
return LocalClient.IsSessionOwner;
}
}
/// <summary>
/// Gets if the server (local or remote) is a host - i.e., also a client
/// </summary>
public bool ServerIsHost { get; private set; }
/// <summary>
/// Gets if we are executing as client
/// </summary>
public bool IsClient { get; private set; }
/// <summary>
/// Gets if we are executing as Host, I.E Server and Client
/// </summary>
public bool IsHost { get; private set; }
/// <summary>
/// Gets Whether or not the object has a owner
/// </summary>
public bool IsOwnedByServer { get; internal set; }
/// <summary>
/// Used to determine if it is safe to access NetworkObject and NetworkManager from within a NetworkBehaviour component
/// Primarily useful when checking NetworkObject/NetworkManager properties within FixedUpate
/// </summary>
public bool IsSpawned { get; internal set; }
internal bool IsBehaviourEditable()
{
// Only server can MODIFY. So allow modification if network is either not running or we are server
return !m_NetworkObject ||
m_NetworkObject.NetworkManager == null ||
m_NetworkObject.NetworkManager.IsListening == false ||
m_NetworkObject.NetworkManager.IsServer;
}
/// TODO: this needs an overhaul. It's expensive, it's ja little naive in how it looks for networkObject in
/// its parent and worst, it creates a puzzle if you are a NetworkBehaviour wanting to see if you're live or not
/// (e.g. editor code). All you want to do is find out if NetworkManager is null, but to do that you
/// need NetworkObject, but if you try and grab NetworkObject and NetworkManager isn't up you'll get
/// the warning below. This is why IsBehaviourEditable had to be created. Matt was going to re-do
/// how NetworkObject works but it was close to the release and too risky to change
/// <summary>
/// Gets the NetworkObject that owns this NetworkBehaviour instance
/// </summary>
public NetworkObject NetworkObject
{
get
{
if (m_NetworkObject != null)
{
return m_NetworkObject;
}
try
{
m_NetworkObject = GetComponentInParent<NetworkObject>();
}
catch (Exception)
{
return null;
}
// ShutdownInProgress check:
// This prevents an edge case scenario where the NetworkManager is shutting down but user code
// in Update and/or in FixedUpdate could still be checking NetworkBehaviour.NetworkObject directly (i.e. does it exist?)
// or NetworkBehaviour.IsSpawned (i.e. to early exit if not spawned) which, in turn, could generate several Warning messages
// per spawned NetworkObject. Checking for ShutdownInProgress prevents these unnecessary LogWarning messages.
// We must check IsSpawned, otherwise a warning will be logged under certain valid conditions (see OnDestroy)
if (IsSpawned && m_NetworkObject == null && (NetworkManager.Singleton == null || !NetworkManager.Singleton.ShutdownInProgress))
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"Could not get {nameof(NetworkObject)} for the {nameof(NetworkBehaviour)}. Are you missing a {nameof(NetworkObject)} component?");
}
}
return m_NetworkObject;
}
}
/// <summary>
/// Gets whether or not this NetworkBehaviour instance has a NetworkObject owner.
/// </summary>
public bool HasNetworkObject => NetworkObject != null;
private NetworkObject m_NetworkObject = null;
/// <summary>
/// Gets the NetworkId of the NetworkObject that owns this NetworkBehaviour
/// </summary>
public ulong NetworkObjectId { get; internal set; }
/// <summary>
/// Gets NetworkId for this NetworkBehaviour from the owner NetworkObject
/// </summary>
public ushort NetworkBehaviourId { get; internal set; }
/// <summary>
/// Internally caches the Id of this behaviour in a NetworkObject. Makes look-up faster
/// </summary>
internal ushort NetworkBehaviourIdCache = 0;
/// <summary>
/// Returns a the NetworkBehaviour with a given BehaviourId for the current NetworkObject
/// </summary>
/// <param name="behaviourId">The behaviourId to return</param>
/// <returns>Returns NetworkBehaviour with given behaviourId</returns>
protected NetworkBehaviour GetNetworkBehaviour(ushort behaviourId)
{
return NetworkObject.GetNetworkBehaviourAtOrderIndex(behaviourId);
}
/// <summary>
/// Gets the ClientId that owns the NetworkObject
/// </summary>
public ulong OwnerClientId { get; internal set; }
/// <summary>
/// Updates properties with network session related
/// dependencies such as a NetworkObject's spawned
/// state or NetworkManager's session state.
/// </summary>
internal void UpdateNetworkProperties()
{
var networkObject = NetworkObject;
// Set NetworkObject dependent properties
if (networkObject != null)
{
var networkManager = NetworkManager;
// Set identification related properties
NetworkObjectId = networkObject.NetworkObjectId;
IsLocalPlayer = networkObject.IsLocalPlayer;
// This is "OK" because GetNetworkBehaviourOrderIndex uses the order of
// NetworkObject.ChildNetworkBehaviours which is set once when first
// accessed.
NetworkBehaviourId = networkObject.GetNetworkBehaviourOrderIndex(this);
// Set ownership related properties
IsOwnedByServer = networkObject.IsOwnedByServer;
IsOwner = networkObject.IsOwner;
OwnerClientId = networkObject.OwnerClientId;
// Set NetworkManager dependent properties
if (networkManager != null)
{
IsHost = networkManager.IsListening && networkManager.IsHost;
IsClient = networkManager.IsListening && networkManager.IsClient;
IsServer = networkManager.IsListening && networkManager.IsServer;
LocalClient = networkManager.LocalClient;
HasAuthority = networkObject.HasAuthority;
ServerIsHost = networkManager.IsListening && networkManager.ServerIsHost;
}
}
else // Shouldn't happen, but if so then set the properties to their default value;
{
OwnerClientId = NetworkObjectId = default;
IsOwnedByServer = IsOwner = IsHost = IsClient = IsServer = ServerIsHost = default;
NetworkBehaviourId = default;
LocalClient = default;
HasAuthority = default;
}
}
/// <summary>
/// Distributed Authority Mode Only
/// Invoked only on the authority instance when a <see cref="NetworkObject"/> is deferring its despawn on non-authoritative instances.
/// </summary>
/// <remarks>
/// See also: <see cref="NetworkObject.DeferDespawn(int, bool)"/>
/// </remarks>
/// <param name="despawnTick">the future network tick that the <see cref="NetworkObject"/> will be despawned on non-authoritative instances</param>
public virtual void OnDeferringDespawn(int despawnTick) { }
/// <summary>
/// Gets called when the <see cref="NetworkObject"/> gets spawned, message handlers are ready to be registered and the network is setup.
/// </summary>
public virtual void OnNetworkSpawn() { }
/// <summary>
/// Gets called when the <see cref="NetworkObject"/> gets despawned. Is called both on the server and clients.
/// </summary>
public virtual void OnNetworkDespawn() { }
internal void InternalOnNetworkSpawn()
{
IsSpawned = true;
InitializeVariables();
UpdateNetworkProperties();
}
internal void VisibleOnNetworkSpawn()
{
try
{
OnNetworkSpawn();
}
catch (Exception e)
{
Debug.LogException(e);
}
InitializeVariables();
if (NetworkObject.HasAuthority)
{
// Since we just spawned the object and since user code might have modified their NetworkVariable, esp.
// NetworkList, we need to mark the object as free of updates.
// This should happen for all objects on the machine triggering the spawn.
PostNetworkVariableWrite(true);
}
}
internal void InternalOnNetworkDespawn()
{
IsSpawned = false;
UpdateNetworkProperties();
try
{
OnNetworkDespawn();
}
catch (Exception e)
{
Debug.LogException(e);
}
}
/// <summary>
/// Gets called when the local client gains ownership of this object
/// </summary>
public virtual void OnGainedOwnership() { }
internal void InternalOnGainedOwnership()
{
UpdateNetworkProperties();
OnGainedOwnership();
}
/// <summary>
/// Invoked on all clients, override this method to be notified of any
/// ownership changes (even if the instance was niether the previous or
/// newly assigned current owner).
/// </summary>
/// <param name="previous">the previous owner</param>
/// <param name="current">the current owner</param>
protected virtual void OnOwnershipChanged(ulong previous, ulong current)
{
}
internal void InternalOnOwnershipChanged(ulong previous, ulong current)
{
OnOwnershipChanged(previous, current);
}
/// <summary>
/// Gets called when we loose ownership of this object
/// </summary>
public virtual void OnLostOwnership() { }
internal void InternalOnLostOwnership()
{
UpdateNetworkProperties();
OnLostOwnership();
}
/// <summary>
/// Gets called when the parent NetworkObject of this NetworkBehaviour's NetworkObject has changed
/// </summary>
/// <param name="parentNetworkObject">the new <see cref="NetworkObject"/> parent</param>
public virtual void OnNetworkObjectParentChanged(NetworkObject parentNetworkObject) { }
private bool m_VarInit = false;
private readonly List<HashSet<int>> m_DeliveryMappedNetworkVariableIndices = new List<HashSet<int>>();
private readonly List<NetworkDelivery> m_DeliveryTypesForNetworkVariableGroups = new List<NetworkDelivery>();
// RuntimeAccessModifiersILPP will make this `protected`
internal readonly List<NetworkVariableBase> NetworkVariableFields = new List<NetworkVariableBase>();
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `protected`
internal virtual void __initializeVariables()
#pragma warning restore IDE1006 // restore naming rule violation check
{
// ILPP generates code for all NetworkBehaviour subtypes to initialize each type's network variables.
}
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `protected`
internal virtual void __initializeRpcs()
#pragma warning restore IDE1006 // restore naming rule violation check
{
// ILPP generates code for all NetworkBehaviour subtypes to initialize each type's RPCs.
}
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `protected`
internal void __registerRpc(uint hash, RpcReceiveHandler handler, string rpcMethodName)
#pragma warning restore IDE1006 // restore naming rule violation check
{
__rpc_func_table[GetType()][hash] = handler;
#if DEVELOPMENT_BUILD || UNITY_EDITOR
__rpc_name_table[GetType()][hash] = rpcMethodName;
#endif
}
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `protected`
// Using this method here because ILPP doesn't seem to let us do visibility modification on properties.
internal void __nameNetworkVariable(NetworkVariableBase variable, string varName)
#pragma warning restore IDE1006 // restore naming rule violation check
{
variable.Name = varName;
}
internal void InitializeVariables()
{
if (m_VarInit)
{
return;
}
m_VarInit = true;
if (!__rpc_func_table.ContainsKey(GetType()))
{
__rpc_func_table[GetType()] = new Dictionary<uint, RpcReceiveHandler>();
#if UNITY_EDITOR || DEVELOPMENT_BUILD
__rpc_name_table[GetType()] = new Dictionary<uint, string>();
#endif
__initializeRpcs();
}
__initializeVariables();
{
// Create index map for delivery types
var firstLevelIndex = new Dictionary<NetworkDelivery, int>();
int secondLevelCounter = 0;
for (int i = 0; i < NetworkVariableFields.Count; i++)
{
var networkDelivery = NetworkVariableBase.Delivery;
if (!firstLevelIndex.ContainsKey(networkDelivery))
{
firstLevelIndex.Add(networkDelivery, secondLevelCounter);
m_DeliveryTypesForNetworkVariableGroups.Add(networkDelivery);
secondLevelCounter++;
}
if (firstLevelIndex[networkDelivery] >= m_DeliveryMappedNetworkVariableIndices.Count)
{
m_DeliveryMappedNetworkVariableIndices.Add(new HashSet<int>());
}
m_DeliveryMappedNetworkVariableIndices[firstLevelIndex[networkDelivery]].Add(i);
}
}
}
internal void PreNetworkVariableWrite()
{
// reset our "which variables got written" data
NetworkVariableIndexesToReset.Clear();
NetworkVariableIndexesToResetSet.Clear();
}
internal void PostNetworkVariableWrite(bool forced = false)
{
if (forced)
{
// Mark every variable as no longer dirty. We just spawned the object and whatever the game code did
// during OnNetworkSpawn has been sent and needs to be cleared
for (int i = 0; i < NetworkVariableFields.Count; i++)
{
NetworkVariableFields[i].ResetDirty();
}
}
else
{
// mark any variables we wrote as no longer dirty
for (int i = 0; i < NetworkVariableIndexesToReset.Count; i++)
{
NetworkVariableFields[NetworkVariableIndexesToReset[i]].ResetDirty();
}
}
MarkVariablesDirty(false);
}
internal void PreVariableUpdate()
{
if (!m_VarInit)
{
InitializeVariables();
}
PreNetworkVariableWrite();
}
internal readonly List<int> NetworkVariableIndexesToReset = new List<int>();
internal readonly HashSet<int> NetworkVariableIndexesToResetSet = new HashSet<int>();
internal void NetworkVariableUpdate(ulong targetClientId)
{
if (!CouldHaveDirtyNetworkVariables())
{
return;
}
// Getting these ahead of time actually improves performance
var networkManager = NetworkManager;
var networkObject = NetworkObject;
var behaviourIndex = networkObject.GetNetworkBehaviourOrderIndex(this);
var messageManager = networkManager.MessageManager;
var connectionManager = networkManager.ConnectionManager;
for (int j = 0; j < m_DeliveryMappedNetworkVariableIndices.Count; j++)
{
var networkVariable = (NetworkVariableBase)null;
var shouldSend = false;
for (int k = 0; k < NetworkVariableFields.Count; k++)
{
networkVariable = NetworkVariableFields[k];
if (networkVariable.IsDirty() && networkVariable.CanClientRead(targetClientId))
{
shouldSend = true;
break;
}
}
// All of this is just to prevent the DA Host from re-sending a NetworkVariable update it received from the client owner
// If this NetworkManager is running as a DAHost:
// - Only when the write permissions is owner (to pass existing integration tests running as DAHost)
// - If the target client ID is the owner and the owner is not the local NetworkManager instance
// - **Special** As long as ownership did not just change and we are sending the new owner any dirty/updated NetworkVariables
// Under these conditions we should not send to the client
if (shouldSend && networkManager.DAHost && networkVariable.WritePerm == NetworkVariableWritePermission.Owner &&
networkObject.OwnerClientId == targetClientId && networkObject.OwnerClientId != networkManager.LocalClientId &&
networkObject.PreviousOwnerId == networkObject.OwnerClientId)
{
shouldSend = false;
}
if (!shouldSend)
{
continue;
}
var message = new NetworkVariableDeltaMessage
{
NetworkObjectId = NetworkObjectId,
NetworkBehaviourIndex = behaviourIndex,
NetworkBehaviour = this,
TargetClientId = targetClientId,
DeliveryMappedNetworkVariableIndex = m_DeliveryMappedNetworkVariableIndices[j]
};
// TODO: Serialization is where the IsDirty flag gets changed.
// Messages don't get sent from the server to itself, so if we're host and sending to ourselves,
// we still have to actually serialize the message even though we're not sending it, otherwise
// the dirty flag doesn't change properly. These two pieces should be decoupled at some point
// so we don't have to do this serialization work if we're not going to use the result.
if (IsServer && targetClientId == NetworkManager.ServerClientId)
{
var tmpWriter = new FastBufferWriter(messageManager.NonFragmentedMessageMaxSize, Allocator.Temp, messageManager.FragmentedMessageMaxSize);
using (tmpWriter)
{
message.Serialize(tmpWriter, message.Version);
}
}
else
{
connectionManager.SendMessage(ref message, m_DeliveryTypesForNetworkVariableGroups[j], targetClientId);
}
}
}
internal static bool LogSentVariableUpdateMessage;
private bool CouldHaveDirtyNetworkVariables()
{
// TODO: There should be a better way by reading one dirty variable vs. 'n'
for (int i = 0; i < NetworkVariableFields.Count; i++)
{
if (NetworkVariableFields[i].IsDirty())
{
return true;
}
}
return false;
}
internal void MarkVariablesDirty(bool dirty)
{
for (int j = 0; j < NetworkVariableFields.Count; j++)
{
NetworkVariableFields[j].SetDirty(dirty);
}
}
/// <summary>
/// Synchronizes by setting only the NetworkVariable field values that the client has permission to read.
/// Note: This is only invoked when first synchronizing a NetworkBehaviour (i.e. late join or spawned NetworkObject)
/// </summary>
/// <remarks>
/// When NetworkConfig.EnsureNetworkVariableLengthSafety is enabled each NetworkVariable field will be preceded
/// by the number of bytes written for that specific field.
/// </remarks>
internal void WriteNetworkVariableData(FastBufferWriter writer, ulong targetClientId)
{
var networkManager = NetworkManager;
if (networkManager.DistributedAuthorityMode)
{
writer.WriteValueSafe((ushort)NetworkVariableFields.Count);
}
if (NetworkVariableFields.Count == 0)
{
return;
}
// DANGO-TODO: Made some modifications here that overlap/won't play nice with EnsureNetworkVariableLenghtSafety.
// Worth either merging or more cleanly separating these codepaths.
for (int j = 0; j < NetworkVariableFields.Count; j++)
{
// Note: In distributed authority mode, all clients can read
if (NetworkVariableFields[j].CanClientRead(targetClientId))
{
if (networkManager.DistributedAuthorityMode)
{
writer.WriteValueSafe(NetworkVariableFields[j].Type);
}
if (networkManager.DistributedAuthorityMode || networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
{
var writePos = writer.Position;
// Note: This value can't be packed because we don't know how large it will be in advance
// we reserve space for it, then write the data, then come back and fill in the space
// to pack here, we'd have to write data to a temporary buffer and copy it in - which
// isn't worth possibly saving one byte if and only if the data is less than 63 bytes long...
// The way we do packing, any value > 63 in a ushort will use the full 2 bytes to represent.
writer.WriteValueSafe((ushort)0);
var startPos = writer.Position;
NetworkVariableFields[j].WriteField(writer);
var size = writer.Position - startPos;
writer.Seek(writePos);
writer.WriteValueSafe((ushort)size);
writer.Seek(startPos + size);
}
else
{
NetworkVariableFields[j].WriteField(writer);
}
}
else
{
// Only if EnsureNetworkVariableLengthSafety, otherwise just skip
if (networkManager.DistributedAuthorityMode || networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
{
writer.WriteValueSafe((ushort)0);
}
}
}
}
/// <summary>
/// Synchronizes by setting only the NetworkVariable field values that the client has permission to read.
/// Note: This is only invoked when first synchronizing a NetworkBehaviour (i.e. late join or spawned NetworkObject)
/// </summary>
/// <remarks>
/// When NetworkConfig.EnsureNetworkVariableLengthSafety is enabled each NetworkVariable field will be preceded
/// by the number of bytes written for that specific field.
/// </remarks>
internal void SetNetworkVariableData(FastBufferReader reader, ulong clientId)
{
var networkManager = NetworkManager;
if (networkManager.DistributedAuthorityMode)
{
reader.ReadValueSafe(out ushort variableCount);
if (variableCount != NetworkVariableFields.Count)
{
Debug.LogError("NetworkVariable count mismatch.");
return;
}
}
if (NetworkVariableFields.Count == 0)
{
return;
}
// DANGO-TODO: Made some modifications here that overlap/won't play nice with EnsureNetworkVariableLenghtSafety.
// Worth either merging or more cleanly separating these codepaths.
for (int j = 0; j < NetworkVariableFields.Count; j++)
{
var varSize = (ushort)0;
var readStartPos = 0;
if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
{
reader.ReadValueSafe(out varSize);
if (varSize == 0)
{
continue;
}
readStartPos = reader.Position;
}
else // If the client cannot read this field, then skip it
if (!NetworkVariableFields[j].CanClientRead(clientId))
{
if (networkManager.DistributedAuthorityMode)
{
reader.ReadValueSafe(out ushort size);
if (size != 0)
{
Debug.LogError("Expected zero size");
}
}
continue;
}
if (networkManager.DistributedAuthorityMode)
{
// Explicit setting of the NetworkVariableType is only needed for CMB Runtime
reader.ReadValueSafe(out NetworkVariableType _);
reader.ReadValueSafe(out ushort size);
var start_marker = reader.Position;
NetworkVariableFields[j].ReadField(reader);
if (reader.Position - start_marker != size)
{
Debug.LogError("Mismatched network variable size");
}
}
else
{
NetworkVariableFields[j].ReadField(reader);
}
if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
{
if (reader.Position > (readStartPos + varSize))
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"Var data read too far. {reader.Position - (readStartPos + varSize)} bytes.");
}
reader.Seek(readStartPos + varSize);
}
else if (reader.Position < (readStartPos + varSize))
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"Var data read too little. {(readStartPos + varSize) - reader.Position} bytes.");
}
reader.Seek(readStartPos + varSize);
}
}
}
}
/// <summary>
/// Gets the local instance of a object with a given NetworkId
/// </summary>
/// <param name="networkId"></param>
/// <returns></returns>
protected NetworkObject GetNetworkObject(ulong networkId)
{
return NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(networkId, out NetworkObject networkObject) ? networkObject : null;
}
/// <summary>
/// Override this method if your derived NetworkBehaviour requires custom synchronization data.
/// Note: Use of this method is only for the initial client synchronization of NetworkBehaviours
/// and will increase the payload size for client synchronization and dynamically spawned
/// <see cref="NetworkObject"/>s.
/// </summary>
/// <remarks>
/// When serializing (writing) this will be invoked during the client synchronization period and
/// when spawning new NetworkObjects.
/// When deserializing (reading), this will be invoked prior to the NetworkBehaviour's associated
/// NetworkObject being spawned.
/// </remarks>
/// <param name="serializer">The serializer to use to read and write the data.</param>
/// <typeparam name="T">
/// Either BufferSerializerReader or BufferSerializerWriter, depending whether the serializer
/// is in read mode or write mode.
/// </typeparam>
/// <param name="targetClientId">the relative client identifier being synchronized</param>
protected virtual void OnSynchronize<T>(ref BufferSerializer<T> serializer) where T : IReaderWriter
{
}
/// <summary>
/// The relative client identifier targeted for the serialization of this <see cref="NetworkBehaviour"/> instance.
/// </summary>
/// <remarks>
/// This value will be set prior to <see cref="OnSynchronize{T}(ref BufferSerializer{T})"/> being invoked.
/// For writing (server-side), this is useful to know which client will receive the serialized data.
/// For reading (client-side), this will be the <see cref="NetworkManager.LocalClientId"/>.
/// When synchronization of this instance is complete, this value will be reset to 0
/// </remarks>
protected ulong m_TargetIdBeingSynchronized { get; private set; }
/// <summary>
/// Internal method that determines if a NetworkBehaviour has additional synchronization data to
/// be synchronized when first instantiated prior to its associated NetworkObject being spawned.
/// </summary>
/// <remarks>
/// This includes try-catch blocks to recover from exceptions that might occur and continue to
/// synchronize any remaining NetworkBehaviours.
/// </remarks>
/// <returns>true if it wrote synchronization data and false if it did not</returns>
internal bool Synchronize<T>(ref BufferSerializer<T> serializer, ulong targetClientId = 0) where T : IReaderWriter
{
m_TargetIdBeingSynchronized = targetClientId;
if (serializer.IsWriter)
{
// Get the writer to handle seeking and determining how many bytes were written
var writer = serializer.GetFastBufferWriter();
// Save our position before we attempt to write anything so we can seek back to it (i.e. error occurs)
var positionBeforeWrite = writer.Position;
writer.WriteValueSafe(NetworkBehaviourId);
// Save our position where we will write the final size being written so we can skip over it in the
// event an exception occurs when deserializing.
var sizePosition = writer.Position;
writer.WriteValueSafe((ushort)0);
// Save our position before synchronizing to determine how much was written
var positionBeforeSynchronize = writer.Position;
var threwException = false;
try
{
OnSynchronize(ref serializer);
}
catch (Exception ex)
{
threwException = true;
if (NetworkManager.LogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"{name} threw an exception during synchronization serialization, this {nameof(NetworkBehaviour)} is being skipped and will not be synchronized!");
if (NetworkManager.LogLevel == LogLevel.Developer)
{
NetworkLog.LogError($"{ex.Message}\n {ex.StackTrace}");
}
}
}
var finalPosition = writer.Position;
// Reset before exiting
m_TargetIdBeingSynchronized = default;
// If we wrote nothing then skip writing anything for this NetworkBehaviour
if (finalPosition == positionBeforeSynchronize || threwException)
{
writer.Seek(positionBeforeWrite);
// Truncate back to the size before
writer.Truncate();
return false;
}
else
{
// Write the number of bytes serialized to handle exceptions on the deserialization side
var bytesWritten = finalPosition - positionBeforeSynchronize;
writer.Seek(sizePosition);
writer.WriteValueSafe((ushort)bytesWritten);
writer.Seek(finalPosition);
}
return true;
}
else
{
var reader = serializer.GetFastBufferReader();
// We will always read the expected byte count
reader.ReadValueSafe(out ushort expectedBytesToRead);
// Save our position before we begin synchronization deserialization
var positionBeforeSynchronize = reader.Position;
var synchronizationError = false;
try
{
// Invoke synchronization
OnSynchronize(ref serializer);
}
catch (Exception ex)
{
if (NetworkManager.LogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"{name} threw an exception during synchronization deserialization, this {nameof(NetworkBehaviour)} is being skipped and will not be synchronized!");
if (NetworkManager.LogLevel == LogLevel.Developer)
{
NetworkLog.LogError($"{ex.Message}\n {ex.StackTrace}");
}
}
synchronizationError = true;
}
var totalBytesRead = reader.Position - positionBeforeSynchronize;
if (totalBytesRead != expectedBytesToRead)
{
if (NetworkManager.LogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"{name} read {totalBytesRead} bytes but was expected to read {expectedBytesToRead} bytes during synchronization deserialization! This {nameof(NetworkBehaviour)}({GetType().Name})is being skipped and will not be synchronized!");
}
synchronizationError = true;
}
// Reset before exiting
m_TargetIdBeingSynchronized = default;
// Skip over the entry if deserialization fails
if (synchronizationError)
{
var skipToPosition = positionBeforeSynchronize + expectedBytesToRead;
reader.Seek(skipToPosition);
return false;
}
return true;
}
}
/// <summary>
/// Invoked when the <see cref="GameObject"/> the <see cref="NetworkBehaviour"/> is attached to.
/// NOTE: If you override this, you will want to always invoke this base class version of this
/// <see cref="OnDestroy"/> method!!
/// </summary>
public virtual void OnDestroy()
{
if (NetworkObject != null && NetworkObject.IsSpawned && IsSpawned)
{
// If the associated NetworkObject is still spawned then this
// NetworkBehaviour will be removed from the NetworkObject's
// ChildNetworkBehaviours list.
NetworkObject.OnNetworkBehaviourDestroyed(this);
}
// this seems odd to do here, but in fact especially in tests we can find ourselves
// here without having called InitializedVariables, which causes problems if any
// of those variables use native containers (e.g. NetworkList) as they won't be
// registered here and therefore won't be cleaned up.
//
// we should study to understand the initialization patterns
if (!m_VarInit)
{
InitializeVariables();
}
for (int i = 0; i < NetworkVariableFields.Count; i++)
{
NetworkVariableFields[i].Dispose();
}
}
}
}