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). ## [1.9.1] - 2024-04-18 ### Added - Added AnticipatedNetworkVariable<T>, which adds support for client anticipation of NetworkVariable values, allowing for more responsive gameplay (#2820) - Added AnticipatedNetworkTransform, which adds support for client anticipation of NetworkTransforms (#2820) - Added NetworkVariableBase.ExceedsDirtinessThreshold to allow network variables to throttle updates by only sending updates when the difference between the current and previous values exceeds a threshold. (This is exposed in NetworkVariable<T> with the callback NetworkVariable<T>.CheckExceedsDirtinessThreshold) (#2820) - Added NetworkVariableUpdateTraits, which add additional throttling support: MinSecondsBetweenUpdates will prevent the NetworkVariable from sending updates more often than the specified time period (even if it exceeds the dirtiness threshold), while MaxSecondsBetweenUpdates will force a dirty NetworkVariable to send an update after the specified time period even if it has not yet exceeded the dirtiness threshold. (#2820) - Added virtual method NetworkVariableBase.OnInitialize() which can be used by NetworkVariable subclasses to add initialization code (#2820) - Added virtual method NetworkVariableBase.Update(), which is called once per frame to support behaviors such as interpolation between an anticipated value and an authoritative one. (#2820) - Added NetworkTime.TickWithPartial, which represents the current tick as a double that includes the fractional/partial tick value. (#2820) - Added NetworkTickSystem.AnticipationTick, which can be helpful with implementation of client anticipation. This value represents the tick the current local client was at at the beginning of the most recent network round trip, which enables it to correlate server update ticks with the client tick that may have triggered them. (#2820) - `NetworkVariable` now includes built-in support for `NativeHashSet`, `NativeHashMap`, `List`, `HashSet`, and `Dictionary` (#2813) - `NetworkVariable` now includes delta compression for collection values (`NativeList`, `NativeArray`, `NativeHashSet`, `NativeHashMap`, `List`, `HashSet`, `Dictionary`, and `FixedString` types) to save bandwidth by only sending the values that changed. (Note: For `NativeList`, `NativeArray`, and `List`, this algorithm works differently than that used in `NetworkList`. This algorithm will use less bandwidth for "set" and "add" operations, but `NetworkList` is more bandwidth-efficient if you are performing frequent "insert" operations.) (#2813) - `UserNetworkVariableSerialization` now has optional callbacks for `WriteDelta` and `ReadDelta`. If both are provided, they will be used for all serialization operations on NetworkVariables of that type except for the first one for each client. If either is missing, the existing `Write` and `Read` will always be used. (#2813) - Network variables wrapping `INetworkSerializable` types can perform delta serialization by setting `UserNetworkVariableSerialization<T>.WriteDelta` and `UserNetworkVariableSerialization<T>.ReadDelta` for those types. The built-in `INetworkSerializable` serializer will continue to be used for all other serialization operations, but if those callbacks are set, it will call into them on all but the initial serialization to perform delta serialization. (This could be useful if you have a large struct where most values do not change regularly and you want to send only the fields that did change.) (#2813) ### Fixed - Fixed issue where NetworkTransformEditor would throw and exception if you excluded the physics package. (#2871) - Fixed issue where `NetworkTransform` could not properly synchronize its base position when using half float precision. (#2845) - 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 `NetworkObjectReference` and `NetworkBehaviourReference` to allow null references when constructing and serializing. (#2874) - Changed `NetworkAnimator` no longer requires the `Animator` component to exist on the same `GameObject`. (#2872) - 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)
364 lines
16 KiB
C#
364 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Unity.Collections;
|
|
using UnityEngine;
|
|
|
|
namespace Unity.Netcode
|
|
{
|
|
/// <summary>
|
|
/// The manager class to manage custom messages, note that this is different from the NetworkManager custom messages.
|
|
/// These are named and are much easier to use.
|
|
/// </summary>
|
|
public class CustomMessagingManager
|
|
{
|
|
private readonly NetworkManager m_NetworkManager;
|
|
|
|
internal CustomMessagingManager(NetworkManager networkManager)
|
|
{
|
|
m_NetworkManager = networkManager;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delegate used for incoming unnamed messages
|
|
/// </summary>
|
|
/// <param name="clientId">The clientId that sent the message</param>
|
|
/// <param name="reader">The stream containing the message data</param>
|
|
public delegate void UnnamedMessageDelegate(ulong clientId, FastBufferReader reader);
|
|
|
|
/// <summary>
|
|
/// Event invoked when unnamed messages arrive
|
|
/// </summary>
|
|
public event UnnamedMessageDelegate OnUnnamedMessage;
|
|
|
|
internal void InvokeUnnamedMessage(ulong clientId, FastBufferReader reader, int serializedHeaderSize)
|
|
{
|
|
if (OnUnnamedMessage != null)
|
|
{
|
|
var pos = reader.Position;
|
|
var delegates = OnUnnamedMessage.GetInvocationList();
|
|
foreach (var handler in delegates)
|
|
{
|
|
reader.Seek(pos);
|
|
((UnnamedMessageDelegate)handler).Invoke(clientId, reader);
|
|
}
|
|
}
|
|
m_NetworkManager.NetworkMetrics.TrackUnnamedMessageReceived(clientId, reader.Length + serializedHeaderSize);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends unnamed message to all clients
|
|
/// </summary>
|
|
/// <param name="messageBuffer">The message stream containing the data</param>
|
|
/// <param name="networkDelivery">The delivery type (QoS) to send data with</param>
|
|
public void SendUnnamedMessageToAll(FastBufferWriter messageBuffer, NetworkDelivery networkDelivery = NetworkDelivery.ReliableSequenced)
|
|
{
|
|
SendUnnamedMessage(m_NetworkManager.ConnectedClientsIds, messageBuffer, networkDelivery);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends unnamed message to a list of clients
|
|
/// </summary>
|
|
/// <param name="clientIds">The clients to send to, sends to everyone if null</param>
|
|
/// <param name="messageBuffer">The message stream containing the data</param>
|
|
/// <param name="networkDelivery">The delivery type (QoS) to send data with</param>
|
|
public void SendUnnamedMessage(IReadOnlyList<ulong> clientIds, FastBufferWriter messageBuffer, NetworkDelivery networkDelivery = NetworkDelivery.ReliableSequenced)
|
|
{
|
|
if (!m_NetworkManager.IsServer)
|
|
{
|
|
throw new InvalidOperationException("Can not send unnamed messages to multiple users as a client");
|
|
}
|
|
|
|
if (clientIds == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(clientIds), "You must pass in a valid clientId List");
|
|
}
|
|
|
|
if (m_NetworkManager.IsHost)
|
|
{
|
|
for (var i = 0; i < clientIds.Count; ++i)
|
|
{
|
|
if (clientIds[i] == m_NetworkManager.LocalClientId)
|
|
{
|
|
InvokeUnnamedMessage(
|
|
m_NetworkManager.LocalClientId,
|
|
new FastBufferReader(messageBuffer, Allocator.None),
|
|
0
|
|
);
|
|
}
|
|
}
|
|
}
|
|
var message = new UnnamedMessage
|
|
{
|
|
SendData = messageBuffer
|
|
};
|
|
var size = m_NetworkManager.ConnectionManager.SendMessage(ref message, networkDelivery, clientIds);
|
|
|
|
// Size is zero if we were only sending the message to ourself in which case it isn't sent.
|
|
if (size != 0)
|
|
{
|
|
m_NetworkManager.NetworkMetrics.TrackUnnamedMessageSent(clientIds, size);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a unnamed message to a specific client
|
|
/// </summary>
|
|
/// <param name="clientId">The client to send the message to</param>
|
|
/// <param name="messageBuffer">The message stream containing the data</param>
|
|
/// <param name="networkDelivery">The delivery type (QoS) to send data with</param>
|
|
public void SendUnnamedMessage(ulong clientId, FastBufferWriter messageBuffer, NetworkDelivery networkDelivery = NetworkDelivery.ReliableSequenced)
|
|
{
|
|
if (m_NetworkManager.IsHost)
|
|
{
|
|
if (clientId == m_NetworkManager.LocalClientId)
|
|
{
|
|
InvokeUnnamedMessage(
|
|
m_NetworkManager.LocalClientId,
|
|
new FastBufferReader(messageBuffer, Allocator.None),
|
|
0
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
var message = new UnnamedMessage
|
|
{
|
|
SendData = messageBuffer
|
|
};
|
|
var size = m_NetworkManager.ConnectionManager.SendMessage(ref message, networkDelivery, clientId);
|
|
// Size is zero if we were only sending the message to ourself in which case it isn't sent.
|
|
if (size != 0)
|
|
{
|
|
m_NetworkManager.NetworkMetrics.TrackUnnamedMessageSent(clientId, size);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delegate used to handle named messages
|
|
/// </summary>
|
|
public delegate void HandleNamedMessageDelegate(ulong senderClientId, FastBufferReader messagePayload);
|
|
|
|
private Dictionary<ulong, HandleNamedMessageDelegate> m_NamedMessageHandlers32 = new Dictionary<ulong, HandleNamedMessageDelegate>();
|
|
private Dictionary<ulong, HandleNamedMessageDelegate> m_NamedMessageHandlers64 = new Dictionary<ulong, HandleNamedMessageDelegate>();
|
|
|
|
private Dictionary<ulong, string> m_MessageHandlerNameLookup32 = new Dictionary<ulong, string>();
|
|
private Dictionary<ulong, string> m_MessageHandlerNameLookup64 = new Dictionary<ulong, string>();
|
|
|
|
internal void InvokeNamedMessage(ulong hash, ulong sender, FastBufferReader reader, int serializedHeaderSize)
|
|
{
|
|
var bytesCount = reader.Length + serializedHeaderSize;
|
|
|
|
if (m_NetworkManager == null)
|
|
{
|
|
// We dont know what size to use. Try every (more collision prone)
|
|
if (m_NamedMessageHandlers32.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler32))
|
|
{
|
|
// handler can remove itself, cache the name for metrics
|
|
string messageName = m_MessageHandlerNameLookup32[hash];
|
|
messageHandler32(sender, reader);
|
|
m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount);
|
|
}
|
|
|
|
if (m_NamedMessageHandlers64.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler64))
|
|
{
|
|
// handler can remove itself, cache the name for metrics
|
|
string messageName = m_MessageHandlerNameLookup64[hash];
|
|
messageHandler64(sender, reader);
|
|
m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Only check the right size.
|
|
switch (m_NetworkManager.NetworkConfig.RpcHashSize)
|
|
{
|
|
case HashSize.VarIntFourBytes:
|
|
if (m_NamedMessageHandlers32.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler32))
|
|
{
|
|
// handler can remove itself, cache the name for metrics
|
|
string messageName = m_MessageHandlerNameLookup32[hash];
|
|
messageHandler32(sender, reader);
|
|
m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount);
|
|
}
|
|
break;
|
|
case HashSize.VarIntEightBytes:
|
|
if (m_NamedMessageHandlers64.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler64))
|
|
{
|
|
// handler can remove itself, cache the name for metrics
|
|
string messageName = m_MessageHandlerNameLookup64[hash];
|
|
messageHandler64(sender, reader);
|
|
m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a named message handler delegate.
|
|
/// </summary>
|
|
/// <param name="name">Name of the message.</param>
|
|
/// <param name="callback">The callback to run when a named message is received.</param>
|
|
public void RegisterNamedMessageHandler(string name, HandleNamedMessageDelegate callback)
|
|
{
|
|
if (string.IsNullOrEmpty(name))
|
|
{
|
|
if (m_NetworkManager.LogLevel <= LogLevel.Error)
|
|
{
|
|
Debug.LogError($"[{nameof(RegisterNamedMessageHandler)}] Cannot register a named message of type null or empty!");
|
|
}
|
|
return;
|
|
}
|
|
var hash32 = XXHash.Hash32(name);
|
|
var hash64 = XXHash.Hash64(name);
|
|
|
|
m_NamedMessageHandlers32[hash32] = callback;
|
|
m_NamedMessageHandlers64[hash64] = callback;
|
|
|
|
m_MessageHandlerNameLookup32[hash32] = name;
|
|
m_MessageHandlerNameLookup64[hash64] = name;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unregisters a named message handler.
|
|
/// </summary>
|
|
/// <param name="name">The name of the message.</param>
|
|
public void UnregisterNamedMessageHandler(string name)
|
|
{
|
|
if (string.IsNullOrEmpty(name))
|
|
{
|
|
if (m_NetworkManager.LogLevel <= LogLevel.Error)
|
|
{
|
|
Debug.LogError($"[{nameof(UnregisterNamedMessageHandler)}] Cannot unregister a named message of type null or empty!");
|
|
}
|
|
return;
|
|
}
|
|
|
|
var hash32 = XXHash.Hash32(name);
|
|
var hash64 = XXHash.Hash64(name);
|
|
|
|
m_NamedMessageHandlers32.Remove(hash32);
|
|
m_NamedMessageHandlers64.Remove(hash64);
|
|
|
|
m_MessageHandlerNameLookup32.Remove(hash32);
|
|
m_MessageHandlerNameLookup64.Remove(hash64);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a named message to all clients
|
|
/// </summary>
|
|
/// <param name="messageName">The message name to send</param>
|
|
/// <param name="messageStream">The message stream containing the data</param>
|
|
/// <param name="networkDelivery">The delivery type (QoS) to send data with</param>
|
|
public void SendNamedMessageToAll(string messageName, FastBufferWriter messageStream, NetworkDelivery networkDelivery = NetworkDelivery.ReliableSequenced)
|
|
{
|
|
SendNamedMessage(messageName, m_NetworkManager.ConnectedClientsIds, messageStream, networkDelivery);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a named message
|
|
/// </summary>
|
|
/// <param name="messageName">The message name to send</param>
|
|
/// <param name="clientId">The client to send the message to</param>
|
|
/// <param name="messageStream">The message stream containing the data</param>
|
|
/// <param name="networkDelivery">The delivery type (QoS) to send data with</param>
|
|
public void SendNamedMessage(string messageName, ulong clientId, FastBufferWriter messageStream, NetworkDelivery networkDelivery = NetworkDelivery.ReliableSequenced)
|
|
{
|
|
ulong hash = 0;
|
|
switch (m_NetworkManager.NetworkConfig.RpcHashSize)
|
|
{
|
|
case HashSize.VarIntFourBytes:
|
|
hash = XXHash.Hash32(messageName);
|
|
break;
|
|
case HashSize.VarIntEightBytes:
|
|
hash = XXHash.Hash64(messageName);
|
|
break;
|
|
}
|
|
if (m_NetworkManager.IsHost)
|
|
{
|
|
if (clientId == m_NetworkManager.LocalClientId)
|
|
{
|
|
InvokeNamedMessage(
|
|
hash,
|
|
m_NetworkManager.LocalClientId,
|
|
new FastBufferReader(messageStream, Allocator.None),
|
|
0
|
|
);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
var message = new NamedMessage
|
|
{
|
|
Hash = hash,
|
|
SendData = messageStream,
|
|
};
|
|
var size = m_NetworkManager.ConnectionManager.SendMessage(ref message, networkDelivery, clientId);
|
|
|
|
// Size is zero if we were only sending the message to ourself in which case it isn't sent.
|
|
if (size != 0)
|
|
{
|
|
m_NetworkManager.NetworkMetrics.TrackNamedMessageSent(clientId, messageName, size);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends the named message
|
|
/// </summary>
|
|
/// <param name="messageName">The message name to send</param>
|
|
/// <param name="clientIds">The clients to send to</param>
|
|
/// <param name="messageStream">The message stream containing the data</param>
|
|
/// <param name="networkDelivery">The delivery type (QoS) to send data with</param>
|
|
public void SendNamedMessage(string messageName, IReadOnlyList<ulong> clientIds, FastBufferWriter messageStream, NetworkDelivery networkDelivery = NetworkDelivery.ReliableSequenced)
|
|
{
|
|
if (!m_NetworkManager.IsServer)
|
|
{
|
|
throw new InvalidOperationException("Can not send unnamed messages to multiple users as a client");
|
|
}
|
|
|
|
if (clientIds == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(clientIds), "You must pass in a valid clientId List");
|
|
}
|
|
|
|
ulong hash = 0;
|
|
switch (m_NetworkManager.NetworkConfig.RpcHashSize)
|
|
{
|
|
case HashSize.VarIntFourBytes:
|
|
hash = XXHash.Hash32(messageName);
|
|
break;
|
|
case HashSize.VarIntEightBytes:
|
|
hash = XXHash.Hash64(messageName);
|
|
break;
|
|
}
|
|
if (m_NetworkManager.IsHost)
|
|
{
|
|
for (var i = 0; i < clientIds.Count; ++i)
|
|
{
|
|
if (clientIds[i] == m_NetworkManager.LocalClientId)
|
|
{
|
|
InvokeNamedMessage(
|
|
hash,
|
|
m_NetworkManager.LocalClientId,
|
|
new FastBufferReader(messageStream, Allocator.None),
|
|
0
|
|
);
|
|
}
|
|
}
|
|
}
|
|
var message = new NamedMessage
|
|
{
|
|
Hash = hash,
|
|
SendData = messageStream
|
|
};
|
|
var size = m_NetworkManager.ConnectionManager.SendMessage(ref message, networkDelivery, clientIds);
|
|
|
|
// Size is zero if we were only sending the message to ourself in which case it isn't sent.
|
|
if (size != 0)
|
|
{
|
|
m_NetworkManager.NetworkMetrics.TrackNamedMessageSent(clientIds, messageName, size);
|
|
}
|
|
}
|
|
}
|
|
}
|