alap
This commit is contained in:
12
Assets/Mirror/Core/AssemblyInfo.cs
Normal file
12
Assets/Mirror/Core/AssemblyInfo.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Common")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests")]
|
||||
// need to use Unity.*.CodeGen assembly name to import Unity.CompilationPipeline
|
||||
// for ILPostProcessor tests.
|
||||
[assembly: InternalsVisibleTo("Unity.Mirror.Tests.CodeGen")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Generated")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Runtime")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Editor")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Runtime")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Editor")]
|
11
Assets/Mirror/Core/AssemblyInfo.cs.meta
Normal file
11
Assets/Mirror/Core/AssemblyInfo.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e28d5f410e25b42e6a76a2ffc10e4675
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
91
Assets/Mirror/Core/Attributes.cs
Normal file
91
Assets/Mirror/Core/Attributes.cs
Normal file
@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// SyncVars are used to synchronize a variable from the server to all clients automatically.
|
||||
/// <para>Value must be changed on server, not directly by clients. Hook parameter allows you to define a client-side method to be invoked when the client gets an update from the server.</para>
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class SyncVarAttribute : PropertyAttribute
|
||||
{
|
||||
public string hook;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call this from a client to run this function on the server.
|
||||
/// <para>Make sure to validate input etc. It's not possible to call this from a server.</para>
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class CommandAttribute : Attribute
|
||||
{
|
||||
public int channel = Channels.Reliable;
|
||||
public bool requiresAuthority = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The server uses a Remote Procedure Call (RPC) to run this function on clients.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class ClientRpcAttribute : Attribute
|
||||
{
|
||||
public int channel = Channels.Reliable;
|
||||
public bool includeOwner = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The server uses a Remote Procedure Call (RPC) to run this function on a specific client.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class TargetRpcAttribute : Attribute
|
||||
{
|
||||
public int channel = Channels.Reliable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prevents clients from running this method.
|
||||
/// <para>Prints a warning if a client tries to execute this method.</para>
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class ServerAttribute : Attribute {}
|
||||
|
||||
/// <summary>
|
||||
/// Prevents clients from running this method.
|
||||
/// <para>No warning is thrown.</para>
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class ServerCallbackAttribute : Attribute {}
|
||||
|
||||
/// <summary>
|
||||
/// Prevents the server from running this method.
|
||||
/// <para>Prints a warning if the server tries to execute this method.</para>
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class ClientAttribute : Attribute {}
|
||||
|
||||
/// <summary>
|
||||
/// Prevents the server from running this method.
|
||||
/// <para>No warning is printed.</para>
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class ClientCallbackAttribute : Attribute {}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a string property into a Scene property in the inspector
|
||||
/// </summary>
|
||||
public class SceneAttribute : PropertyAttribute {}
|
||||
|
||||
/// <summary>
|
||||
/// Used to show private SyncList in the inspector,
|
||||
/// <para> Use instead of SerializeField for non Serializable types </para>
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class ShowInInspectorAttribute : Attribute {}
|
||||
|
||||
/// <summary>
|
||||
/// Used to make a field readonly in the inspector
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class ReadOnlyAttribute : PropertyAttribute {}
|
||||
}
|
11
Assets/Mirror/Core/Attributes.cs.meta
Normal file
11
Assets/Mirror/Core/Attributes.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c04c722ee2ffd49c8a56ab33667b10b0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Core/Batching.meta
Normal file
8
Assets/Mirror/Core/Batching.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c38e1bebe9947f8b842a8a57aa2b71c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
150
Assets/Mirror/Core/Batching/Batcher.cs
Normal file
150
Assets/Mirror/Core/Batching/Batcher.cs
Normal file
@ -0,0 +1,150 @@
|
||||
// batching functionality encapsulated into one class.
|
||||
// -> less complexity
|
||||
// -> easy to test
|
||||
//
|
||||
// IMPORTANT: we use THRESHOLD batching, not MAXED SIZE batching.
|
||||
// see threshold comments below.
|
||||
//
|
||||
// includes timestamp for tick batching.
|
||||
// -> allows NetworkTransform etc. to use timestamp without including it in
|
||||
// every single message
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public class Batcher
|
||||
{
|
||||
// batching threshold instead of max size.
|
||||
// -> small messages are fit into threshold sized batches
|
||||
// -> messages larger than threshold are single batches
|
||||
//
|
||||
// in other words, we fit up to 'threshold' but still allow larger ones
|
||||
// for two reasons:
|
||||
// 1.) data races: skipping batching for larger messages would send a
|
||||
// large spawn message immediately, while others are batched and
|
||||
// only flushed at the end of the frame
|
||||
// 2) timestamp batching: if each batch is expected to contain a
|
||||
// timestamp, then large messages have to be a batch too. otherwise
|
||||
// they would not contain a timestamp
|
||||
readonly int threshold;
|
||||
|
||||
// TimeStamp header size. each batch has one.
|
||||
public const int TimestampSize = sizeof(double);
|
||||
|
||||
// Message header size. each message has one.
|
||||
public static int MessageHeaderSize(int messageSize) =>
|
||||
Compression.VarUIntSize((ulong)messageSize);
|
||||
|
||||
// maximum overhead for a single message.
|
||||
// useful for the outside to calculate max message sizes.
|
||||
public static int MaxMessageOverhead(int messageSize) =>
|
||||
TimestampSize + MessageHeaderSize(messageSize);
|
||||
|
||||
// full batches ready to be sent.
|
||||
// DO NOT queue NetworkMessage, it would box.
|
||||
// DO NOT queue each serialization separately.
|
||||
// it would allocate too many writers.
|
||||
// https://github.com/vis2k/Mirror/pull/3127
|
||||
// => best to build batches on the fly.
|
||||
readonly Queue<NetworkWriterPooled> batches = new Queue<NetworkWriterPooled>();
|
||||
|
||||
// current batch in progress
|
||||
NetworkWriterPooled batch;
|
||||
|
||||
public Batcher(int threshold)
|
||||
{
|
||||
this.threshold = threshold;
|
||||
}
|
||||
|
||||
// add a message for batching
|
||||
// we allow any sized messages.
|
||||
// caller needs to make sure they are within max packet size.
|
||||
public void AddMessage(ArraySegment<byte> message, double timeStamp)
|
||||
{
|
||||
// predict the needed size, which is varint(size) + content
|
||||
int headerSize = Compression.VarUIntSize((ulong)message.Count);
|
||||
int neededSize = headerSize + message.Count;
|
||||
|
||||
// when appending to a batch in progress, check final size.
|
||||
// if it expands beyond threshold, then we should finalize it first.
|
||||
// => less than or exactly threshold is fine.
|
||||
// GetBatch() will finalize it.
|
||||
// => see unit tests.
|
||||
if (batch != null &&
|
||||
batch.Position + neededSize > threshold)
|
||||
{
|
||||
batches.Enqueue(batch);
|
||||
batch = null;
|
||||
}
|
||||
|
||||
// initialize a new batch if necessary
|
||||
if (batch == null)
|
||||
{
|
||||
// borrow from pool. we return it in GetBatch.
|
||||
batch = NetworkWriterPool.Get();
|
||||
|
||||
// write timestamp first.
|
||||
// -> double precision for accuracy over long periods of time
|
||||
// -> batches are per-frame, it doesn't matter which message's
|
||||
// timestamp we use.
|
||||
batch.WriteDouble(timeStamp);
|
||||
}
|
||||
|
||||
// add serialization to current batch. even if > threshold.
|
||||
// -> we do allow > threshold sized messages as single batch
|
||||
// -> WriteBytes instead of WriteSegment because the latter
|
||||
// would add a size header. we want to write directly.
|
||||
//
|
||||
// include size prefix as varint!
|
||||
// -> fixes NetworkMessage serialization mismatch corrupting the
|
||||
// next message in a batch.
|
||||
// -> a _lot_ of time was wasted debugging corrupt batches.
|
||||
// no easy way to figure out which NetworkMessage has a mismatch.
|
||||
// -> this is worth everyone's sanity.
|
||||
// -> varint means we prefix with 1 byte most of the time.
|
||||
// -> the same issue in NetworkIdentity was why Mirror started!
|
||||
Compression.CompressVarUInt(batch, (ulong)message.Count);
|
||||
batch.WriteBytes(message.Array, message.Offset, message.Count);
|
||||
}
|
||||
|
||||
// helper function to copy a batch to writer and return it to pool
|
||||
static void CopyAndReturn(NetworkWriterPooled batch, NetworkWriter writer)
|
||||
{
|
||||
// make sure the writer is fresh to avoid uncertain situations
|
||||
if (writer.Position != 0)
|
||||
throw new ArgumentException($"GetBatch needs a fresh writer!");
|
||||
|
||||
// copy to the target writer
|
||||
ArraySegment<byte> segment = batch.ToArraySegment();
|
||||
writer.WriteBytes(segment.Array, segment.Offset, segment.Count);
|
||||
|
||||
// return batch to pool for reuse
|
||||
NetworkWriterPool.Return(batch);
|
||||
}
|
||||
|
||||
// get the next batch which is available for sending (if any).
|
||||
// TODO safely get & return a batch instead of copying to writer?
|
||||
// TODO could return pooled writer & use GetBatch in a 'using' statement!
|
||||
public bool GetBatch(NetworkWriter writer)
|
||||
{
|
||||
// get first batch from queue (if any)
|
||||
if (batches.TryDequeue(out NetworkWriterPooled first))
|
||||
{
|
||||
CopyAndReturn(first, writer);
|
||||
return true;
|
||||
}
|
||||
|
||||
// if queue was empty, we can send the batch in progress.
|
||||
if (batch != null)
|
||||
{
|
||||
CopyAndReturn(batch, writer);
|
||||
batch = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
// nothing was written
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/Batching/Batcher.cs.meta
Normal file
11
Assets/Mirror/Core/Batching/Batcher.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0afaaa611a2142d48a07bdd03b68b2b3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
129
Assets/Mirror/Core/Batching/Unbatcher.cs
Normal file
129
Assets/Mirror/Core/Batching/Unbatcher.cs
Normal file
@ -0,0 +1,129 @@
|
||||
// un-batching functionality encapsulated into one class.
|
||||
// -> less complexity
|
||||
// -> easy to test
|
||||
//
|
||||
// includes timestamp for tick batching.
|
||||
// -> allows NetworkTransform etc. to use timestamp without including it in
|
||||
// every single message
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public class Unbatcher
|
||||
{
|
||||
// supporting adding multiple batches before GetNextMessage is called.
|
||||
// just in case.
|
||||
readonly Queue<NetworkWriterPooled> batches = new Queue<NetworkWriterPooled>();
|
||||
|
||||
public int BatchesCount => batches.Count;
|
||||
|
||||
// NetworkReader is only created once,
|
||||
// then pointed to the first batch.
|
||||
readonly NetworkReader reader = new NetworkReader(new byte[0]);
|
||||
|
||||
// timestamp that was written into the batch remotely.
|
||||
// for the batch that our reader is currently pointed at.
|
||||
double readerRemoteTimeStamp;
|
||||
|
||||
// helper function to start reading a batch.
|
||||
void StartReadingBatch(NetworkWriterPooled batch)
|
||||
{
|
||||
// point reader to it
|
||||
reader.SetBuffer(batch.ToArraySegment());
|
||||
|
||||
// read remote timestamp (double)
|
||||
// -> AddBatch quarantees that we have at least 8 bytes to read
|
||||
readerRemoteTimeStamp = reader.ReadDouble();
|
||||
}
|
||||
|
||||
// add a new batch.
|
||||
// returns true if valid.
|
||||
// returns false if not, in which case the connection should be disconnected.
|
||||
public bool AddBatch(ArraySegment<byte> batch)
|
||||
{
|
||||
// IMPORTANT: ArraySegment is only valid until returning. we copy it!
|
||||
//
|
||||
// NOTE: it's not possible to create empty ArraySegments, so we
|
||||
// don't need to check against that.
|
||||
|
||||
// make sure we have at least 8 bytes to read for tick timestamp
|
||||
if (batch.Count < Batcher.TimestampSize)
|
||||
return false;
|
||||
|
||||
// put into a (pooled) writer
|
||||
// -> WriteBytes instead of WriteSegment because the latter
|
||||
// would add a size header. we want to write directly.
|
||||
// -> will be returned to pool when sending!
|
||||
NetworkWriterPooled writer = NetworkWriterPool.Get();
|
||||
writer.WriteBytes(batch.Array, batch.Offset, batch.Count);
|
||||
|
||||
// first batch? then point reader there
|
||||
if (batches.Count == 0)
|
||||
StartReadingBatch(writer);
|
||||
|
||||
// add batch
|
||||
batches.Enqueue(writer);
|
||||
//Debug.Log($"Adding Batch {BitConverter.ToString(batch.Array, batch.Offset, batch.Count)} => batches={batches.Count} reader={reader}");
|
||||
return true;
|
||||
}
|
||||
|
||||
// get next message, unpacked from batch (if any)
|
||||
// message ArraySegment is only valid until the next call.
|
||||
// timestamp is the REMOTE time when the batch was created remotely.
|
||||
public bool GetNextMessage(out ArraySegment<byte> message, out double remoteTimeStamp)
|
||||
{
|
||||
message = default;
|
||||
remoteTimeStamp = 0;
|
||||
|
||||
// do nothing if we don't have any batches.
|
||||
// otherwise the below queue.Dequeue() would throw an
|
||||
// InvalidOperationException if operating on empty queue.
|
||||
if (batches.Count == 0)
|
||||
return false;
|
||||
|
||||
// was our reader pointed to anything yet?
|
||||
if (reader.Capacity == 0)
|
||||
return false;
|
||||
|
||||
// no more data to read?
|
||||
if (reader.Remaining == 0)
|
||||
{
|
||||
// retire the batch
|
||||
NetworkWriterPooled writer = batches.Dequeue();
|
||||
NetworkWriterPool.Return(writer);
|
||||
|
||||
// do we have another batch?
|
||||
if (batches.Count > 0)
|
||||
{
|
||||
// point reader to the next batch.
|
||||
// we'll return the reader below.
|
||||
NetworkWriterPooled next = batches.Peek();
|
||||
StartReadingBatch(next);
|
||||
}
|
||||
// otherwise there's nothing more to read
|
||||
else return false;
|
||||
}
|
||||
|
||||
// use the current batch's remote timestamp
|
||||
// AFTER potentially moving to the next batch ABOVE!
|
||||
remoteTimeStamp = readerRemoteTimeStamp;
|
||||
|
||||
// enough data to read the size prefix?
|
||||
if (reader.Remaining == 0)
|
||||
return false;
|
||||
|
||||
// read the size prefix as varint
|
||||
// see Batcher.AddMessage comments for explanation.
|
||||
int size = (int)Compression.DecompressVarUInt(reader);
|
||||
|
||||
// validate size prefix, in case attackers send malicious data
|
||||
if (reader.Remaining < size)
|
||||
return false;
|
||||
|
||||
// return the message of size
|
||||
message = reader.ReadBytesSegment(size);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/Batching/Unbatcher.cs.meta
Normal file
11
Assets/Mirror/Core/Batching/Unbatcher.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 328562d71e1c45c58581b958845aa7a4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
74
Assets/Mirror/Core/ConnectionQuality.cs
Normal file
74
Assets/Mirror/Core/ConnectionQuality.cs
Normal file
@ -0,0 +1,74 @@
|
||||
// standalone, Unity-independent connection-quality algorithm & enum.
|
||||
// don't need to use this directly, it's built into Mirror's NetworkClient.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public enum ConnectionQuality : byte
|
||||
{
|
||||
ESTIMATING, // still estimating
|
||||
POOR, // unplayable
|
||||
FAIR, // very noticeable latency, not very enjoyable anymore
|
||||
GOOD, // very playable for everyone but high level competitors
|
||||
EXCELLENT // ideal experience for high level competitors
|
||||
}
|
||||
|
||||
public enum ConnectionQualityMethod : byte
|
||||
{
|
||||
Simple, // simple estimation based on rtt and jitter
|
||||
Pragmatic // based on snapshot interpolation adjustment
|
||||
}
|
||||
|
||||
// provide different heuristics for users to choose from.
|
||||
// simple heuristics to get started.
|
||||
// this will be iterated on over time based on user feedback.
|
||||
public static class ConnectionQualityHeuristics
|
||||
{
|
||||
// convenience extension to color code Connection Quality
|
||||
public static Color ColorCode(this ConnectionQuality quality)
|
||||
{
|
||||
switch (quality)
|
||||
{
|
||||
case ConnectionQuality.POOR: return Color.red;
|
||||
case ConnectionQuality.FAIR: return new Color(1.0f, 0.647f, 0.0f);
|
||||
case ConnectionQuality.GOOD: return Color.yellow;
|
||||
case ConnectionQuality.EXCELLENT: return Color.green;
|
||||
default: return Color.gray; // ESTIMATING
|
||||
}
|
||||
}
|
||||
|
||||
// straight forward estimation
|
||||
// rtt: average round trip time in seconds.
|
||||
// jitter: average latency variance.
|
||||
public static ConnectionQuality Simple(double rtt, double jitter)
|
||||
{
|
||||
if (rtt <= 0.100 && jitter <= 0.10) return ConnectionQuality.EXCELLENT;
|
||||
if (rtt <= 0.200 && jitter <= 0.20) return ConnectionQuality.GOOD;
|
||||
if (rtt <= 0.400 && jitter <= 0.50) return ConnectionQuality.FAIR;
|
||||
return ConnectionQuality.POOR;
|
||||
}
|
||||
|
||||
// snapshot interpolation based estimation.
|
||||
// snap. interp. adjusts buffer time based on connection quality.
|
||||
// based on this, we can measure how far away we are from the ideal.
|
||||
// the returned quality will always directly correlate with gameplay.
|
||||
// => requires SnapshotInterpolation dynamicAdjustment to be enabled!
|
||||
public static ConnectionQuality Pragmatic(double targetBufferTime, double currentBufferTime)
|
||||
{
|
||||
// buffer time is set by the game developer.
|
||||
// estimating in multiples is a great way to be game independent.
|
||||
// for example, a fast paced shooter and a slow paced RTS will both
|
||||
// have poor connection if the multiplier is >10.
|
||||
double multiplier = currentBufferTime / targetBufferTime;
|
||||
|
||||
// empirically measured with Tanks demo + LatencySimulation.
|
||||
// it's not obvious to estimate on paper.
|
||||
if (multiplier <= 1.15) return ConnectionQuality.EXCELLENT;
|
||||
if (multiplier <= 1.25) return ConnectionQuality.GOOD;
|
||||
if (multiplier <= 1.50) return ConnectionQuality.FAIR;
|
||||
|
||||
// anything else is poor
|
||||
return ConnectionQuality.POOR;
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/ConnectionQuality.cs.meta
Normal file
11
Assets/Mirror/Core/ConnectionQuality.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff663b880e33e4606b545c8b497041c2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
44
Assets/Mirror/Core/HostMode.cs
Normal file
44
Assets/Mirror/Core/HostMode.cs
Normal file
@ -0,0 +1,44 @@
|
||||
// host mode related helper functions.
|
||||
// usually they set up both server & client.
|
||||
// it's cleaner to keep them in one place, instead of only in server / client.
|
||||
using System;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public static class HostMode
|
||||
{
|
||||
// keep the local connections setup in one function.
|
||||
// makes host setup easier to follow.
|
||||
internal static void SetupConnections()
|
||||
{
|
||||
// create local connections pair, both are connected
|
||||
Utils.CreateLocalConnections(
|
||||
out LocalConnectionToClient connectionToClient,
|
||||
out LocalConnectionToServer connectionToServer);
|
||||
|
||||
// set client connection
|
||||
NetworkClient.connection = connectionToServer;
|
||||
|
||||
// set server connection
|
||||
NetworkServer.SetLocalConnection(connectionToClient);
|
||||
}
|
||||
|
||||
// call OnConnected on server & client.
|
||||
// public because NetworkClient.ConnectLocalServer was public before too.
|
||||
public static void InvokeOnConnected()
|
||||
{
|
||||
// call server OnConnected with server's connection to client
|
||||
NetworkServer.OnConnected(NetworkServer.localConnection);
|
||||
|
||||
// call client OnConnected with client's connection to server
|
||||
// => previously we used to send a ConnectMessage to
|
||||
// NetworkServer.localConnection. this would queue the message
|
||||
// until NetworkClient.Update processes it.
|
||||
// => invoking the client's OnConnected event directly here makes
|
||||
// tests fail. so let's do it exactly the same order as before by
|
||||
// queueing the event for next Update!
|
||||
//OnConnectedEvent?.Invoke(connection);
|
||||
((LocalConnectionToServer)NetworkClient.connection).QueueConnectedEvent();
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/HostMode.cs.meta
Normal file
11
Assets/Mirror/Core/HostMode.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d27175a08d5341fc97645b49ee533d5a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
146
Assets/Mirror/Core/InterestManagement.cs
Normal file
146
Assets/Mirror/Core/InterestManagement.cs
Normal file
@ -0,0 +1,146 @@
|
||||
// interest management component for custom solutions like
|
||||
// distance based, spatial hashing, raycast based, etc.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public abstract class InterestManagement : InterestManagementBase
|
||||
{
|
||||
// allocate newObservers helper HashSet
|
||||
readonly HashSet<NetworkConnectionToClient> newObservers =
|
||||
new HashSet<NetworkConnectionToClient>();
|
||||
|
||||
// rebuild observers for the given NetworkIdentity.
|
||||
// Server will automatically spawn/despawn added/removed ones.
|
||||
// newObservers: cached hashset to put the result into
|
||||
// initialize: true if being rebuilt for the first time
|
||||
//
|
||||
// IMPORTANT:
|
||||
// => global rebuild would be more simple, BUT
|
||||
// => local rebuild is way faster for spawn/despawn because we can
|
||||
// simply rebuild a select NetworkIdentity only
|
||||
// => having both .observers and .observing is necessary for local
|
||||
// rebuilds
|
||||
//
|
||||
// in other words, this is the perfect solution even though it's not
|
||||
// completely simple (due to .observers & .observing).
|
||||
//
|
||||
// Mirror maintains .observing automatically in the background. best of
|
||||
// both worlds without any worrying now!
|
||||
public abstract void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers);
|
||||
|
||||
// helper function to trigger a full rebuild.
|
||||
// most implementations should call this in a certain interval.
|
||||
// some might call this all the time, or only on team changes or
|
||||
// scene changes and so on.
|
||||
//
|
||||
// IMPORTANT: check if NetworkServer.active when using Update()!
|
||||
[ServerCallback]
|
||||
protected void RebuildAll()
|
||||
{
|
||||
foreach (NetworkIdentity identity in NetworkServer.spawned.Values)
|
||||
{
|
||||
NetworkServer.RebuildObservers(identity, false);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Rebuild(NetworkIdentity identity, bool initialize)
|
||||
{
|
||||
// clear newObservers hashset before using it
|
||||
newObservers.Clear();
|
||||
|
||||
// not force hidden?
|
||||
if (identity.visibility != Visibility.ForceHidden)
|
||||
{
|
||||
OnRebuildObservers(identity, newObservers);
|
||||
}
|
||||
|
||||
// IMPORTANT: AFTER rebuilding add own player connection in any case
|
||||
// to ensure player always sees himself no matter what.
|
||||
// -> OnRebuildObservers might clear observers, so we need to add
|
||||
// the player's own connection AFTER. 100% fail safe.
|
||||
// -> fixes https://github.com/vis2k/Mirror/issues/692 where a
|
||||
// player might teleport out of the ProximityChecker's cast,
|
||||
// losing the own connection as observer.
|
||||
if (identity.connectionToClient != null)
|
||||
{
|
||||
newObservers.Add(identity.connectionToClient);
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
|
||||
// add all newObservers that aren't in .observers yet
|
||||
foreach (NetworkConnectionToClient conn in newObservers)
|
||||
{
|
||||
// only add ready connections.
|
||||
// otherwise the player might not be in the world yet or anymore
|
||||
if (conn != null && conn.isReady)
|
||||
{
|
||||
if (initialize || !identity.observers.ContainsKey(conn.connectionId))
|
||||
{
|
||||
// new observer
|
||||
conn.AddToObserving(identity);
|
||||
// Debug.Log($"New Observer for {gameObject} {conn}");
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove all old .observers that aren't in newObservers anymore
|
||||
foreach (NetworkConnectionToClient conn in identity.observers.Values)
|
||||
{
|
||||
if (!newObservers.Contains(conn))
|
||||
{
|
||||
// removed observer
|
||||
conn.RemoveFromObserving(identity, false);
|
||||
// Debug.Log($"Removed Observer for {gameObject} {conn}");
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// copy new observers to observers
|
||||
if (changed)
|
||||
{
|
||||
identity.observers.Clear();
|
||||
foreach (NetworkConnectionToClient conn in newObservers)
|
||||
{
|
||||
if (conn != null && conn.isReady)
|
||||
identity.observers.Add(conn.connectionId, conn);
|
||||
}
|
||||
}
|
||||
|
||||
// special case for host mode: we use SetHostVisibility to hide
|
||||
// NetworkIdentities that aren't in observer range from host.
|
||||
// this is what games like Dota/Counter-Strike do too, where a host
|
||||
// does NOT see all players by default. they are in memory, but
|
||||
// hidden to the host player.
|
||||
//
|
||||
// this code is from UNET, it's a bit strange but it works:
|
||||
// * it hides newly connected identities in host mode
|
||||
// => that part was the intended behaviour
|
||||
// * it hides ALL NetworkIdentities in host mode when the host
|
||||
// connects but hasn't selected a character yet
|
||||
// => this only works because we have no .localConnection != null
|
||||
// check. at this stage, localConnection is null because
|
||||
// StartHost starts the server first, then calls this code,
|
||||
// then starts the client and sets .localConnection. so we can
|
||||
// NOT add a null check without breaking host visibility here.
|
||||
// * it hides ALL NetworkIdentities in server-only mode because
|
||||
// observers never contain the 'null' .localConnection
|
||||
// => that was not intended, but let's keep it as it is so we
|
||||
// don't break anything in host mode. it's way easier than
|
||||
// iterating all identities in a special function in StartHost.
|
||||
if (initialize)
|
||||
{
|
||||
if (!newObservers.Contains(NetworkServer.localConnection))
|
||||
{
|
||||
SetHostVisibility(identity, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/InterestManagement.cs.meta
Normal file
11
Assets/Mirror/Core/InterestManagement.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 41d809934003479f97e992eebb7ed6af
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
77
Assets/Mirror/Core/InterestManagementBase.cs
Normal file
77
Assets/Mirror/Core/InterestManagementBase.cs
Normal file
@ -0,0 +1,77 @@
|
||||
// interest management component for custom solutions like
|
||||
// distance based, spatial hashing, raycast based, etc.
|
||||
// low level base class allows for low level spatial hashing etc., which is 3-5x faster.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public abstract class InterestManagementBase : MonoBehaviour
|
||||
{
|
||||
// initialize NetworkServer/Client .aoi.
|
||||
// previously we did this in Awake(), but that's called for disabled
|
||||
// components too. if we do it OnEnable(), then it's not set for
|
||||
// disabled components.
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
// do not check if == null or error if already set.
|
||||
// users may enabled/disable components randomly,
|
||||
// causing this to be called multiple times.
|
||||
NetworkServer.aoi = this;
|
||||
NetworkClient.aoi = this;
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public virtual void ResetState() {}
|
||||
|
||||
// Callback used by the visibility system to determine if an observer
|
||||
// (player) can see the NetworkIdentity. If this function returns true,
|
||||
// the network connection will be added as an observer.
|
||||
// conn: Network connection of a player.
|
||||
// returns True if the player can see this object.
|
||||
public abstract bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver);
|
||||
|
||||
|
||||
// Callback used by the visibility system for objects on a host.
|
||||
// Objects on a host (with a local client) cannot be disabled or
|
||||
// destroyed when they are not visible to the local client. So this
|
||||
// function is called to allow custom code to hide these objects. A
|
||||
// typical implementation will disable renderer components on the
|
||||
// object. This is only called on local clients on a host.
|
||||
// => need the function in here and virtual so people can overwrite!
|
||||
// => not everyone wants to hide renderers!
|
||||
[ServerCallback]
|
||||
public virtual void SetHostVisibility(NetworkIdentity identity, bool visible)
|
||||
{
|
||||
foreach (Renderer rend in identity.GetComponentsInChildren<Renderer>())
|
||||
rend.enabled = visible;
|
||||
}
|
||||
|
||||
/// <summary>Called on the server when a new networked object is spawned.</summary>
|
||||
// (useful for 'only rebuild if changed' interest management algorithms)
|
||||
[ServerCallback]
|
||||
public virtual void OnSpawned(NetworkIdentity identity) {}
|
||||
|
||||
/// <summary>Called on the server when a networked object is destroyed.</summary>
|
||||
// (useful for 'only rebuild if changed' interest management algorithms)
|
||||
[ServerCallback]
|
||||
public virtual void OnDestroyed(NetworkIdentity identity) {}
|
||||
|
||||
public abstract void Rebuild(NetworkIdentity identity, bool initialize);
|
||||
|
||||
/// <summary>Adds the specified connection to the observers of identity</summary>
|
||||
protected void AddObserver(NetworkConnectionToClient connection, NetworkIdentity identity)
|
||||
{
|
||||
connection.AddToObserving(identity);
|
||||
identity.observers.Add(connection.connectionId, connection);
|
||||
}
|
||||
|
||||
/// <summary>Removes the specified connection from the observers of identity</summary>
|
||||
protected void RemoveObserver(NetworkConnectionToClient connection, NetworkIdentity identity)
|
||||
{
|
||||
connection.RemoveFromObserving(identity, false);
|
||||
identity.observers.Remove(connection.connectionId);
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/InterestManagementBase.cs.meta
Normal file
11
Assets/Mirror/Core/InterestManagementBase.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 18bd2ffe65a444f3b13d59bdac7f2228
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
3
Assets/Mirror/Core/LagCompensation.meta
Normal file
3
Assets/Mirror/Core/LagCompensation.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d2656015ded44e83a24f4c4776bafd40
|
||||
timeCreated: 1687920405
|
13
Assets/Mirror/Core/LagCompensation/Capture.cs
Normal file
13
Assets/Mirror/Core/LagCompensation/Capture.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace Mirror
|
||||
{
|
||||
public interface Capture
|
||||
{
|
||||
// server timestamp at time of capture.
|
||||
double timestamp { get; set; }
|
||||
|
||||
// optional gizmo drawing for visual debugging.
|
||||
// history is only known on the server, which usually doesn't render.
|
||||
// showing Gizmos in the Editor is enough.
|
||||
void DrawGizmo();
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/LagCompensation/Capture.cs.meta
Normal file
11
Assets/Mirror/Core/LagCompensation/Capture.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 347e831952e943a49095cadd39a5aeb2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
139
Assets/Mirror/Core/LagCompensation/HistoryBounds.cs
Normal file
139
Assets/Mirror/Core/LagCompensation/HistoryBounds.cs
Normal file
@ -0,0 +1,139 @@
|
||||
// HistoryBounds keeps a bounding box of all the object's bounds in the past N seconds.
|
||||
// useful to decide which objects to rollback, instead of rolling back all of them.
|
||||
// https://www.youtube.com/watch?v=zrIY0eIyqmI (37:00)
|
||||
// standalone C# implementation to be engine (and language) agnostic.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// FakeByte: gather bounds in smaller buckets.
|
||||
// for example, bucket(t0,t1,t2), bucket(t3,t4,t5), ...
|
||||
// instead of removing old bounds t0, t1, ...
|
||||
// we remove a whole bucket every 3 times: bucket(t0,t1,t2)
|
||||
// and when building total bounds, we encapsulate a few larger buckets
|
||||
// instead of many smaller bounds.
|
||||
//
|
||||
// => a bucket is encapsulate(bounds0, bounds1, bounds2) so we don't
|
||||
// need a custom struct, simply reuse bounds but remember that each
|
||||
// entry includes N timestamps.
|
||||
//
|
||||
// => note that simply reducing capture interval is _not_ the same.
|
||||
// we want to capture in detail in case players run in zig-zag.
|
||||
// but still grow larger buckets internally.
|
||||
public class HistoryBounds
|
||||
{
|
||||
// mischa: use MinMaxBounds to avoid Unity Bounds.Encapsulate conversions.
|
||||
readonly int boundsPerBucket;
|
||||
readonly Queue<MinMaxBounds> fullBuckets;
|
||||
|
||||
// full bucket limit. older ones will be removed.
|
||||
readonly int bucketLimit;
|
||||
|
||||
// bucket in progress, contains 0..boundsPerBucket bounds encapsulated.
|
||||
MinMaxBounds? currentBucket;
|
||||
int currentBucketSize;
|
||||
|
||||
// amount of total bounds, including bounds in full buckets + current
|
||||
public int boundsCount { get; private set; }
|
||||
|
||||
// total bounds encapsulating all of the bounds history.
|
||||
// totalMinMax is used for internal calculations.
|
||||
// public total is used for Unity representation.
|
||||
MinMaxBounds totalMinMax;
|
||||
public Bounds total
|
||||
{
|
||||
get
|
||||
{
|
||||
Bounds bounds = new Bounds();
|
||||
bounds.SetMinMax(totalMinMax.min, totalMinMax.max);
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
|
||||
public HistoryBounds(int boundsLimit, int boundsPerBucket)
|
||||
{
|
||||
// bucketLimit via '/' cuts off remainder.
|
||||
// that's what we want, since we always have a 'currentBucket'.
|
||||
this.boundsPerBucket = boundsPerBucket;
|
||||
this.bucketLimit = (boundsLimit / boundsPerBucket);
|
||||
|
||||
// initialize queue with maximum capacity to avoid runtime resizing
|
||||
// capacity +1 because it makes the code easier if we insert first, and then remove.
|
||||
fullBuckets = new Queue<MinMaxBounds>(bucketLimit + 1);
|
||||
}
|
||||
|
||||
// insert new bounds into history. calculates new total bounds.
|
||||
// Queue.Dequeue() always has the oldest bounds.
|
||||
public void Insert(Bounds bounds)
|
||||
{
|
||||
// convert to MinMax representation for faster .Encapsulate()
|
||||
MinMaxBounds minmax = new MinMaxBounds
|
||||
{
|
||||
min = bounds.min,
|
||||
max = bounds.max
|
||||
};
|
||||
|
||||
// initialize 'total' if not initialized yet.
|
||||
// we don't want to call (0,0).Encapsulate(bounds).
|
||||
if (boundsCount == 0)
|
||||
{
|
||||
totalMinMax = minmax;
|
||||
}
|
||||
|
||||
// add to current bucket:
|
||||
// either initialize new one, or encapsulate into existing one
|
||||
if (currentBucket == null)
|
||||
{
|
||||
currentBucket = minmax;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentBucket.Value.Encapsulate(minmax);
|
||||
}
|
||||
|
||||
// current bucket has one more bounds.
|
||||
// total bounds increased as well.
|
||||
currentBucketSize += 1;
|
||||
boundsCount += 1;
|
||||
|
||||
// always encapsulate into total immediately.
|
||||
// this is free.
|
||||
totalMinMax.Encapsulate(minmax);
|
||||
|
||||
// current bucket full?
|
||||
if (currentBucketSize == boundsPerBucket)
|
||||
{
|
||||
// move it to full buckets
|
||||
fullBuckets.Enqueue(currentBucket.Value);
|
||||
currentBucket = null;
|
||||
currentBucketSize = 0;
|
||||
|
||||
// full bucket capacity reached?
|
||||
if (fullBuckets.Count > bucketLimit)
|
||||
{
|
||||
// remove oldest bucket
|
||||
fullBuckets.Dequeue();
|
||||
boundsCount -= boundsPerBucket;
|
||||
|
||||
// recompute total bounds
|
||||
// instead of iterating N buckets, we iterate N / boundsPerBucket buckets.
|
||||
// TODO technically we could reuse 'currentBucket' before clearing instead of encapsulating again
|
||||
totalMinMax = minmax;
|
||||
foreach (MinMaxBounds bucket in fullBuckets)
|
||||
totalMinMax.Encapsulate(bucket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
fullBuckets.Clear();
|
||||
currentBucket = null;
|
||||
currentBucketSize = 0;
|
||||
boundsCount = 0;
|
||||
totalMinMax = new MinMaxBounds();
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/LagCompensation/HistoryBounds.cs.meta
Normal file
11
Assets/Mirror/Core/LagCompensation/HistoryBounds.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca9ea58b98a34f73801b162cd5de724e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
144
Assets/Mirror/Core/LagCompensation/LagCompensation.cs
Normal file
144
Assets/Mirror/Core/LagCompensation/LagCompensation.cs
Normal file
@ -0,0 +1,144 @@
|
||||
// standalone lag compensation algorithm
|
||||
// based on the Valve Networking Model:
|
||||
// https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public static class LagCompensation
|
||||
{
|
||||
// history is of <timestamp, capture>.
|
||||
// Queue allows for fast 'remove first' and 'append last'.
|
||||
//
|
||||
// make sure to always insert in order.
|
||||
// inserting out of order like [1,2,4,3] would cause issues.
|
||||
// can't safeguard this because Queue doesn't have .Last access.
|
||||
public static void Insert<T>(
|
||||
Queue<KeyValuePair<double, T>> history,
|
||||
int historyLimit,
|
||||
double timestamp,
|
||||
T capture)
|
||||
where T : Capture
|
||||
{
|
||||
// make space according to history limit.
|
||||
// do this before inserting, to avoid resizing past capacity.
|
||||
if (history.Count >= historyLimit)
|
||||
history.Dequeue();
|
||||
|
||||
// insert
|
||||
history.Enqueue(new KeyValuePair<double, T>(timestamp, capture));
|
||||
}
|
||||
|
||||
// get the two snapshots closest to a given timestamp.
|
||||
// those can be used to interpolate the exact snapshot at that time.
|
||||
// if timestamp is newer than the newest history entry, then we extrapolate.
|
||||
// 't' will be between 1 and 2, before is second last, after is last.
|
||||
// callers should Lerp(before, after, t=1.5) to extrapolate the hit.
|
||||
// see comments below for extrapolation.
|
||||
public static bool Sample<T>(
|
||||
Queue<KeyValuePair<double, T>> history,
|
||||
double timestamp, // current server time
|
||||
double interval, // capture interval
|
||||
out T before,
|
||||
out T after,
|
||||
out double t) // interpolation factor
|
||||
where T : Capture
|
||||
{
|
||||
before = default;
|
||||
after = default;
|
||||
t = 0;
|
||||
|
||||
// can't sample an empty history
|
||||
// interpolation needs at least one entry.
|
||||
// extrapolation needs at least two entries.
|
||||
// can't Lerp(A, A, 1.5). dist(A, A) * 1.5 is always 0.
|
||||
if(history.Count < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// older than oldest
|
||||
if (timestamp < history.Peek().Key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// iterate through the history
|
||||
// TODO faster version: guess start index by how many 'intervals' we are behind.
|
||||
// search around that area.
|
||||
// should be O(1) most of the time, unless sampling was off.
|
||||
KeyValuePair<double, T> prev = new KeyValuePair<double, T>();
|
||||
KeyValuePair<double, T> prevPrev = new KeyValuePair<double, T>();
|
||||
foreach(KeyValuePair<double, T> entry in history) {
|
||||
// exact match?
|
||||
if (timestamp == entry.Key) {
|
||||
before = entry.Value;
|
||||
after = entry.Value;
|
||||
t = Mathd.InverseLerp(before.timestamp, after.timestamp, timestamp);
|
||||
return true;
|
||||
}
|
||||
|
||||
// did we check beyond timestamp? then return the previous two.
|
||||
if (entry.Key > timestamp) {
|
||||
before = prev.Value;
|
||||
after = entry.Value;
|
||||
t = Mathd.InverseLerp(before.timestamp, after.timestamp, timestamp);
|
||||
return true;
|
||||
}
|
||||
|
||||
// remember the last two for extrapolation.
|
||||
// Queue doesn't have access to .Last.
|
||||
prevPrev = prev;
|
||||
prev = entry;
|
||||
}
|
||||
|
||||
// newer than newest: extrapolate up to one interval.
|
||||
// let's say we capture every 100 ms:
|
||||
// 100, 200, 300, 400
|
||||
// and the server is at 499
|
||||
// if a client sends CmdFire at time 480, then there's no history entry.
|
||||
// => adding the current entry every time would be too expensive.
|
||||
// worst case we would capture at 401, 402, 403, 404, ... 100 times
|
||||
// => not extrapolating isn't great. low latency clients would be
|
||||
// punished by missing their targets since no entry at 'time' was found.
|
||||
// => extrapolation is the best solution. make sure this works as
|
||||
// expected and within limits.
|
||||
if (prev.Key < timestamp && timestamp <= prev.Key + interval) {
|
||||
// return the last two valid snapshots.
|
||||
// can't just return (after, after) because we can't extrapolate
|
||||
// if their distance is 0.
|
||||
before = prevPrev.Value;
|
||||
after = prev.Value;
|
||||
|
||||
// InverseLerp will give [after, after+interval].
|
||||
// but we return [before, after, t].
|
||||
// so add +1 for the distance from before->after
|
||||
t = 1 + Mathd.InverseLerp(after.timestamp, after.timestamp + interval, timestamp);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// never trust the client.
|
||||
// we estimate when a message was sent.
|
||||
// don't trust the client to tell us the time.
|
||||
// https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
|
||||
// Command Execution Time = Current Server Time - Packet Latency - Client View Interpolation
|
||||
// => lag compensation demo estimation is off by only ~6ms
|
||||
public static double EstimateTime(double serverTime, double rtt, double bufferTime)
|
||||
{
|
||||
// packet latency is one trip from client to server, so rtt / 2
|
||||
// client view interpolation is the snapshot interpolation buffer time
|
||||
double latency = rtt / 2;
|
||||
return serverTime - latency - bufferTime;
|
||||
}
|
||||
|
||||
// convenience function to draw all history gizmos.
|
||||
// this should be called from OnDrawGizmos.
|
||||
public static void DrawGizmos<T>(Queue<KeyValuePair<double, T>> history)
|
||||
where T : Capture
|
||||
{
|
||||
foreach (KeyValuePair<double, T> entry in history)
|
||||
entry.Value.DrawGizmo();
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/LagCompensation/LagCompensation.cs.meta
Normal file
11
Assets/Mirror/Core/LagCompensation/LagCompensation.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad53cc7d12144d0ba3a8b0a4515e5d17
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,19 @@
|
||||
// snapshot interpolation settings struct.
|
||||
// can easily be exposed in Unity inspectors.
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// class so we can define defaults easily
|
||||
[Serializable]
|
||||
public class LagCompensationSettings
|
||||
{
|
||||
[Header("Buffering")]
|
||||
[Tooltip("Keep this many past snapshots in the buffer. The larger this is, the further we can rewind into the past.\nMaximum rewind time := historyAmount * captureInterval")]
|
||||
public int historyLimit = 6;
|
||||
|
||||
[Tooltip("Capture state every 'captureInterval' seconds. Larger values will space out the captures more, which gives a longer history but with possible gaps inbetween.\nSmaller values will have fewer gaps, with shorter history.")]
|
||||
public float captureInterval = 0.100f; // 100 ms
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa80bec245f94bf8a28ec78777992a1c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
73
Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs
Normal file
73
Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs
Normal file
@ -0,0 +1,73 @@
|
||||
// Unity's Bounds struct is represented as (center, extents).
|
||||
// HistoryBounds make heavy use of .Encapsulate(), which has to convert
|
||||
// Unity's (center, extents) to (min, max) every time, and then convert back.
|
||||
//
|
||||
// It's faster to use a (min, max) representation directly instead.
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public struct MinMaxBounds: IEquatable<Bounds>
|
||||
{
|
||||
public Vector3 min;
|
||||
public Vector3 max;
|
||||
|
||||
// encapsulate a single point
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Encapsulate(Vector3 point)
|
||||
{
|
||||
min = Vector3.Min(this.min, point);
|
||||
max = Vector3.Max(this.max, point);
|
||||
}
|
||||
|
||||
// encapsulate another bounds
|
||||
public void Encapsulate(MinMaxBounds bounds)
|
||||
{
|
||||
Encapsulate(bounds.min);
|
||||
Encapsulate(bounds.max);
|
||||
}
|
||||
|
||||
// convenience comparison with Unity's bounds, for unit tests etc.
|
||||
public static bool operator ==(MinMaxBounds lhs, Bounds rhs) =>
|
||||
lhs.min == rhs.min &&
|
||||
lhs.max == rhs.max;
|
||||
|
||||
public static bool operator !=(MinMaxBounds lhs, Bounds rhs) =>
|
||||
!(lhs == rhs);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is MinMaxBounds other &&
|
||||
min == other.min &&
|
||||
max == other.max;
|
||||
|
||||
public bool Equals(MinMaxBounds other) =>
|
||||
min.Equals(other.min) && max.Equals(other.max);
|
||||
|
||||
public bool Equals(Bounds other) =>
|
||||
min.Equals(other.min) && max.Equals(other.max);
|
||||
|
||||
#if UNITY_2021_3_OR_NEWER
|
||||
// Unity 2019/2020 don't have HashCode.Combine yet.
|
||||
// this is only to avoid reflection. without defining, it works too.
|
||||
// default generated by rider
|
||||
public override int GetHashCode() => HashCode.Combine(min, max);
|
||||
#else
|
||||
public override int GetHashCode()
|
||||
{
|
||||
// return HashCode.Combine(min, max); without using .Combine for older Unity versions
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
hash = hash * 23 + min.GetHashCode();
|
||||
hash = hash * 23 + max.GetHashCode();
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// tostring
|
||||
public override string ToString() => $"({min}, {max})";
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs.meta
Normal file
11
Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4372b1e1a1cc4c669cc7bf0925f59d29
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
82
Assets/Mirror/Core/LocalConnectionToClient.cs
Normal file
82
Assets/Mirror/Core/LocalConnectionToClient.cs
Normal file
@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// a server's connection TO a LocalClient.
|
||||
// sending messages on this connection causes the client's handler function to be invoked directly
|
||||
public class LocalConnectionToClient : NetworkConnectionToClient
|
||||
{
|
||||
internal LocalConnectionToServer connectionToServer;
|
||||
|
||||
// packet queue
|
||||
internal readonly Queue<NetworkWriterPooled> queue = new Queue<NetworkWriterPooled>();
|
||||
|
||||
public LocalConnectionToClient() : base(LocalConnectionId) {}
|
||||
|
||||
public override string address => "localhost";
|
||||
|
||||
internal override void Send(ArraySegment<byte> segment, int channelId = Channels.Reliable)
|
||||
{
|
||||
// instead of invoking it directly, we enqueue and process next update.
|
||||
// this way we can simulate a similar call flow as with remote clients.
|
||||
// the closer we get to simulating host as remote, the better!
|
||||
// both directions do this, so [Command] and [Rpc] behave the same way.
|
||||
|
||||
//Debug.Log($"Enqueue {BitConverter.ToString(segment.Array, segment.Offset, segment.Count)}");
|
||||
NetworkWriterPooled writer = NetworkWriterPool.Get();
|
||||
writer.WriteBytes(segment.Array, segment.Offset, segment.Count);
|
||||
connectionToServer.queue.Enqueue(writer);
|
||||
}
|
||||
|
||||
// true because local connections never timeout
|
||||
internal override bool IsAlive(float timeout) => true;
|
||||
|
||||
// don't ping host client in host mode
|
||||
protected override void UpdatePing() {}
|
||||
|
||||
internal override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// process internal messages so they are applied at the correct time
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
// call receive on queued writer's content, return to pool
|
||||
NetworkWriterPooled writer = queue.Dequeue();
|
||||
ArraySegment<byte> message = writer.ToArraySegment();
|
||||
|
||||
// OnTransportData assumes a proper batch with timestamp etc.
|
||||
// let's make a proper batch and pass it to OnTransportData.
|
||||
Batcher batcher = GetBatchForChannelId(Channels.Reliable);
|
||||
batcher.AddMessage(message, NetworkTime.localTime);
|
||||
|
||||
using (NetworkWriterPooled batchWriter = NetworkWriterPool.Get())
|
||||
{
|
||||
// make a batch with our local time (double precision)
|
||||
if (batcher.GetBatch(batchWriter))
|
||||
{
|
||||
NetworkServer.OnTransportData(connectionId, batchWriter.ToArraySegment(), Channels.Reliable);
|
||||
}
|
||||
}
|
||||
|
||||
NetworkWriterPool.Return(writer);
|
||||
}
|
||||
}
|
||||
|
||||
internal void DisconnectInternal()
|
||||
{
|
||||
// set not ready and handle clientscene disconnect in any case
|
||||
// (might be client or host mode here)
|
||||
isReady = false;
|
||||
RemoveFromObservingsObservers();
|
||||
}
|
||||
|
||||
/// <summary>Disconnects this connection.</summary>
|
||||
public override void Disconnect()
|
||||
{
|
||||
DisconnectInternal();
|
||||
connectionToServer.DisconnectInternal();
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/LocalConnectionToClient.cs.meta
Normal file
11
Assets/Mirror/Core/LocalConnectionToClient.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a88758df7db2043d6a9d926e0b6d4191
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
117
Assets/Mirror/Core/LocalConnectionToServer.cs
Normal file
117
Assets/Mirror/Core/LocalConnectionToServer.cs
Normal file
@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// a localClient's connection TO a server.
|
||||
// send messages on this connection causes the server's handler function to be invoked directly.
|
||||
public class LocalConnectionToServer : NetworkConnectionToServer
|
||||
{
|
||||
internal LocalConnectionToClient connectionToClient;
|
||||
|
||||
// packet queue
|
||||
internal readonly Queue<NetworkWriterPooled> queue = new Queue<NetworkWriterPooled>();
|
||||
|
||||
// see caller for comments on why we need this
|
||||
bool connectedEventPending;
|
||||
bool disconnectedEventPending;
|
||||
internal void QueueConnectedEvent() => connectedEventPending = true;
|
||||
internal void QueueDisconnectedEvent() => disconnectedEventPending = true;
|
||||
|
||||
// Send stage two: serialized NetworkMessage as ArraySegment<byte>
|
||||
internal override void Send(ArraySegment<byte> segment, int channelId = Channels.Reliable)
|
||||
{
|
||||
if (segment.Count == 0)
|
||||
{
|
||||
Debug.LogError("LocalConnection.SendBytes cannot send zero bytes");
|
||||
return;
|
||||
}
|
||||
|
||||
// instead of invoking it directly, we enqueue and process next update.
|
||||
// this way we can simulate a similar call flow as with remote clients.
|
||||
// the closer we get to simulating host as remote, the better!
|
||||
// both directions do this, so [Command] and [Rpc] behave the same way.
|
||||
|
||||
//Debug.Log($"Enqueue {BitConverter.ToString(segment.Array, segment.Offset, segment.Count)}");
|
||||
NetworkWriterPooled writer = NetworkWriterPool.Get();
|
||||
writer.WriteBytes(segment.Array, segment.Offset, segment.Count);
|
||||
connectionToClient.queue.Enqueue(writer);
|
||||
}
|
||||
|
||||
internal override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// should we still process a connected event?
|
||||
if (connectedEventPending)
|
||||
{
|
||||
connectedEventPending = false;
|
||||
NetworkClient.OnConnectedEvent?.Invoke();
|
||||
}
|
||||
|
||||
// process internal messages so they are applied at the correct time
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
// call receive on queued writer's content, return to pool
|
||||
NetworkWriterPooled writer = queue.Dequeue();
|
||||
ArraySegment<byte> message = writer.ToArraySegment();
|
||||
|
||||
// OnTransportData assumes a proper batch with timestamp etc.
|
||||
// let's make a proper batch and pass it to OnTransportData.
|
||||
Batcher batcher = GetBatchForChannelId(Channels.Reliable);
|
||||
batcher.AddMessage(message, NetworkTime.localTime);
|
||||
|
||||
using (NetworkWriterPooled batchWriter = NetworkWriterPool.Get())
|
||||
{
|
||||
// make a batch with our local time (double precision)
|
||||
if (batcher.GetBatch(batchWriter))
|
||||
{
|
||||
NetworkClient.OnTransportData(batchWriter.ToArraySegment(), Channels.Reliable);
|
||||
}
|
||||
}
|
||||
|
||||
NetworkWriterPool.Return(writer);
|
||||
}
|
||||
|
||||
// should we still process a disconnected event?
|
||||
if (disconnectedEventPending)
|
||||
{
|
||||
disconnectedEventPending = false;
|
||||
NetworkClient.OnDisconnectedEvent?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Disconnects this connection.</summary>
|
||||
internal void DisconnectInternal()
|
||||
{
|
||||
// set not ready and handle clientscene disconnect in any case
|
||||
// (might be client or host mode here)
|
||||
// TODO remove redundant state. have one source of truth for .ready!
|
||||
isReady = false;
|
||||
NetworkClient.ready = false;
|
||||
}
|
||||
|
||||
/// <summary>Disconnects this connection.</summary>
|
||||
public override void Disconnect()
|
||||
{
|
||||
connectionToClient.DisconnectInternal();
|
||||
DisconnectInternal();
|
||||
|
||||
// simulate what a true remote connection would do:
|
||||
// first, the server should remove it:
|
||||
// TODO should probably be in connectionToClient.DisconnectInternal
|
||||
// because that's the NetworkServer's connection!
|
||||
NetworkServer.RemoveLocalConnection();
|
||||
|
||||
// then call OnTransportDisconnected for proper disconnect handling,
|
||||
// callbacks & cleanups.
|
||||
// => otherwise OnClientDisconnected() is never called!
|
||||
// => see NetworkClientTests.DisconnectCallsOnClientDisconnect_HostMode()
|
||||
NetworkClient.OnTransportDisconnected();
|
||||
}
|
||||
|
||||
// true because local connections never timeout
|
||||
internal override bool IsAlive(float timeout) => true;
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/LocalConnectionToServer.cs.meta
Normal file
11
Assets/Mirror/Core/LocalConnectionToServer.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cdfff390c3504158a269e8b8662e2a40
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
140
Assets/Mirror/Core/Messages.cs
Normal file
140
Assets/Mirror/Core/Messages.cs
Normal file
@ -0,0 +1,140 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// need to send time every sendInterval.
|
||||
// batching automatically includes remoteTimestamp.
|
||||
// all we need to do is ensure that an empty message is sent.
|
||||
// and react to it.
|
||||
// => we don't want to insert a snapshot on every batch.
|
||||
// => do it exactly every sendInterval on every TimeSnapshotMessage.
|
||||
public struct TimeSnapshotMessage : NetworkMessage {}
|
||||
|
||||
public struct ReadyMessage : NetworkMessage {}
|
||||
|
||||
public struct NotReadyMessage : NetworkMessage {}
|
||||
|
||||
public struct AddPlayerMessage : NetworkMessage {}
|
||||
|
||||
public struct SceneMessage : NetworkMessage
|
||||
{
|
||||
public string sceneName;
|
||||
// Normal = 0, LoadAdditive = 1, UnloadAdditive = 2
|
||||
public SceneOperation sceneOperation;
|
||||
public bool customHandling;
|
||||
}
|
||||
|
||||
public enum SceneOperation : byte
|
||||
{
|
||||
Normal,
|
||||
LoadAdditive,
|
||||
UnloadAdditive
|
||||
}
|
||||
|
||||
public struct CommandMessage : NetworkMessage
|
||||
{
|
||||
public uint netId;
|
||||
public byte componentIndex;
|
||||
public ushort functionHash;
|
||||
// the parameters for the Cmd function
|
||||
// -> ArraySegment to avoid unnecessary allocations
|
||||
public ArraySegment<byte> payload;
|
||||
}
|
||||
|
||||
public struct RpcMessage : NetworkMessage
|
||||
{
|
||||
public uint netId;
|
||||
public byte componentIndex;
|
||||
public ushort functionHash;
|
||||
// the parameters for the Cmd function
|
||||
// -> ArraySegment to avoid unnecessary allocations
|
||||
public ArraySegment<byte> payload;
|
||||
}
|
||||
|
||||
public struct SpawnMessage : NetworkMessage
|
||||
{
|
||||
// netId of new or existing object
|
||||
public uint netId;
|
||||
public bool isLocalPlayer;
|
||||
// Sets hasAuthority on the spawned object
|
||||
public bool isOwner;
|
||||
public ulong sceneId;
|
||||
// If sceneId != 0 then it is used instead of assetId
|
||||
public uint assetId;
|
||||
// Local position
|
||||
public Vector3 position;
|
||||
// Local rotation
|
||||
public Quaternion rotation;
|
||||
// Local scale
|
||||
public Vector3 scale;
|
||||
// serialized component data
|
||||
// ArraySegment to avoid unnecessary allocations
|
||||
public ArraySegment<byte> payload;
|
||||
}
|
||||
|
||||
public struct ChangeOwnerMessage : NetworkMessage
|
||||
{
|
||||
public uint netId;
|
||||
public bool isOwner;
|
||||
public bool isLocalPlayer;
|
||||
}
|
||||
|
||||
public struct ObjectSpawnStartedMessage : NetworkMessage {}
|
||||
|
||||
public struct ObjectSpawnFinishedMessage : NetworkMessage {}
|
||||
|
||||
public struct ObjectDestroyMessage : NetworkMessage
|
||||
{
|
||||
public uint netId;
|
||||
}
|
||||
|
||||
public struct ObjectHideMessage : NetworkMessage
|
||||
{
|
||||
public uint netId;
|
||||
}
|
||||
|
||||
public struct EntityStateMessage : NetworkMessage
|
||||
{
|
||||
public uint netId;
|
||||
// the serialized component data
|
||||
// -> ArraySegment to avoid unnecessary allocations
|
||||
public ArraySegment<byte> payload;
|
||||
}
|
||||
|
||||
// whoever wants to measure rtt, sends this to the other end.
|
||||
public struct NetworkPingMessage : NetworkMessage
|
||||
{
|
||||
// local time is used to calculate round trip time,
|
||||
// and to calculate the predicted time offset.
|
||||
public double localTime;
|
||||
|
||||
// predicted time is sent to compare the final error, for debugging only
|
||||
public double predictedTimeAdjusted;
|
||||
|
||||
public NetworkPingMessage(double localTime, double predictedTimeAdjusted)
|
||||
{
|
||||
this.localTime = localTime;
|
||||
this.predictedTimeAdjusted = predictedTimeAdjusted;
|
||||
}
|
||||
}
|
||||
|
||||
// the other end responds with this message.
|
||||
// we can use this to calculate rtt.
|
||||
public struct NetworkPongMessage : NetworkMessage
|
||||
{
|
||||
// local time is used to calculate round trip time.
|
||||
public double localTime;
|
||||
|
||||
// predicted error is used to adjust the predicted timeline.
|
||||
public double predictionErrorUnadjusted;
|
||||
public double predictionErrorAdjusted; // for debug purposes
|
||||
|
||||
public NetworkPongMessage(double localTime, double predictionErrorUnadjusted, double predictionErrorAdjusted)
|
||||
{
|
||||
this.localTime = localTime;
|
||||
this.predictionErrorUnadjusted = predictionErrorUnadjusted;
|
||||
this.predictionErrorAdjusted = predictionErrorAdjusted;
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/Messages.cs.meta
Normal file
11
Assets/Mirror/Core/Messages.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 938f6f28a6c5b48a0bbd7782342d763b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
16
Assets/Mirror/Core/Mirror.asmdef
Normal file
16
Assets/Mirror/Core/Mirror.asmdef
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Mirror",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:325984b52e4128546bc7558552f8b1d2"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": true,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
7
Assets/Mirror/Core/Mirror.asmdef.meta
Normal file
7
Assets/Mirror/Core/Mirror.asmdef.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 30817c1a0e6d646d99c048fc403f5979
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
84
Assets/Mirror/Core/NetworkAuthenticator.cs
Normal file
84
Assets/Mirror/Core/NetworkAuthenticator.cs
Normal file
@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[Serializable] public class UnityEventNetworkConnection : UnityEvent<NetworkConnectionToClient> {}
|
||||
|
||||
/// <summary>Base class for implementing component-based authentication during the Connect phase</summary>
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-authenticators")]
|
||||
public abstract class NetworkAuthenticator : MonoBehaviour
|
||||
{
|
||||
/// <summary>Notify subscribers on the server when a client is authenticated</summary>
|
||||
[Header("Event Listeners (optional)")]
|
||||
[Tooltip("Mirror has an internal subscriber to this event. You can add your own here.")]
|
||||
public UnityEventNetworkConnection OnServerAuthenticated = new UnityEventNetworkConnection();
|
||||
|
||||
/// <summary>Notify subscribers on the client when the client is authenticated</summary>
|
||||
[Tooltip("Mirror has an internal subscriber to this event. You can add your own here.")]
|
||||
public UnityEvent OnClientAuthenticated = new UnityEvent();
|
||||
|
||||
/// <summary>Called when server starts, used to register message handlers if needed.</summary>
|
||||
public virtual void OnStartServer() {}
|
||||
|
||||
/// <summary>Called when server stops, used to unregister message handlers if needed.</summary>
|
||||
public virtual void OnStopServer() {}
|
||||
|
||||
/// <summary>Called on server from OnServerConnectInternal when a client needs to authenticate</summary>
|
||||
public virtual void OnServerAuthenticate(NetworkConnectionToClient conn) {}
|
||||
|
||||
protected void ServerAccept(NetworkConnectionToClient conn)
|
||||
{
|
||||
OnServerAuthenticated.Invoke(conn);
|
||||
}
|
||||
|
||||
protected void ServerReject(NetworkConnectionToClient conn)
|
||||
{
|
||||
conn.Disconnect();
|
||||
}
|
||||
|
||||
/// <summary>Called when client starts, used to register message handlers if needed.</summary>
|
||||
public virtual void OnStartClient() {}
|
||||
|
||||
/// <summary>Called when client stops, used to unregister message handlers if needed.</summary>
|
||||
public virtual void OnStopClient() {}
|
||||
|
||||
/// <summary>Called on client from OnClientConnectInternal when a client needs to authenticate</summary>
|
||||
public virtual void OnClientAuthenticate() {}
|
||||
|
||||
protected void ClientAccept()
|
||||
{
|
||||
OnClientAuthenticated.Invoke();
|
||||
}
|
||||
|
||||
protected void ClientReject()
|
||||
{
|
||||
// Set this on the client for local reference
|
||||
NetworkClient.connection.isAuthenticated = false;
|
||||
|
||||
// disconnect the client
|
||||
NetworkClient.connection.Disconnect();
|
||||
}
|
||||
|
||||
// Reset() instead of OnValidate():
|
||||
// Any NetworkAuthenticator assigns itself to the NetworkManager, this is fine on first adding it,
|
||||
// but if someone intentionally sets Authenticator to null on the NetworkManager again then the
|
||||
// Authenticator will reassign itself if a value in the inspector is changed.
|
||||
// My change switches OnValidate to Reset since Reset is only called when the component is first
|
||||
// added (or reset is pressed).
|
||||
void Reset()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
// automatically assign authenticator field if we add this to NetworkManager
|
||||
NetworkManager manager = GetComponent<NetworkManager>();
|
||||
if (manager != null && manager.authenticator == null)
|
||||
{
|
||||
// undo has to be called before the change happens
|
||||
UnityEditor.Undo.RecordObject(manager, "Assigned NetworkManager authenticator");
|
||||
manager.authenticator = this;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkAuthenticator.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkAuthenticator.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 407fc95d4a8257f448799f26cdde0c2a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
1370
Assets/Mirror/Core/NetworkBehaviour.cs
Normal file
1370
Assets/Mirror/Core/NetworkBehaviour.cs
Normal file
File diff suppressed because it is too large
Load Diff
11
Assets/Mirror/Core/NetworkBehaviour.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkBehaviour.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 655ee8cba98594f70880da5cc4dc442d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
33
Assets/Mirror/Core/NetworkBehaviourSyncVar.cs
Normal file
33
Assets/Mirror/Core/NetworkBehaviourSyncVar.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// backing field for sync NetworkBehaviour
|
||||
public struct NetworkBehaviourSyncVar : IEquatable<NetworkBehaviourSyncVar>
|
||||
{
|
||||
public uint netId;
|
||||
// limited to 255 behaviours per identity
|
||||
public byte componentIndex;
|
||||
|
||||
public NetworkBehaviourSyncVar(uint netId, int componentIndex) : this()
|
||||
{
|
||||
this.netId = netId;
|
||||
this.componentIndex = (byte)componentIndex;
|
||||
}
|
||||
|
||||
public bool Equals(NetworkBehaviourSyncVar other)
|
||||
{
|
||||
return other.netId == netId && other.componentIndex == componentIndex;
|
||||
}
|
||||
|
||||
public bool Equals(uint netId, int componentIndex)
|
||||
{
|
||||
return this.netId == netId && this.componentIndex == componentIndex;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[netId:{netId} compIndex:{componentIndex}]";
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkBehaviourSyncVar.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkBehaviourSyncVar.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b04fe7518657486089dfaf811db0b3ea
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
1842
Assets/Mirror/Core/NetworkClient.cs
Normal file
1842
Assets/Mirror/Core/NetworkClient.cs
Normal file
File diff suppressed because it is too large
Load Diff
11
Assets/Mirror/Core/NetworkClient.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkClient.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abe6be14204d94224a3e7cd99dd2ea73
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
168
Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs
Normal file
168
Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs
Normal file
@ -0,0 +1,168 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public static partial class NetworkClient
|
||||
{
|
||||
// snapshot interpolation settings /////////////////////////////////////
|
||||
// TODO expose the settings to the user later.
|
||||
// via NetMan or NetworkClientConfig or NetworkClient as component etc.
|
||||
public static SnapshotInterpolationSettings snapshotSettings = new SnapshotInterpolationSettings();
|
||||
|
||||
// snapshot interpolation runtime data /////////////////////////////////
|
||||
// buffer time is dynamically adjusted.
|
||||
// store the current multiplier here, without touching the original in settings.
|
||||
// this way we can easily reset to or compare with original where needed.
|
||||
public static double bufferTimeMultiplier;
|
||||
|
||||
// original buffer time based on the settings
|
||||
// dynamically adjusted buffer time based on dynamically adjusted multiplier
|
||||
public static double initialBufferTime => NetworkServer.sendInterval * snapshotSettings.bufferTimeMultiplier;
|
||||
public static double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier;
|
||||
|
||||
// <servertime, snaps>
|
||||
public static SortedList<double, TimeSnapshot> snapshots = new SortedList<double, TimeSnapshot>();
|
||||
|
||||
// for smooth interpolation, we need to interpolate along server time.
|
||||
// any other time (arrival on client, client local time, etc.) is not
|
||||
// going to give smooth results.
|
||||
// in other words, this is the remote server's time, but adjusted.
|
||||
//
|
||||
// internal for use from NetworkTime.
|
||||
// double for long running servers, see NetworkTime comments.
|
||||
internal static double localTimeline;
|
||||
|
||||
// catchup / slowdown adjustments are applied to timescale,
|
||||
// to be adjusted in every update instead of when receiving messages.
|
||||
internal static double localTimescale = 1;
|
||||
|
||||
// catchup /////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
// we use EMA to average the last second worth of snapshot time diffs.
|
||||
// manually averaging the last second worth of values with a for loop
|
||||
// would be the same, but a moving average is faster because we only
|
||||
// ever add one value.
|
||||
static ExponentialMovingAverage driftEma;
|
||||
|
||||
// dynamic buffer time adjustment //////////////////////////////////////
|
||||
// dynamically adjusts bufferTimeMultiplier for smooth results.
|
||||
// to understand how this works, try this manually:
|
||||
//
|
||||
// - disable dynamic adjustment
|
||||
// - set jitter = 0.2 (20% is a lot!)
|
||||
// - notice some stuttering
|
||||
// - disable interpolation to see just how much jitter this really is(!)
|
||||
// - enable interpolation again
|
||||
// - manually increase bufferTimeMultiplier to 3-4
|
||||
// ... the cube slows down (blue) until it's smooth
|
||||
// - with dynamic adjustment enabled, it will set 4 automatically
|
||||
// ... the cube slows down (blue) until it's smooth as well
|
||||
//
|
||||
// note that 20% jitter is extreme.
|
||||
// for this to be perfectly smooth, set the safety tolerance to '2'.
|
||||
// but realistically this is not necessary, and '1' is enough.
|
||||
[Header("Snapshot Interpolation: Dynamic Adjustment")]
|
||||
[Tooltip("Automatically adjust bufferTimeMultiplier for smooth results.\nSets a low multiplier on stable connections, and a high multiplier on jittery connections.")]
|
||||
public static bool dynamicAdjustment = true;
|
||||
|
||||
[Tooltip("Safety buffer that is always added to the dynamic bufferTimeMultiplier adjustment.")]
|
||||
public static float dynamicAdjustmentTolerance = 1; // 1 is realistically just fine, 2 is very very safe even for 20% jitter. can be half a frame too. (see above comments)
|
||||
|
||||
[Tooltip("Dynamic adjustment is computed over n-second exponential moving average standard deviation.")]
|
||||
public static int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time
|
||||
static ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter)
|
||||
|
||||
// OnValidate: see NetworkClient.cs
|
||||
// add snapshot & initialize client interpolation time if needed
|
||||
|
||||
// initialization called from Awake
|
||||
static void InitTimeInterpolation()
|
||||
{
|
||||
// reset timeline, localTimescale & snapshots from last session (if any)
|
||||
bufferTimeMultiplier = snapshotSettings.bufferTimeMultiplier;
|
||||
localTimeline = 0;
|
||||
localTimescale = 1;
|
||||
snapshots.Clear();
|
||||
|
||||
// initialize EMA with 'emaDuration' seconds worth of history.
|
||||
// 1 second holds 'sendRate' worth of values.
|
||||
// multiplied by emaDuration gives n-seconds.
|
||||
driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * snapshotSettings.driftEmaDuration);
|
||||
deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * snapshotSettings.deliveryTimeEmaDuration);
|
||||
}
|
||||
|
||||
// server sends TimeSnapshotMessage every sendInterval.
|
||||
// batching already includes the remoteTimestamp.
|
||||
// we simply insert it on-message here.
|
||||
// => only for reliable channel. unreliable would always arrive earlier.
|
||||
static void OnTimeSnapshotMessage(TimeSnapshotMessage _)
|
||||
{
|
||||
// insert another snapshot for snapshot interpolation.
|
||||
// before calling OnDeserialize so components can use
|
||||
// NetworkTime.time and NetworkTime.timeStamp.
|
||||
|
||||
// Unity 2019 doesn't have Time.timeAsDouble yet
|
||||
OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime));
|
||||
}
|
||||
|
||||
// see comments at the top of this file
|
||||
public static void OnTimeSnapshot(TimeSnapshot snap)
|
||||
{
|
||||
// Debug.Log($"NetworkClient: OnTimeSnapshot @ {snap.remoteTime:F3}");
|
||||
|
||||
// (optional) dynamic adjustment
|
||||
if (snapshotSettings.dynamicAdjustment)
|
||||
{
|
||||
// set bufferTime on the fly.
|
||||
// shows in inspector for easier debugging :)
|
||||
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
|
||||
NetworkServer.sendInterval,
|
||||
deliveryTimeEma.StandardDeviation,
|
||||
snapshotSettings.dynamicAdjustmentTolerance
|
||||
);
|
||||
}
|
||||
|
||||
// insert into the buffer & initialize / adjust / catchup
|
||||
SnapshotInterpolation.InsertAndAdjust(
|
||||
snapshots,
|
||||
snapshotSettings.bufferLimit,
|
||||
snap,
|
||||
ref localTimeline,
|
||||
ref localTimescale,
|
||||
NetworkServer.sendInterval,
|
||||
bufferTime,
|
||||
snapshotSettings.catchupSpeed,
|
||||
snapshotSettings.slowdownSpeed,
|
||||
ref driftEma,
|
||||
snapshotSettings.catchupNegativeThreshold,
|
||||
snapshotSettings.catchupPositiveThreshold,
|
||||
ref deliveryTimeEma);
|
||||
|
||||
// Debug.Log($"inserted TimeSnapshot remote={snap.remoteTime:F2} local={snap.localTime:F2} total={snapshots.Count}");
|
||||
}
|
||||
|
||||
// call this from early update, so the timeline is safe to use in update
|
||||
static void UpdateTimeInterpolation()
|
||||
{
|
||||
// only while we have snapshots.
|
||||
// timeline starts when the first snapshot arrives.
|
||||
if (snapshots.Count > 0)
|
||||
{
|
||||
// progress local timeline.
|
||||
// NetworkTime uses unscaled time and ignores Time.timeScale.
|
||||
// fixes Time.timeScale getting server & client time out of sync:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3409
|
||||
SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref localTimeline, localTimescale);
|
||||
|
||||
// progress local interpolation.
|
||||
// TimeSnapshot doesn't interpolate anything.
|
||||
// this is merely to keep removing older snapshots.
|
||||
SnapshotInterpolation.StepInterpolation(snapshots, localTimeline, out _, out _, out double t);
|
||||
// Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad039071a9cc487b9f7831d28bbe8e83
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
207
Assets/Mirror/Core/NetworkConnection.cs
Normal file
207
Assets/Mirror/Core/NetworkConnection.cs
Normal file
@ -0,0 +1,207 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>Base NetworkConnection class for server-to-client and client-to-server connection.</summary>
|
||||
public abstract class NetworkConnection
|
||||
{
|
||||
public const int LocalConnectionId = 0;
|
||||
|
||||
/// <summary>Unique identifier for this connection that is assigned by the transport layer.</summary>
|
||||
// assigned by transport, this id is unique for every connection on server.
|
||||
// clients don't know their own id and they don't know other client's ids.
|
||||
public readonly int connectionId;
|
||||
|
||||
/// <summary>Flag that indicates the client has been authenticated.</summary>
|
||||
public bool isAuthenticated;
|
||||
|
||||
/// <summary>General purpose object to hold authentication data, character selection, tokens, etc.</summary>
|
||||
public object authenticationData;
|
||||
|
||||
/// <summary>A server connection is ready after joining the game world.</summary>
|
||||
// TODO move this to ConnectionToClient so the flag only lives on server
|
||||
// connections? clients could use NetworkClient.ready to avoid redundant
|
||||
// state.
|
||||
public bool isReady;
|
||||
|
||||
/// <summary>Last time a message was received for this connection. Includes system and user messages.</summary>
|
||||
public float lastMessageTime;
|
||||
|
||||
/// <summary>This connection's main object (usually the player object).</summary>
|
||||
public NetworkIdentity identity { get; internal set; }
|
||||
|
||||
/// <summary>All NetworkIdentities owned by this connection. Can be main player, pets, etc.</summary>
|
||||
// .owned is now valid both on server and on client.
|
||||
// IMPORTANT: this needs to be <NetworkIdentity>, not <uint netId>.
|
||||
// fixes a bug where DestroyOwnedObjects wouldn't find the
|
||||
// netId anymore: https://github.com/vis2k/Mirror/issues/1380
|
||||
// Works fine with NetworkIdentity pointers though.
|
||||
public readonly HashSet<NetworkIdentity> owned = new HashSet<NetworkIdentity>();
|
||||
|
||||
// batching from server to client & client to server.
|
||||
// fewer transport calls give us significantly better performance/scale.
|
||||
//
|
||||
// for a 64KB max message transport and 64 bytes/message on average, we
|
||||
// reduce transport calls by a factor of 1000.
|
||||
//
|
||||
// depending on the transport, this can give 10x performance.
|
||||
//
|
||||
// Dictionary<channelId, batch> because we have multiple channels.
|
||||
protected Dictionary<int, Batcher> batches = new Dictionary<int, Batcher>();
|
||||
|
||||
/// <summary>last batch's remote timestamp. not interpolated. useful for NetworkTransform etc.</summary>
|
||||
// for any given NetworkMessage/Rpc/Cmd/OnSerialize, this was the time
|
||||
// on the REMOTE END when it was sent.
|
||||
//
|
||||
// NOTE: this is NOT in NetworkTime, it needs to be per-connection
|
||||
// because the server receives different batch timestamps from
|
||||
// different connections.
|
||||
public double remoteTimeStamp { get; internal set; }
|
||||
|
||||
internal NetworkConnection()
|
||||
{
|
||||
// set lastTime to current time when creating connection to make
|
||||
// sure it isn't instantly kicked for inactivity
|
||||
lastMessageTime = Time.time;
|
||||
}
|
||||
|
||||
internal NetworkConnection(int networkConnectionId) : this()
|
||||
{
|
||||
connectionId = networkConnectionId;
|
||||
}
|
||||
|
||||
// TODO if we only have Reliable/Unreliable, then we could initialize
|
||||
// two batches and avoid this code
|
||||
protected Batcher GetBatchForChannelId(int channelId)
|
||||
{
|
||||
// get existing or create new writer for the channelId
|
||||
Batcher batch;
|
||||
if (!batches.TryGetValue(channelId, out batch))
|
||||
{
|
||||
// get max batch size for this channel
|
||||
int threshold = Transport.active.GetBatchThreshold(channelId);
|
||||
|
||||
// create batcher
|
||||
batch = new Batcher(threshold);
|
||||
batches[channelId] = batch;
|
||||
}
|
||||
return batch;
|
||||
}
|
||||
|
||||
// Send stage one: NetworkMessage<T>
|
||||
/// <summary>Send a NetworkMessage to this connection over the given channel.</summary>
|
||||
public void Send<T>(T message, int channelId = Channels.Reliable)
|
||||
where T : struct, NetworkMessage
|
||||
{
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
// pack message
|
||||
NetworkMessages.Pack(message, writer);
|
||||
|
||||
// validate packet size immediately.
|
||||
// we know how much can fit into one batch at max.
|
||||
// if it's larger, log an error immediately with the type <T>.
|
||||
// previously we only logged in Update() when processing batches,
|
||||
// but there we don't have type information anymore.
|
||||
int max = NetworkMessages.MaxMessageSize(channelId);
|
||||
if (writer.Position > max)
|
||||
{
|
||||
Debug.LogError($"NetworkConnection.Send: message of type {typeof(T)} with a size of {writer.Position} bytes is larger than the max allowed message size in one batch: {max}.\nThe message was dropped, please make it smaller.");
|
||||
return;
|
||||
}
|
||||
|
||||
// send allocation free
|
||||
NetworkDiagnostics.OnSend(message, channelId, writer.Position, 1);
|
||||
Send(writer.ToArraySegment(), channelId);
|
||||
}
|
||||
}
|
||||
|
||||
// Send stage two: serialized NetworkMessage as ArraySegment<byte>
|
||||
// internal because no one except Mirror should send bytes directly to
|
||||
// the client. they would be detected as a message. send messages instead.
|
||||
// => make sure to validate message<T> size before calling Send<byte>!
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal virtual void Send(ArraySegment<byte> segment, int channelId = Channels.Reliable)
|
||||
{
|
||||
//Debug.Log($"ConnectionSend {this} bytes:{BitConverter.ToString(segment.Array, segment.Offset, segment.Count)}");
|
||||
|
||||
// add to batch no matter what.
|
||||
// batching will try to fit as many as possible into MTU.
|
||||
// but we still allow > MTU, e.g. kcp max packet size 144kb.
|
||||
// those are simply sent as single batches.
|
||||
//
|
||||
// IMPORTANT: do NOT send > batch sized messages directly:
|
||||
// - data race: large messages would be sent directly. small
|
||||
// messages would be sent in the batch at the end of frame
|
||||
// - timestamps: if batching assumes a timestamp, then large
|
||||
// messages need that too.
|
||||
//
|
||||
// NOTE: we ALWAYS batch. it's not optional, because the
|
||||
// receiver needs timestamps for NT etc.
|
||||
//
|
||||
// NOTE: we do NOT ValidatePacketSize here yet. the final packet
|
||||
// will be the full batch, including timestamp.
|
||||
GetBatchForChannelId(channelId).AddMessage(segment, NetworkTime.localTime);
|
||||
}
|
||||
|
||||
// Send stage three: hand off to transport
|
||||
protected abstract void SendToTransport(ArraySegment<byte> segment, int channelId = Channels.Reliable);
|
||||
|
||||
// flush batched messages at the end of every Update.
|
||||
internal virtual void Update()
|
||||
{
|
||||
// go through batches for all channels
|
||||
// foreach ((int key, Batcher batcher) in batches) // Unity 2020 doesn't support deconstruct yet
|
||||
foreach (KeyValuePair<int, Batcher> kvp in batches)
|
||||
{
|
||||
// make and send as many batches as necessary from the stored
|
||||
// messages.
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
// make a batch with our local time (double precision)
|
||||
while (kvp.Value.GetBatch(writer))
|
||||
{
|
||||
// message size is validated in Send<T>, with test coverage.
|
||||
// we can send directly without checking again.
|
||||
ArraySegment<byte> segment = writer.ToArraySegment();
|
||||
|
||||
// send to transport
|
||||
SendToTransport(segment, kvp.Key);
|
||||
//UnityEngine.Debug.Log($"sending batch of {writer.Position} bytes for channel={kvp.Key} connId={connectionId}");
|
||||
|
||||
// reset writer for each new batch
|
||||
writer.Position = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Check if we received a message within the last 'timeout' seconds.</summary>
|
||||
internal virtual bool IsAlive(float timeout) => Time.time - lastMessageTime < timeout;
|
||||
|
||||
/// <summary>Disconnects this connection.</summary>
|
||||
// for future reference, here is how Disconnects work in Mirror.
|
||||
//
|
||||
// first, there are two types of disconnects:
|
||||
// * voluntary: the other end simply disconnected
|
||||
// * involuntary: server disconnects a client by itself
|
||||
//
|
||||
// UNET had special (complex) code to handle both cases differently.
|
||||
//
|
||||
// Mirror handles both cases the same way:
|
||||
// * Disconnect is called from TOP to BOTTOM
|
||||
// NetworkServer/Client -> NetworkConnection -> Transport.Disconnect()
|
||||
// * Disconnect is handled from BOTTOM to TOP
|
||||
// Transport.OnDisconnected -> ...
|
||||
//
|
||||
// in other words, calling Disconnect() does no cleanup whatsoever.
|
||||
// it simply asks the transport to disconnect.
|
||||
// then later the transport events will do the clean up.
|
||||
public abstract void Disconnect();
|
||||
|
||||
public override string ToString() => $"connection({connectionId})";
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkConnection.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkConnection.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11ea41db366624109af1f0834bcdde2f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
220
Assets/Mirror/Core/NetworkConnectionToClient.cs
Normal file
220
Assets/Mirror/Core/NetworkConnectionToClient.cs
Normal file
@ -0,0 +1,220 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public class NetworkConnectionToClient : NetworkConnection
|
||||
{
|
||||
// rpcs are collected in a buffer, and then flushed out together.
|
||||
// this way we don't need one NetworkMessage per rpc.
|
||||
// => prepares for LocalWorldState as well.
|
||||
// ensure max size when adding!
|
||||
readonly NetworkWriter reliableRpcs = new NetworkWriter();
|
||||
readonly NetworkWriter unreliableRpcs = new NetworkWriter();
|
||||
|
||||
public virtual string address => Transport.active.ServerGetClientAddress(connectionId);
|
||||
|
||||
/// <summary>NetworkIdentities that this connection can see</summary>
|
||||
// TODO move to server's NetworkConnectionToClient?
|
||||
public readonly HashSet<NetworkIdentity> observing = new HashSet<NetworkIdentity>();
|
||||
|
||||
// unbatcher
|
||||
public Unbatcher unbatcher = new Unbatcher();
|
||||
|
||||
// server runs a time snapshot interpolation for each client's local time.
|
||||
// this is necessary for client auth movement to still be smooth on the
|
||||
// server for host mode.
|
||||
// TODO move them along server's timeline in the future.
|
||||
// perhaps with an offset.
|
||||
// for now, keep compatibility by manually constructing a timeline.
|
||||
ExponentialMovingAverage driftEma;
|
||||
ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter)
|
||||
public double remoteTimeline;
|
||||
public double remoteTimescale;
|
||||
double bufferTimeMultiplier = 2;
|
||||
double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier;
|
||||
|
||||
// <clienttime, snaps>
|
||||
readonly SortedList<double, TimeSnapshot> snapshots = new SortedList<double, TimeSnapshot>();
|
||||
|
||||
// Snapshot Buffer size limit to avoid ever growing list memory consumption attacks from clients.
|
||||
public int snapshotBufferSizeLimit = 64;
|
||||
|
||||
// ping for rtt (round trip time)
|
||||
// useful for statistics, lag compensation, etc.
|
||||
double lastPingTime = 0;
|
||||
internal ExponentialMovingAverage _rtt = new ExponentialMovingAverage(NetworkTime.PingWindowSize);
|
||||
|
||||
/// <summary>Round trip time (in seconds) that it takes a message to go server->client->server.</summary>
|
||||
public double rtt => _rtt.Value;
|
||||
|
||||
public NetworkConnectionToClient(int networkConnectionId)
|
||||
: base(networkConnectionId)
|
||||
{
|
||||
// initialize EMA with 'emaDuration' seconds worth of history.
|
||||
// 1 second holds 'sendRate' worth of values.
|
||||
// multiplied by emaDuration gives n-seconds.
|
||||
driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.snapshotSettings.driftEmaDuration);
|
||||
deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.snapshotSettings.deliveryTimeEmaDuration);
|
||||
|
||||
// buffer limit should be at least multiplier to have enough in there
|
||||
snapshotBufferSizeLimit = Mathf.Max((int)NetworkClient.snapshotSettings.bufferTimeMultiplier, snapshotBufferSizeLimit);
|
||||
}
|
||||
|
||||
public void OnTimeSnapshot(TimeSnapshot snapshot)
|
||||
{
|
||||
// protect against ever growing buffer size attacks
|
||||
if (snapshots.Count >= snapshotBufferSizeLimit) return;
|
||||
|
||||
// (optional) dynamic adjustment
|
||||
if (NetworkClient.snapshotSettings.dynamicAdjustment)
|
||||
{
|
||||
// set bufferTime on the fly.
|
||||
// shows in inspector for easier debugging :)
|
||||
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
|
||||
NetworkServer.sendInterval,
|
||||
deliveryTimeEma.StandardDeviation,
|
||||
NetworkClient.snapshotSettings.dynamicAdjustmentTolerance
|
||||
);
|
||||
// Debug.Log($"[Server]: {name} delivery std={serverDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} ");
|
||||
}
|
||||
|
||||
// insert into the server buffer & initialize / adjust / catchup
|
||||
SnapshotInterpolation.InsertAndAdjust(
|
||||
snapshots,
|
||||
NetworkClient.snapshotSettings.bufferLimit,
|
||||
snapshot,
|
||||
ref remoteTimeline,
|
||||
ref remoteTimescale,
|
||||
NetworkServer.sendInterval,
|
||||
bufferTime,
|
||||
NetworkClient.snapshotSettings.catchupSpeed,
|
||||
NetworkClient.snapshotSettings.slowdownSpeed,
|
||||
ref driftEma,
|
||||
NetworkClient.snapshotSettings.catchupNegativeThreshold,
|
||||
NetworkClient.snapshotSettings.catchupPositiveThreshold,
|
||||
ref deliveryTimeEma
|
||||
);
|
||||
}
|
||||
|
||||
public void UpdateTimeInterpolation()
|
||||
{
|
||||
// timeline starts when the first snapshot arrives.
|
||||
if (snapshots.Count > 0)
|
||||
{
|
||||
// progress local timeline.
|
||||
SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref remoteTimeline, remoteTimescale);
|
||||
|
||||
// progress local interpolation.
|
||||
// TimeSnapshot doesn't interpolate anything.
|
||||
// this is merely to keep removing older snapshots.
|
||||
SnapshotInterpolation.StepInterpolation(snapshots, remoteTimeline, out _, out _, out _);
|
||||
// Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}");
|
||||
}
|
||||
}
|
||||
|
||||
// Send stage three: hand off to transport
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void SendToTransport(ArraySegment<byte> segment, int channelId = Channels.Reliable) =>
|
||||
Transport.active.ServerSend(connectionId, segment, channelId);
|
||||
|
||||
protected virtual void UpdatePing()
|
||||
{
|
||||
// localTime (double) instead of Time.time for accuracy over days
|
||||
if (NetworkTime.localTime >= lastPingTime + NetworkTime.PingInterval)
|
||||
{
|
||||
// TODO it would be safer for the server to store the last N
|
||||
// messages' timestamp and only send a message number.
|
||||
// This way client's can't just modify the timestamp.
|
||||
// predictedTime parameter is 0 because the server doesn't predict.
|
||||
NetworkPingMessage pingMessage = new NetworkPingMessage(NetworkTime.localTime, 0);
|
||||
Send(pingMessage, Channels.Unreliable);
|
||||
lastPingTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
|
||||
internal override void Update()
|
||||
{
|
||||
UpdatePing();
|
||||
base.Update();
|
||||
}
|
||||
|
||||
/// <summary>Disconnects this connection.</summary>
|
||||
public override void Disconnect()
|
||||
{
|
||||
// set not ready and handle clientscene disconnect in any case
|
||||
// (might be client or host mode here)
|
||||
isReady = false;
|
||||
reliableRpcs.Position = 0;
|
||||
unreliableRpcs.Position = 0;
|
||||
Transport.active.ServerDisconnect(connectionId);
|
||||
|
||||
// IMPORTANT: NetworkConnection.Disconnect() is NOT called for
|
||||
// voluntary disconnects from the other end.
|
||||
// -> so all 'on disconnect' cleanup code needs to be in
|
||||
// OnTransportDisconnect, where it's called for both voluntary
|
||||
// and involuntary disconnects!
|
||||
}
|
||||
|
||||
internal void AddToObserving(NetworkIdentity netIdentity)
|
||||
{
|
||||
observing.Add(netIdentity);
|
||||
|
||||
// spawn identity for this conn
|
||||
NetworkServer.ShowForConnection(netIdentity, this);
|
||||
}
|
||||
|
||||
internal void RemoveFromObserving(NetworkIdentity netIdentity, bool isDestroyed)
|
||||
{
|
||||
observing.Remove(netIdentity);
|
||||
|
||||
if (!isDestroyed)
|
||||
{
|
||||
// hide identity for this conn
|
||||
NetworkServer.HideForConnection(netIdentity, this);
|
||||
}
|
||||
}
|
||||
|
||||
internal void RemoveFromObservingsObservers()
|
||||
{
|
||||
foreach (NetworkIdentity netIdentity in observing)
|
||||
{
|
||||
netIdentity.RemoveObserver(this);
|
||||
}
|
||||
observing.Clear();
|
||||
}
|
||||
|
||||
internal void AddOwnedObject(NetworkIdentity obj)
|
||||
{
|
||||
owned.Add(obj);
|
||||
}
|
||||
|
||||
internal void RemoveOwnedObject(NetworkIdentity obj)
|
||||
{
|
||||
owned.Remove(obj);
|
||||
}
|
||||
|
||||
internal void DestroyOwnedObjects()
|
||||
{
|
||||
// create a copy because the list might be modified when destroying
|
||||
HashSet<NetworkIdentity> tmp = new HashSet<NetworkIdentity>(owned);
|
||||
foreach (NetworkIdentity netIdentity in tmp)
|
||||
{
|
||||
if (netIdentity != null)
|
||||
{
|
||||
// unspawn scene objects, destroy instantiated objects.
|
||||
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3538
|
||||
if (netIdentity.sceneId != 0)
|
||||
NetworkServer.UnSpawn(netIdentity.gameObject);
|
||||
else
|
||||
NetworkServer.Destroy(netIdentity.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
// clear the hashset because we destroyed them all
|
||||
owned.Clear();
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkConnectionToClient.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkConnectionToClient.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb2195f8b29d24f0680a57fde2e9fd09
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
24
Assets/Mirror/Core/NetworkConnectionToServer.cs
Normal file
24
Assets/Mirror/Core/NetworkConnectionToServer.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public class NetworkConnectionToServer : NetworkConnection
|
||||
{
|
||||
// Send stage three: hand off to transport
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void SendToTransport(ArraySegment<byte> segment, int channelId = Channels.Reliable) =>
|
||||
Transport.active.ClientSend(segment, channelId);
|
||||
|
||||
/// <summary>Disconnects this connection.</summary>
|
||||
public override void Disconnect()
|
||||
{
|
||||
// set not ready and handle clientscene disconnect in any case
|
||||
// (might be client or host mode here)
|
||||
// TODO remove redundant state. have one source of truth for .ready!
|
||||
isReady = false;
|
||||
NetworkClient.ready = false;
|
||||
Transport.active.ClientDisconnect();
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkConnectionToServer.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkConnectionToServer.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 761977cbf38a34ded9dd89de45445675
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
63
Assets/Mirror/Core/NetworkDiagnostics.cs
Normal file
63
Assets/Mirror/Core/NetworkDiagnostics.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using System;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>Profiling statistics for tool to subscribe to (profiler etc.)</summary>
|
||||
public static class NetworkDiagnostics
|
||||
{
|
||||
/// <summary>Describes an outgoing message</summary>
|
||||
public readonly struct MessageInfo
|
||||
{
|
||||
/// <summary>The message being sent</summary>
|
||||
public readonly NetworkMessage message;
|
||||
/// <summary>channel through which the message was sent</summary>
|
||||
public readonly int channel;
|
||||
/// <summary>how big was the message (does not include transport headers)</summary>
|
||||
public readonly int bytes;
|
||||
/// <summary>How many connections was the message sent to.</summary>
|
||||
public readonly int count;
|
||||
|
||||
internal MessageInfo(NetworkMessage message, int channel, int bytes, int count)
|
||||
{
|
||||
this.message = message;
|
||||
this.channel = channel;
|
||||
this.bytes = bytes;
|
||||
this.count = count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Event for when Mirror sends a message. Can be subscribed to.</summary>
|
||||
public static event Action<MessageInfo> OutMessageEvent;
|
||||
|
||||
/// <summary>Event for when Mirror receives a message. Can be subscribed to.</summary>
|
||||
public static event Action<MessageInfo> InMessageEvent;
|
||||
|
||||
// RuntimeInitializeOnLoadMethod -> fast playmode without domain reload
|
||||
[UnityEngine.RuntimeInitializeOnLoadMethod]
|
||||
static void ResetStatics()
|
||||
{
|
||||
InMessageEvent = null;
|
||||
OutMessageEvent = null;
|
||||
}
|
||||
|
||||
internal static void OnSend<T>(T message, int channel, int bytes, int count)
|
||||
where T : struct, NetworkMessage
|
||||
{
|
||||
if (count > 0 && OutMessageEvent != null)
|
||||
{
|
||||
MessageInfo outMessage = new MessageInfo(message, channel, bytes, count);
|
||||
OutMessageEvent?.Invoke(outMessage);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void OnReceive<T>(T message, int channel, int bytes)
|
||||
where T : struct, NetworkMessage
|
||||
{
|
||||
if (InMessageEvent != null)
|
||||
{
|
||||
MessageInfo inMessage = new MessageInfo(message, channel, bytes, 1);
|
||||
InMessageEvent?.Invoke(inMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkDiagnostics.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkDiagnostics.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3754b39e5f8740fd93f3337b2c4274e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
1390
Assets/Mirror/Core/NetworkIdentity.cs
Normal file
1390
Assets/Mirror/Core/NetworkIdentity.cs
Normal file
File diff suppressed because it is too large
Load Diff
11
Assets/Mirror/Core/NetworkIdentity.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkIdentity.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b91ecbcc199f4492b9a91e820070131
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
211
Assets/Mirror/Core/NetworkLoop.cs
Normal file
211
Assets/Mirror/Core/NetworkLoop.cs
Normal file
@ -0,0 +1,211 @@
|
||||
// our ideal update looks like this:
|
||||
// transport.process_incoming()
|
||||
// update_world()
|
||||
// transport.process_outgoing()
|
||||
//
|
||||
// this way we avoid unnecessary latency for low-ish server tick rates.
|
||||
// for example, if we were to use this tick:
|
||||
// transport.process_incoming/outgoing()
|
||||
// update_world()
|
||||
//
|
||||
// then anything sent in update_world wouldn't be actually sent out by the
|
||||
// transport until the next frame. if server runs at 60Hz, then this can add
|
||||
// 16ms latency for every single packet.
|
||||
//
|
||||
// => instead we process incoming, update world, process_outgoing in the same
|
||||
// frame. it's more clear (no race conditions) and lower latency.
|
||||
// => we need to add custom Update functions to the Unity engine:
|
||||
// NetworkEarlyUpdate before Update()/FixedUpdate()
|
||||
// NetworkLateUpdate after LateUpdate()
|
||||
// this way the user can update the world in Update/FixedUpdate/LateUpdate
|
||||
// and networking still runs before/after those functions no matter what!
|
||||
// => see also: https://docs.unity3d.com/Manual/ExecutionOrder.html
|
||||
// => update order:
|
||||
// * we add to the end of EarlyUpdate so it runs after any Unity initializations
|
||||
// * we add to the end of PreLateUpdate so it runs after LateUpdate(). adding
|
||||
// to the beginning of PostLateUpdate doesn't actually work.
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.LowLevel;
|
||||
using UnityEngine.PlayerLoop;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public static class NetworkLoop
|
||||
{
|
||||
// helper enum to add loop to begin/end of subSystemList
|
||||
internal enum AddMode { Beginning, End }
|
||||
|
||||
// callbacks for others to hook into if they need Early/LateUpdate.
|
||||
public static Action OnEarlyUpdate;
|
||||
public static Action OnLateUpdate;
|
||||
|
||||
// RuntimeInitializeOnLoadMethod -> fast playmode without domain reload
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
static void ResetStatics()
|
||||
{
|
||||
OnEarlyUpdate = null;
|
||||
OnLateUpdate = null;
|
||||
}
|
||||
|
||||
// helper function to find an update function's index in a player loop
|
||||
// type. this is used for testing to guarantee our functions are added
|
||||
// at the beginning/end properly.
|
||||
internal static int FindPlayerLoopEntryIndex(PlayerLoopSystem.UpdateFunction function, PlayerLoopSystem playerLoop, Type playerLoopSystemType)
|
||||
{
|
||||
// did we find the type? e.g. EarlyUpdate/PreLateUpdate/etc.
|
||||
if (playerLoop.type == playerLoopSystemType)
|
||||
return Array.FindIndex(playerLoop.subSystemList, (elem => elem.updateDelegate == function));
|
||||
|
||||
// recursively keep looking
|
||||
if (playerLoop.subSystemList != null)
|
||||
{
|
||||
for (int i = 0; i < playerLoop.subSystemList.Length; ++i)
|
||||
{
|
||||
int index = FindPlayerLoopEntryIndex(function, playerLoop.subSystemList[i], playerLoopSystemType);
|
||||
if (index != -1) return index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// MODIFIED AddSystemToPlayerLoopList from Unity.Entities.ScriptBehaviourUpdateOrder (ECS)
|
||||
//
|
||||
// => adds an update function to the Unity internal update type.
|
||||
// => Unity has different update loops:
|
||||
// https://medium.com/@thebeardphantom/unity-2018-and-playerloop-5c46a12a677
|
||||
// EarlyUpdate
|
||||
// FixedUpdate
|
||||
// PreUpdate
|
||||
// Update
|
||||
// PreLateUpdate
|
||||
// PostLateUpdate
|
||||
//
|
||||
// function: the custom update function to add
|
||||
// IMPORTANT: according to a comment in Unity.Entities.ScriptBehaviourUpdateOrder,
|
||||
// the UpdateFunction can not be virtual because
|
||||
// Mono 4.6 has problems invoking virtual methods
|
||||
// as delegates from native!
|
||||
// ownerType: the .type to fill in so it's obvious who the new function
|
||||
// belongs to. seems to be mostly for debugging. pass any.
|
||||
// addMode: prepend or append to update list
|
||||
internal static bool AddToPlayerLoop(PlayerLoopSystem.UpdateFunction function, Type ownerType, ref PlayerLoopSystem playerLoop, Type playerLoopSystemType, AddMode addMode)
|
||||
{
|
||||
// did we find the type? e.g. EarlyUpdate/PreLateUpdate/etc.
|
||||
if (playerLoop.type == playerLoopSystemType)
|
||||
{
|
||||
// debugging
|
||||
//Debug.Log($"Found playerLoop of type {playerLoop.type} with {playerLoop.subSystemList.Length} Functions:");
|
||||
//foreach (PlayerLoopSystem sys in playerLoop.subSystemList)
|
||||
// Debug.Log($" ->{sys.type}");
|
||||
|
||||
// make sure the function wasn't added yet.
|
||||
// with domain reload disabled, it would otherwise be added twice:
|
||||
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3392
|
||||
if (Array.FindIndex(playerLoop.subSystemList, (s => s.updateDelegate == function)) != -1)
|
||||
{
|
||||
// loop contains the function, so return true.
|
||||
return true;
|
||||
}
|
||||
|
||||
// resize & expand subSystemList to fit one more entry
|
||||
int oldListLength = (playerLoop.subSystemList != null) ? playerLoop.subSystemList.Length : 0;
|
||||
Array.Resize(ref playerLoop.subSystemList, oldListLength + 1);
|
||||
|
||||
// IMPORTANT: always insert a FRESH PlayerLoopSystem!
|
||||
// We CAN NOT resize and then OVERWRITE an entry's type/loop.
|
||||
// => PlayerLoopSystem has native IntPtr loop members
|
||||
// => forgetting to clear those would cause undefined behaviour!
|
||||
// see also: https://github.com/vis2k/Mirror/pull/2652
|
||||
PlayerLoopSystem system = new PlayerLoopSystem {
|
||||
type = ownerType,
|
||||
updateDelegate = function
|
||||
};
|
||||
|
||||
// prepend our custom loop to the beginning
|
||||
if (addMode == AddMode.Beginning)
|
||||
{
|
||||
// shift to the right, write into first array element
|
||||
Array.Copy(playerLoop.subSystemList, 0, playerLoop.subSystemList, 1, playerLoop.subSystemList.Length - 1);
|
||||
playerLoop.subSystemList[0] = system;
|
||||
}
|
||||
// append our custom loop to the end
|
||||
else if (addMode == AddMode.End)
|
||||
{
|
||||
// simply write into last array element
|
||||
playerLoop.subSystemList[oldListLength] = system;
|
||||
}
|
||||
|
||||
// debugging
|
||||
//Debug.Log($"New playerLoop of type {playerLoop.type} with {playerLoop.subSystemList.Length} Functions:");
|
||||
//foreach (PlayerLoopSystem sys in playerLoop.subSystemList)
|
||||
// Debug.Log($" ->{sys.type}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// recursively keep looking
|
||||
if (playerLoop.subSystemList != null)
|
||||
{
|
||||
for (int i = 0; i < playerLoop.subSystemList.Length; ++i)
|
||||
{
|
||||
if (AddToPlayerLoop(function, ownerType, ref playerLoop.subSystemList[i], playerLoopSystemType, addMode))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// hook into Unity runtime to actually add our custom functions
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
static void RuntimeInitializeOnLoad()
|
||||
{
|
||||
//Debug.Log("Mirror: adding Network[Early/Late]Update to Unity...");
|
||||
|
||||
// get loop
|
||||
// 2019 has GetCURRENTPlayerLoop which is safe to use without
|
||||
// breaking other custom system's custom loops.
|
||||
// see also: https://github.com/vis2k/Mirror/pull/2627/files
|
||||
PlayerLoopSystem playerLoop = PlayerLoop.GetCurrentPlayerLoop();
|
||||
|
||||
// add NetworkEarlyUpdate to the end of EarlyUpdate so it runs after
|
||||
// any Unity initializations but before the first Update/FixedUpdate
|
||||
AddToPlayerLoop(NetworkEarlyUpdate, typeof(NetworkLoop), ref playerLoop, typeof(EarlyUpdate), AddMode.End);
|
||||
|
||||
// add NetworkLateUpdate to the end of PreLateUpdate so it runs after
|
||||
// LateUpdate(). adding to the beginning of PostLateUpdate doesn't
|
||||
// actually work.
|
||||
AddToPlayerLoop(NetworkLateUpdate, typeof(NetworkLoop), ref playerLoop, typeof(PreLateUpdate), AddMode.End);
|
||||
|
||||
// set the new loop
|
||||
PlayerLoop.SetPlayerLoop(playerLoop);
|
||||
}
|
||||
|
||||
static void NetworkEarlyUpdate()
|
||||
{
|
||||
// loop functions run in edit mode and in play mode.
|
||||
// however, we only want to call NetworkServer/Client in play mode.
|
||||
if (!Application.isPlaying) return;
|
||||
|
||||
NetworkTime.EarlyUpdate();
|
||||
//Debug.Log($"NetworkEarlyUpdate {Time.time}");
|
||||
NetworkServer.NetworkEarlyUpdate();
|
||||
NetworkClient.NetworkEarlyUpdate();
|
||||
// invoke event after mirror has done it's early updating.
|
||||
OnEarlyUpdate?.Invoke();
|
||||
}
|
||||
|
||||
static void NetworkLateUpdate()
|
||||
{
|
||||
// loop functions run in edit mode and in play mode.
|
||||
// however, we only want to call NetworkServer/Client in play mode.
|
||||
if (!Application.isPlaying) return;
|
||||
|
||||
//Debug.Log($"NetworkLateUpdate {Time.time}");
|
||||
// invoke event before mirror does its final late updating.
|
||||
OnLateUpdate?.Invoke();
|
||||
NetworkServer.NetworkLateUpdate();
|
||||
NetworkClient.NetworkLateUpdate();
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkLoop.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkLoop.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c6cec4e279774b919386e05545317b8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
1504
Assets/Mirror/Core/NetworkManager.cs
Normal file
1504
Assets/Mirror/Core/NetworkManager.cs
Normal file
File diff suppressed because it is too large
Load Diff
11
Assets/Mirror/Core/NetworkManager.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkManager.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8aab4c8111b7c411b9b92cf3dbc5bd4e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
162
Assets/Mirror/Core/NetworkManagerHUD.cs
Normal file
162
Assets/Mirror/Core/NetworkManagerHUD.cs
Normal file
@ -0,0 +1,162 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>Shows NetworkManager controls in a GUI at runtime.</summary>
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Manager HUD")]
|
||||
[RequireComponent(typeof(NetworkManager))]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-manager-hud")]
|
||||
public class NetworkManagerHUD : MonoBehaviour
|
||||
{
|
||||
NetworkManager manager;
|
||||
|
||||
public int offsetX;
|
||||
public int offsetY;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
manager = GetComponent<NetworkManager>();
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
// If this width is changed, also change offsetX in GUIConsole::OnGUI
|
||||
int width = 300;
|
||||
|
||||
GUILayout.BeginArea(new Rect(10 + offsetX, 40 + offsetY, width, 9999));
|
||||
|
||||
if (!NetworkClient.isConnected && !NetworkServer.active)
|
||||
StartButtons();
|
||||
else
|
||||
StatusLabels();
|
||||
|
||||
if (NetworkClient.isConnected && !NetworkClient.ready)
|
||||
{
|
||||
if (GUILayout.Button("Client Ready"))
|
||||
{
|
||||
// client ready
|
||||
NetworkClient.Ready();
|
||||
if (NetworkClient.localPlayer == null)
|
||||
NetworkClient.AddPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
StopButtons();
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
void StartButtons()
|
||||
{
|
||||
if (!NetworkClient.active)
|
||||
{
|
||||
#if UNITY_WEBGL
|
||||
// cant be a server in webgl build
|
||||
if (GUILayout.Button("Single Player"))
|
||||
{
|
||||
NetworkServer.dontListen = true;
|
||||
manager.StartHost();
|
||||
}
|
||||
#else
|
||||
// Server + Client
|
||||
if (GUILayout.Button("Host (Server + Client)"))
|
||||
manager.StartHost();
|
||||
#endif
|
||||
|
||||
// Client + IP (+ PORT)
|
||||
GUILayout.BeginHorizontal();
|
||||
|
||||
if (GUILayout.Button("Client"))
|
||||
manager.StartClient();
|
||||
|
||||
manager.networkAddress = GUILayout.TextField(manager.networkAddress);
|
||||
// only show a port field if we have a port transport
|
||||
// we can't have "IP:PORT" in the address field since this only
|
||||
// works for IPV4:PORT.
|
||||
// for IPV6:PORT it would be misleading since IPV6 contains ":":
|
||||
// 2001:0db8:0000:0000:0000:ff00:0042:8329
|
||||
if (Transport.active is PortTransport portTransport)
|
||||
{
|
||||
// use TryParse in case someone tries to enter non-numeric characters
|
||||
if (ushort.TryParse(GUILayout.TextField(portTransport.Port.ToString()), out ushort port))
|
||||
portTransport.Port = port;
|
||||
}
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
// Server Only
|
||||
#if UNITY_WEBGL
|
||||
// cant be a server in webgl build
|
||||
GUILayout.Box("( WebGL cannot be server )");
|
||||
#else
|
||||
if (GUILayout.Button("Server Only"))
|
||||
manager.StartServer();
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
// Connecting
|
||||
GUILayout.Label($"Connecting to {manager.networkAddress}..");
|
||||
if (GUILayout.Button("Cancel Connection Attempt"))
|
||||
manager.StopClient();
|
||||
}
|
||||
}
|
||||
|
||||
void StatusLabels()
|
||||
{
|
||||
// host mode
|
||||
// display separately because this always confused people:
|
||||
// Server: ...
|
||||
// Client: ...
|
||||
if (NetworkServer.active && NetworkClient.active)
|
||||
{
|
||||
// host mode
|
||||
GUILayout.Label($"<b>Host</b>: running via {Transport.active}");
|
||||
}
|
||||
else if (NetworkServer.active)
|
||||
{
|
||||
// server only
|
||||
GUILayout.Label($"<b>Server</b>: running via {Transport.active}");
|
||||
}
|
||||
else if (NetworkClient.isConnected)
|
||||
{
|
||||
// client only
|
||||
GUILayout.Label($"<b>Client</b>: connected to {manager.networkAddress} via {Transport.active}");
|
||||
}
|
||||
}
|
||||
|
||||
void StopButtons()
|
||||
{
|
||||
if (NetworkServer.active && NetworkClient.isConnected)
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
#if UNITY_WEBGL
|
||||
if (GUILayout.Button("Stop Single Player"))
|
||||
manager.StopHost();
|
||||
#else
|
||||
// stop host if host mode
|
||||
if (GUILayout.Button("Stop Host"))
|
||||
manager.StopHost();
|
||||
|
||||
// stop client if host mode, leaving server up
|
||||
if (GUILayout.Button("Stop Client"))
|
||||
manager.StopClient();
|
||||
#endif
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
else if (NetworkClient.isConnected)
|
||||
{
|
||||
// stop client if client-only
|
||||
if (GUILayout.Button("Stop Client"))
|
||||
manager.StopClient();
|
||||
}
|
||||
else if (NetworkServer.active)
|
||||
{
|
||||
// stop server if server-only
|
||||
if (GUILayout.Button("Stop Server"))
|
||||
manager.StopServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkManagerHUD.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkManagerHUD.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6442dc8070ceb41f094e44de0bf87274
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
4
Assets/Mirror/Core/NetworkMessage.cs
Normal file
4
Assets/Mirror/Core/NetworkMessage.cs
Normal file
@ -0,0 +1,4 @@
|
||||
namespace Mirror
|
||||
{
|
||||
public interface NetworkMessage {}
|
||||
}
|
11
Assets/Mirror/Core/NetworkMessage.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkMessage.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb04e4848a2e4452aa2dbd7adb801c51
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
210
Assets/Mirror/Core/NetworkMessages.cs
Normal file
210
Assets/Mirror/Core/NetworkMessages.cs
Normal file
@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// for performance, we (ab)use c# generics to cache the message id in a static field
|
||||
// this is significantly faster than doing the computation at runtime or looking up cached results via Dictionary
|
||||
// generic classes have separate static fields per type specification
|
||||
public static class NetworkMessageId<T> where T : struct, NetworkMessage
|
||||
{
|
||||
// automated message id from type hash.
|
||||
// platform independent via stable hashcode.
|
||||
// => convenient so we don't need to track messageIds across projects
|
||||
// => addons can work with each other without knowing their ids before
|
||||
// => 2 bytes is enough to avoid collisions.
|
||||
// registering a messageId twice will log a warning anyway.
|
||||
public static readonly ushort Id = CalculateId();
|
||||
|
||||
// Gets the 32bit fnv1a hash
|
||||
// To get it down to 16bit but still reduce hash collisions we cant just cast it to ushort
|
||||
// Instead we take the highest 16bits of the 32bit hash and fold them with xor into the lower 16bits
|
||||
// This will create a more uniform 16bit hash, the method is described in:
|
||||
// http://www.isthe.com/chongo/tech/comp/fnv/ in section "Changing the FNV hash size - xor-folding"
|
||||
static ushort CalculateId() => typeof(T).FullName.GetStableHashCode16();
|
||||
}
|
||||
|
||||
// message packing all in one place, instead of constructing headers in all
|
||||
// kinds of different places
|
||||
//
|
||||
// MsgType (2 bytes)
|
||||
// Content (ContentSize bytes)
|
||||
public static class NetworkMessages
|
||||
{
|
||||
// size of message id header in bytes
|
||||
public const int IdSize = sizeof(ushort);
|
||||
|
||||
// Id <> Type lookup for debugging, profiler, etc.
|
||||
// important when debugging messageId errors!
|
||||
public static readonly Dictionary<ushort, Type> Lookup =
|
||||
new Dictionary<ushort, Type>();
|
||||
|
||||
// dump all types for debugging
|
||||
public static void LogTypes()
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("NetworkMessageIds:");
|
||||
foreach (KeyValuePair<ushort, Type> kvp in Lookup)
|
||||
{
|
||||
builder.AppendLine($" Id={kvp.Key} = {kvp.Value}");
|
||||
}
|
||||
Debug.Log(builder.ToString());
|
||||
}
|
||||
|
||||
// max message content size (without header) calculation for convenience
|
||||
// -> Transport.GetMaxPacketSize is the raw maximum
|
||||
// -> Every message gets serialized into <<id, content>>
|
||||
// -> Every serialized message get put into a batch with one timestamp per batch
|
||||
// -> Every message in a batch has a varuint size header.
|
||||
// use the worst case VarUInt size for the largest possible
|
||||
// message size = int.max.
|
||||
public static int MaxContentSize(int channelId)
|
||||
{
|
||||
// calculate the max possible size that can fit in a batch
|
||||
int transportMax = Transport.active.GetMaxPacketSize(channelId);
|
||||
return transportMax - IdSize - Batcher.MaxMessageOverhead(transportMax);
|
||||
}
|
||||
|
||||
// max message size which includes header + content.
|
||||
public static int MaxMessageSize(int channelId) =>
|
||||
MaxContentSize(channelId) + IdSize;
|
||||
|
||||
// automated message id from type hash.
|
||||
// platform independent via stable hashcode.
|
||||
// => convenient so we don't need to track messageIds across projects
|
||||
// => addons can work with each other without knowing their ids before
|
||||
// => 2 bytes is enough to avoid collisions.
|
||||
// registering a messageId twice will log a warning anyway.
|
||||
// keep this for convenience. easier to use than NetworkMessageId<T>.Id.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ushort GetId<T>() where T : struct, NetworkMessage =>
|
||||
NetworkMessageId<T>.Id;
|
||||
|
||||
// pack message before sending
|
||||
// -> NetworkWriter passed as arg so that we can use .ToArraySegment
|
||||
// and do an allocation free send before recycling it.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Pack<T>(T message, NetworkWriter writer)
|
||||
where T : struct, NetworkMessage
|
||||
{
|
||||
writer.WriteUShort(NetworkMessageId<T>.Id);
|
||||
writer.Write(message);
|
||||
}
|
||||
|
||||
// read only the message id.
|
||||
// common function in case we ever change the header size.
|
||||
public static bool UnpackId(NetworkReader reader, out ushort messageId)
|
||||
{
|
||||
// read message type
|
||||
try
|
||||
{
|
||||
messageId = reader.ReadUShort();
|
||||
return true;
|
||||
}
|
||||
catch (System.IO.EndOfStreamException)
|
||||
{
|
||||
messageId = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// version for handlers with channelId
|
||||
// inline! only exists for 20-30 messages and they call it all the time.
|
||||
internal static NetworkMessageDelegate WrapHandler<T, C>(Action<C, T, int> handler, bool requireAuthentication, bool exceptionsDisconnect)
|
||||
where T : struct, NetworkMessage
|
||||
where C : NetworkConnection
|
||||
=> (conn, reader, channelId) =>
|
||||
{
|
||||
// protect against DOS attacks if attackers try to send invalid
|
||||
// data packets to crash the server/client. there are a thousand
|
||||
// ways to cause an exception in data handling:
|
||||
// - invalid headers
|
||||
// - invalid message ids
|
||||
// - invalid data causing exceptions
|
||||
// - negative ReadBytesAndSize prefixes
|
||||
// - invalid utf8 strings
|
||||
// - etc.
|
||||
//
|
||||
// let's catch them all and then disconnect that connection to avoid
|
||||
// further attacks.
|
||||
T message = default;
|
||||
// record start position for NetworkDiagnostics because reader might contain multiple messages if using batching
|
||||
int startPos = reader.Position;
|
||||
try
|
||||
{
|
||||
if (requireAuthentication && !conn.isAuthenticated)
|
||||
{
|
||||
// message requires authentication, but the connection was not authenticated
|
||||
Debug.LogWarning($"Disconnecting connection: {conn}. Received message {typeof(T)} that required authentication, but the user has not authenticated yet");
|
||||
conn.Disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
//Debug.Log($"ConnectionRecv {conn} msgType:{typeof(T)} content:{BitConverter.ToString(reader.buffer.Array, reader.buffer.Offset, reader.buffer.Count)}");
|
||||
|
||||
// if it is a value type, just use default(T)
|
||||
// otherwise allocate a new instance
|
||||
message = reader.Read<T>();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// should we disconnect on exceptions?
|
||||
if (exceptionsDisconnect)
|
||||
{
|
||||
Debug.LogError($"Disconnecting connection: {conn} because reading a message of type {typeof(T)} caused an Exception. This can happen if the other side accidentally (or an attacker intentionally) sent invalid data. Reason: {exception}");
|
||||
conn.Disconnect();
|
||||
return;
|
||||
}
|
||||
// otherwise log it but allow the connection to keep playing
|
||||
else
|
||||
{
|
||||
Debug.LogError($"Caught an Exception when reading a message from: {conn} of type {typeof(T)}. Reason: {exception}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
int endPos = reader.Position;
|
||||
// TODO: Figure out the correct channel
|
||||
NetworkDiagnostics.OnReceive(message, channelId, endPos - startPos);
|
||||
}
|
||||
|
||||
// user handler exception should not stop the whole server
|
||||
try
|
||||
{
|
||||
// user implemented handler
|
||||
handler((C)conn, message, channelId);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// should we disconnect on exceptions?
|
||||
if (exceptionsDisconnect)
|
||||
{
|
||||
Debug.LogError($"Disconnecting connection: {conn} because handling a message of type {typeof(T)} caused an Exception. This can happen if the other side accidentally (or an attacker intentionally) sent invalid data. Reason: {exception}");
|
||||
conn.Disconnect();
|
||||
}
|
||||
// otherwise log it but allow the connection to keep playing
|
||||
else
|
||||
{
|
||||
Debug.LogError($"Caught an Exception when handling a message from: {conn} of type {typeof(T)}. Reason: {exception}");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// version for handlers without channelId
|
||||
// TODO obsolete this some day to always use the channelId version.
|
||||
// all handlers in this version are wrapped with 1 extra action.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static NetworkMessageDelegate WrapHandler<T, C>(Action<C, T> handler, bool requireAuthentication, bool exceptionsDisconnect)
|
||||
where T : struct, NetworkMessage
|
||||
where C : NetworkConnection
|
||||
{
|
||||
// wrap action as channelId version, call original
|
||||
void Wrapped(C conn, T msg, int _) => handler(conn, msg);
|
||||
return WrapHandler((Action<C, T, int>)Wrapped, requireAuthentication, exceptionsDisconnect);
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkMessages.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkMessages.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2db134099f0df4d96a84ae7a0cd9b4bc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
249
Assets/Mirror/Core/NetworkReader.cs
Normal file
249
Assets/Mirror/Core/NetworkReader.cs
Normal file
@ -0,0 +1,249 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>Network Reader for most simple types like floats, ints, buffers, structs, etc. Use NetworkReaderPool.GetReader() to avoid allocations.</summary>
|
||||
// Note: This class is intended to be extremely pedantic,
|
||||
// and throw exceptions whenever stuff is going slightly wrong.
|
||||
// The exceptions will be handled in NetworkServer/NetworkClient.
|
||||
//
|
||||
// Note that NetworkWriter can be passed in constructor thanks to implicit
|
||||
// ArraySegment conversion:
|
||||
// NetworkReader reader = new NetworkReader(writer);
|
||||
public class NetworkReader
|
||||
{
|
||||
// internal buffer
|
||||
// byte[] pointer would work, but we use ArraySegment to also support
|
||||
// the ArraySegment constructor
|
||||
internal ArraySegment<byte> buffer;
|
||||
|
||||
/// <summary>Next position to read from the buffer</summary>
|
||||
// 'int' is the best type for .Position. 'short' is too small if we send >32kb which would result in negative .Position
|
||||
// -> converting long to int is fine until 2GB of data (MAX_INT), so we don't have to worry about overflows here
|
||||
public int Position;
|
||||
|
||||
/// <summary>Remaining bytes that can be read, for convenience.</summary>
|
||||
public int Remaining => buffer.Count - Position;
|
||||
|
||||
/// <summary>Total buffer capacity, independent of reader position.</summary>
|
||||
public int Capacity => buffer.Count;
|
||||
|
||||
// cache encoding for ReadString instead of creating it with each time
|
||||
// 1000 readers before: 1MB GC, 30ms
|
||||
// 1000 readers after: 0.8MB GC, 18ms
|
||||
// member(!) to avoid static state.
|
||||
//
|
||||
// throwOnInvalidBytes is true.
|
||||
// if false, it would silently ignore the invalid bytes but continue
|
||||
// with the valid ones, creating strings like "a<><61><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>".
|
||||
// instead, we want to catch it manually and return String.Empty.
|
||||
// this is safer. see test: ReadString_InvalidUTF8().
|
||||
internal readonly UTF8Encoding encoding = new UTF8Encoding(false, true);
|
||||
|
||||
// while allocation free ReadArraySegment is encouraged,
|
||||
// some functions can allocate a new byte[], List<T>, Texture, etc.
|
||||
// we should keep a reasonable allocation size limit:
|
||||
// -> server won't accidentally allocate 2GB on a mobile device
|
||||
// -> client won't allocate 2GB on server for ClientToServer [SyncVar]s
|
||||
// -> unlike max string length of 64 KB, we need a larger limit here.
|
||||
// large enough to not break existing projects,
|
||||
// small enough to reasonably limit allocation attacks.
|
||||
// -> we don't know the exact size of ReadList<T> etc. because <T> is
|
||||
// managed. instead, this is considered a 'collection length' limit.
|
||||
public const int AllocationLimit = 1024 * 1024 * 16; // 16 MB * sizeof(T)
|
||||
|
||||
public NetworkReader(ArraySegment<byte> segment)
|
||||
{
|
||||
buffer = segment;
|
||||
}
|
||||
|
||||
#if !UNITY_2021_3_OR_NEWER
|
||||
// Unity 2019 doesn't have the implicit byte[] to segment conversion yet
|
||||
public NetworkReader(byte[] bytes)
|
||||
{
|
||||
buffer = new ArraySegment<byte>(bytes, 0, bytes.Length);
|
||||
}
|
||||
#endif
|
||||
|
||||
// sometimes it's useful to point a reader on another buffer instead of
|
||||
// allocating a new reader (e.g. NetworkReaderPool)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetBuffer(ArraySegment<byte> segment)
|
||||
{
|
||||
buffer = segment;
|
||||
Position = 0;
|
||||
}
|
||||
|
||||
#if !UNITY_2021_3_OR_NEWER
|
||||
// Unity 2019 doesn't have the implicit byte[] to segment conversion yet
|
||||
public void SetBuffer(byte[] bytes)
|
||||
{
|
||||
buffer = new ArraySegment<byte>(bytes, 0, bytes.Length);
|
||||
Position = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
// ReadBlittable<T> from DOTSNET
|
||||
// this is extremely fast, but only works for blittable types.
|
||||
// => private to make sure nobody accidentally uses it for non-blittable
|
||||
//
|
||||
// Benchmark: see NetworkWriter.WriteBlittable!
|
||||
//
|
||||
// Note:
|
||||
// ReadBlittable assumes same endianness for server & client.
|
||||
// All Unity 2018+ platforms are little endian.
|
||||
//
|
||||
// This is not safe to expose to random structs.
|
||||
// * StructLayout.Sequential is the default, which is safe.
|
||||
// if the struct contains a reference type, it is converted to Auto.
|
||||
// but since all structs here are unmanaged blittable, it's safe.
|
||||
// see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.layoutkind?view=netframework-4.8#system-runtime-interopservices-layoutkind-sequential
|
||||
// * StructLayout.Pack depends on CPU word size.
|
||||
// this may be different 4 or 8 on some ARM systems, etc.
|
||||
// this is not safe, and would cause bytes/shorts etc. to be padded.
|
||||
// see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.structlayoutattribute.pack?view=net-6.0
|
||||
// * If we force pack all to '1', they would have no padding which is
|
||||
// great for bandwidth. but on some android systems, CPU can't read
|
||||
// unaligned memory.
|
||||
// see also: https://github.com/vis2k/Mirror/issues/3044
|
||||
// * The only option would be to force explicit layout with multiples
|
||||
// of word size. but this requires lots of weaver checking and is
|
||||
// still questionable (IL2CPP etc.).
|
||||
//
|
||||
// Note: inlining ReadBlittable is enough. don't inline ReadInt etc.
|
||||
// we don't want ReadBlittable to be copied in place everywhere.
|
||||
internal unsafe T ReadBlittable<T>()
|
||||
where T : unmanaged
|
||||
{
|
||||
// check if blittable for safety
|
||||
#if UNITY_EDITOR
|
||||
if (!UnsafeUtility.IsBlittable(typeof(T)))
|
||||
{
|
||||
throw new ArgumentException($"{typeof(T)} is not blittable!");
|
||||
}
|
||||
#endif
|
||||
|
||||
// calculate size
|
||||
// sizeof(T) gets the managed size at compile time.
|
||||
// Marshal.SizeOf<T> gets the unmanaged size at runtime (slow).
|
||||
// => our 1mio writes benchmark is 6x slower with Marshal.SizeOf<T>
|
||||
// => for blittable types, sizeof(T) is even recommended:
|
||||
// https://docs.microsoft.com/en-us/dotnet/standard/native-interop/best-practices
|
||||
int size = sizeof(T);
|
||||
|
||||
// ensure remaining
|
||||
if (Remaining < size)
|
||||
{
|
||||
throw new EndOfStreamException($"ReadBlittable<{typeof(T)}> not enough data in buffer to read {size} bytes: {ToString()}");
|
||||
}
|
||||
|
||||
// read blittable
|
||||
T value;
|
||||
fixed (byte* ptr = &buffer.Array[buffer.Offset + Position])
|
||||
{
|
||||
#if UNITY_ANDROID
|
||||
// on some android systems, reading *(T*)ptr throws a NRE if
|
||||
// the ptr isn't aligned (i.e. if Position is 1,2,3,5, etc.).
|
||||
// here we have to use memcpy.
|
||||
//
|
||||
// => we can't get a pointer of a struct in C# without
|
||||
// marshalling allocations
|
||||
// => instead, we stack allocate an array of type T and use that
|
||||
// => stackalloc avoids GC and is very fast. it only works for
|
||||
// value types, but all blittable types are anyway.
|
||||
//
|
||||
// this way, we can still support blittable reads on android.
|
||||
// see also: https://github.com/vis2k/Mirror/issues/3044
|
||||
// (solution discovered by AIIO, FakeByte, mischa)
|
||||
T* valueBuffer = stackalloc T[1];
|
||||
UnsafeUtility.MemCpy(valueBuffer, ptr, size);
|
||||
value = valueBuffer[0];
|
||||
#else
|
||||
// cast buffer to a T* pointer and then read from it.
|
||||
value = *(T*)ptr;
|
||||
#endif
|
||||
}
|
||||
Position += size;
|
||||
return value;
|
||||
}
|
||||
|
||||
// blittable'?' template for code reuse
|
||||
// note: bool isn't blittable. need to read as byte.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal T? ReadBlittableNullable<T>()
|
||||
where T : unmanaged =>
|
||||
ReadByte() != 0 ? ReadBlittable<T>() : default(T?);
|
||||
|
||||
public byte ReadByte() => ReadBlittable<byte>();
|
||||
|
||||
/// <summary>Read 'count' bytes into the bytes array</summary>
|
||||
// NOTE: returns byte[] because all reader functions return something.
|
||||
public byte[] ReadBytes(byte[] bytes, int count)
|
||||
{
|
||||
// user may call ReadBytes(ReadInt()). ensure positive count.
|
||||
if (count < 0) throw new ArgumentOutOfRangeException("ReadBytes requires count >= 0");
|
||||
|
||||
// check if passed byte array is big enough
|
||||
if (count > bytes.Length)
|
||||
{
|
||||
throw new EndOfStreamException($"ReadBytes can't read {count} + bytes because the passed byte[] only has length {bytes.Length}");
|
||||
}
|
||||
// ensure remaining
|
||||
if (Remaining < count)
|
||||
{
|
||||
throw new EndOfStreamException($"ReadBytesSegment can't read {count} bytes because it would read past the end of the stream. {ToString()}");
|
||||
}
|
||||
|
||||
Array.Copy(buffer.Array, buffer.Offset + Position, bytes, 0, count);
|
||||
Position += count;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>Read 'count' bytes allocation-free as ArraySegment that points to the internal array.</summary>
|
||||
public ArraySegment<byte> ReadBytesSegment(int count)
|
||||
{
|
||||
// user may call ReadBytes(ReadInt()). ensure positive count.
|
||||
if (count < 0) throw new ArgumentOutOfRangeException("ReadBytesSegment requires count >= 0");
|
||||
|
||||
// ensure remaining
|
||||
if (Remaining < count)
|
||||
{
|
||||
throw new EndOfStreamException($"ReadBytesSegment can't read {count} bytes because it would read past the end of the stream. {ToString()}");
|
||||
}
|
||||
|
||||
// return the segment
|
||||
ArraySegment<byte> result = new ArraySegment<byte>(buffer.Array, buffer.Offset + Position, count);
|
||||
Position += count;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Reads any data type that mirror supports. Uses weaver populated Reader(T).read</summary>
|
||||
public T Read<T>()
|
||||
{
|
||||
Func<NetworkReader, T> readerDelegate = Reader<T>.read;
|
||||
if (readerDelegate == null)
|
||||
{
|
||||
Debug.LogError($"No reader found for {typeof(T)}. Use a type supported by Mirror or define a custom reader extension for {typeof(T)}.");
|
||||
return default;
|
||||
}
|
||||
return readerDelegate(this);
|
||||
}
|
||||
|
||||
// print the full buffer with position / capacity.
|
||||
public override string ToString() =>
|
||||
$"[{buffer.ToHexString()} @ {Position}/{Capacity}]";
|
||||
}
|
||||
|
||||
/// <summary>Helper class that weaver populates with all reader types.</summary>
|
||||
// Note that c# creates a different static variable for each type
|
||||
// -> Weaver.ReaderWriterProcessor.InitializeReaderAndWriters() populates it
|
||||
public static class Reader<T>
|
||||
{
|
||||
public static Func<NetworkReader, T> read;
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkReader.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkReader.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1610f05ec5bd14d6882e689f7372596a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
388
Assets/Mirror/Core/NetworkReaderExtensions.cs
Normal file
388
Assets/Mirror/Core/NetworkReaderExtensions.cs
Normal file
@ -0,0 +1,388 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// Mirror's Weaver automatically detects all NetworkReader function types,
|
||||
// but they do all need to be extensions.
|
||||
public static class NetworkReaderExtensions
|
||||
{
|
||||
public static byte ReadByte(this NetworkReader reader) => reader.ReadBlittable<byte>();
|
||||
public static byte? ReadByteNullable(this NetworkReader reader) => reader.ReadBlittableNullable<byte>();
|
||||
|
||||
public static sbyte ReadSByte(this NetworkReader reader) => reader.ReadBlittable<sbyte>();
|
||||
public static sbyte? ReadSByteNullable(this NetworkReader reader) => reader.ReadBlittableNullable<sbyte>();
|
||||
|
||||
// bool is not blittable. read as ushort.
|
||||
public static char ReadChar(this NetworkReader reader) => (char)reader.ReadBlittable<ushort>();
|
||||
public static char? ReadCharNullable(this NetworkReader reader) => (char?)reader.ReadBlittableNullable<ushort>();
|
||||
|
||||
// bool is not blittable. read as byte.
|
||||
public static bool ReadBool(this NetworkReader reader) => reader.ReadBlittable<byte>() != 0;
|
||||
public static bool? ReadBoolNullable(this NetworkReader reader)
|
||||
{
|
||||
byte? value = reader.ReadBlittableNullable<byte>();
|
||||
return value.HasValue ? (value.Value != 0) : default(bool?);
|
||||
}
|
||||
|
||||
public static short ReadShort(this NetworkReader reader) => (short)reader.ReadUShort();
|
||||
public static short? ReadShortNullable(this NetworkReader reader) => reader.ReadBlittableNullable<short>();
|
||||
|
||||
public static ushort ReadUShort(this NetworkReader reader) => reader.ReadBlittable<ushort>();
|
||||
public static ushort? ReadUShortNullable(this NetworkReader reader) => reader.ReadBlittableNullable<ushort>();
|
||||
|
||||
public static int ReadInt(this NetworkReader reader) => reader.ReadBlittable<int>();
|
||||
public static int? ReadIntNullable(this NetworkReader reader) => reader.ReadBlittableNullable<int>();
|
||||
|
||||
public static uint ReadUInt(this NetworkReader reader) => reader.ReadBlittable<uint>();
|
||||
public static uint? ReadUIntNullable(this NetworkReader reader) => reader.ReadBlittableNullable<uint>();
|
||||
|
||||
public static long ReadLong(this NetworkReader reader) => reader.ReadBlittable<long>();
|
||||
public static long? ReadLongNullable(this NetworkReader reader) => reader.ReadBlittableNullable<long>();
|
||||
|
||||
public static ulong ReadULong(this NetworkReader reader) => reader.ReadBlittable<ulong>();
|
||||
public static ulong? ReadULongNullable(this NetworkReader reader) => reader.ReadBlittableNullable<ulong>();
|
||||
|
||||
public static float ReadFloat(this NetworkReader reader) => reader.ReadBlittable<float>();
|
||||
public static float? ReadFloatNullable(this NetworkReader reader) => reader.ReadBlittableNullable<float>();
|
||||
|
||||
public static double ReadDouble(this NetworkReader reader) => reader.ReadBlittable<double>();
|
||||
public static double? ReadDoubleNullable(this NetworkReader reader) => reader.ReadBlittableNullable<double>();
|
||||
|
||||
public static decimal ReadDecimal(this NetworkReader reader) => reader.ReadBlittable<decimal>();
|
||||
public static decimal? ReadDecimalNullable(this NetworkReader reader) => reader.ReadBlittableNullable<decimal>();
|
||||
|
||||
/// <exception cref="T:System.ArgumentException">if an invalid utf8 string is sent</exception>
|
||||
public static string ReadString(this NetworkReader reader)
|
||||
{
|
||||
// read number of bytes
|
||||
ushort size = reader.ReadUShort();
|
||||
|
||||
// null support, see NetworkWriter
|
||||
if (size == 0)
|
||||
return null;
|
||||
|
||||
ushort realSize = (ushort)(size - 1);
|
||||
|
||||
// make sure it's within limits to avoid allocation attacks etc.
|
||||
if (realSize > NetworkWriter.MaxStringLength)
|
||||
throw new EndOfStreamException($"NetworkReader.ReadString - Value too long: {realSize} bytes. Limit is: {NetworkWriter.MaxStringLength} bytes");
|
||||
|
||||
ArraySegment<byte> data = reader.ReadBytesSegment(realSize);
|
||||
|
||||
// convert directly from buffer to string via encoding
|
||||
// throws in case of invalid utf8.
|
||||
// see test: ReadString_InvalidUTF8()
|
||||
return reader.encoding.GetString(data.Array, data.Offset, data.Count);
|
||||
}
|
||||
|
||||
public static byte[] ReadBytes(this NetworkReader reader, int count)
|
||||
{
|
||||
// prevent allocation attacks with a reasonable limit.
|
||||
// server shouldn't allocate too much on client devices.
|
||||
// client shouldn't allocate too much on server in ClientToServer [SyncVar]s.
|
||||
if (count > NetworkReader.AllocationLimit)
|
||||
{
|
||||
// throw EndOfStream for consistency with ReadBlittable when out of data
|
||||
throw new EndOfStreamException($"NetworkReader attempted to allocate {count} bytes, which is larger than the allowed limit of {NetworkReader.AllocationLimit} bytes.");
|
||||
}
|
||||
|
||||
byte[] bytes = new byte[count];
|
||||
reader.ReadBytes(bytes, count);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <exception cref="T:OverflowException">if count is invalid</exception>
|
||||
public static byte[] ReadBytesAndSize(this NetworkReader reader)
|
||||
{
|
||||
// count = 0 means the array was null
|
||||
// otherwise count -1 is the length of the array
|
||||
uint count = reader.ReadUInt();
|
||||
// Use checked() to force it to throw OverflowException if data is invalid
|
||||
return count == 0 ? null : reader.ReadBytes(checked((int)(count - 1u)));
|
||||
}
|
||||
// Reads ArraySegment and size header
|
||||
/// <exception cref="T:OverflowException">if count is invalid</exception>
|
||||
public static ArraySegment<byte> ReadArraySegmentAndSize(this NetworkReader reader)
|
||||
{
|
||||
// count = 0 means the array was null
|
||||
// otherwise count - 1 is the length of the array
|
||||
uint count = reader.ReadUInt();
|
||||
// Use checked() to force it to throw OverflowException if data is invalid
|
||||
return count == 0 ? default : reader.ReadBytesSegment(checked((int)(count - 1u)));
|
||||
}
|
||||
|
||||
public static Vector2 ReadVector2(this NetworkReader reader) => reader.ReadBlittable<Vector2>();
|
||||
public static Vector2? ReadVector2Nullable(this NetworkReader reader) => reader.ReadBlittableNullable<Vector2>();
|
||||
|
||||
public static Vector3 ReadVector3(this NetworkReader reader) => reader.ReadBlittable<Vector3>();
|
||||
public static Vector3? ReadVector3Nullable(this NetworkReader reader) => reader.ReadBlittableNullable<Vector3>();
|
||||
|
||||
public static Vector4 ReadVector4(this NetworkReader reader) => reader.ReadBlittable<Vector4>();
|
||||
public static Vector4? ReadVector4Nullable(this NetworkReader reader) => reader.ReadBlittableNullable<Vector4>();
|
||||
|
||||
public static Vector2Int ReadVector2Int(this NetworkReader reader) => reader.ReadBlittable<Vector2Int>();
|
||||
public static Vector2Int? ReadVector2IntNullable(this NetworkReader reader) => reader.ReadBlittableNullable<Vector2Int>();
|
||||
|
||||
public static Vector3Int ReadVector3Int(this NetworkReader reader) => reader.ReadBlittable<Vector3Int>();
|
||||
public static Vector3Int? ReadVector3IntNullable(this NetworkReader reader) => reader.ReadBlittableNullable<Vector3Int>();
|
||||
|
||||
public static Color ReadColor(this NetworkReader reader) => reader.ReadBlittable<Color>();
|
||||
public static Color? ReadColorNullable(this NetworkReader reader) => reader.ReadBlittableNullable<Color>();
|
||||
|
||||
public static Color32 ReadColor32(this NetworkReader reader) => reader.ReadBlittable<Color32>();
|
||||
public static Color32? ReadColor32Nullable(this NetworkReader reader) => reader.ReadBlittableNullable<Color32>();
|
||||
|
||||
public static Quaternion ReadQuaternion(this NetworkReader reader) => reader.ReadBlittable<Quaternion>();
|
||||
public static Quaternion? ReadQuaternionNullable(this NetworkReader reader) => reader.ReadBlittableNullable<Quaternion>();
|
||||
|
||||
// Rect is a struct with properties instead of fields
|
||||
public static Rect ReadRect(this NetworkReader reader) => new Rect(reader.ReadVector2(), reader.ReadVector2());
|
||||
public static Rect? ReadRectNullable(this NetworkReader reader) => reader.ReadBool() ? ReadRect(reader) : default(Rect?);
|
||||
|
||||
// Plane is a struct with properties instead of fields
|
||||
public static Plane ReadPlane(this NetworkReader reader) => new Plane(reader.ReadVector3(), reader.ReadFloat());
|
||||
public static Plane? ReadPlaneNullable(this NetworkReader reader) => reader.ReadBool() ? ReadPlane(reader) : default(Plane?);
|
||||
|
||||
// Ray is a struct with properties instead of fields
|
||||
public static Ray ReadRay(this NetworkReader reader) => new Ray(reader.ReadVector3(), reader.ReadVector3());
|
||||
public static Ray? ReadRayNullable(this NetworkReader reader) => reader.ReadBool() ? ReadRay(reader) : default(Ray?);
|
||||
|
||||
// LayerMask is a struct with properties instead of fields
|
||||
public static LayerMask ReadLayerMask(this NetworkReader reader)
|
||||
{
|
||||
// LayerMask doesn't have a constructor that takes an initial value.
|
||||
// 32 layers as a flags enum, max value of 496, we only need a UShort.
|
||||
LayerMask layerMask = default;
|
||||
layerMask.value = reader.ReadUShort();
|
||||
return layerMask;
|
||||
}
|
||||
|
||||
public static LayerMask? ReadLayerMaskNullable(this NetworkReader reader) => reader.ReadBool() ? ReadLayerMask(reader) : default(LayerMask?);
|
||||
|
||||
public static Matrix4x4 ReadMatrix4x4(this NetworkReader reader) => reader.ReadBlittable<Matrix4x4>();
|
||||
public static Matrix4x4? ReadMatrix4x4Nullable(this NetworkReader reader) => reader.ReadBlittableNullable<Matrix4x4>();
|
||||
|
||||
public static Guid ReadGuid(this NetworkReader reader)
|
||||
{
|
||||
#if !UNITY_2021_3_OR_NEWER
|
||||
// Unity 2019 doesn't have Span yet
|
||||
return new Guid(reader.ReadBytes(16));
|
||||
#else
|
||||
// ReadBlittable(Guid) isn't safe. see ReadBlittable comments.
|
||||
// Guid is Sequential, but we can't guarantee packing.
|
||||
if (reader.Remaining >= 16)
|
||||
{
|
||||
ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(reader.buffer.Array, reader.buffer.Offset + reader.Position, 16);
|
||||
reader.Position += 16;
|
||||
return new Guid(span);
|
||||
}
|
||||
throw new EndOfStreamException($"ReadGuid out of range: {reader}");
|
||||
#endif
|
||||
}
|
||||
public static Guid? ReadGuidNullable(this NetworkReader reader) => reader.ReadBool() ? ReadGuid(reader) : default(Guid?);
|
||||
|
||||
public static NetworkIdentity ReadNetworkIdentity(this NetworkReader reader)
|
||||
{
|
||||
uint netId = reader.ReadUInt();
|
||||
if (netId == 0)
|
||||
return null;
|
||||
|
||||
// NOTE: a netId not being in spawned is common.
|
||||
// for example, "[SyncVar] NetworkIdentity target" netId would not
|
||||
// be known on client if the monster walks out of proximity for a
|
||||
// moment. no need to log any error or warning here.
|
||||
return Utils.GetSpawnedInServerOrClient(netId);
|
||||
}
|
||||
|
||||
public static NetworkBehaviour ReadNetworkBehaviour(this NetworkReader reader)
|
||||
{
|
||||
// read netId first.
|
||||
//
|
||||
// IMPORTANT: if netId != 0, writer always writes componentIndex.
|
||||
// reusing ReadNetworkIdentity() might return a null NetworkIdentity
|
||||
// even if netId was != 0 but the identity disappeared on the client,
|
||||
// resulting in unequal amounts of data being written / read.
|
||||
// https://github.com/vis2k/Mirror/issues/2972
|
||||
uint netId = reader.ReadUInt();
|
||||
if (netId == 0)
|
||||
return null;
|
||||
|
||||
// read component index in any case, BEFORE searching the spawned
|
||||
// NetworkIdentity by netId.
|
||||
byte componentIndex = reader.ReadByte();
|
||||
|
||||
// NOTE: a netId not being in spawned is common.
|
||||
// for example, "[SyncVar] NetworkIdentity target" netId would not
|
||||
// be known on client if the monster walks out of proximity for a
|
||||
// moment. no need to log any error or warning here.
|
||||
NetworkIdentity identity = Utils.GetSpawnedInServerOrClient(netId);
|
||||
|
||||
return identity != null
|
||||
? identity.NetworkBehaviours[componentIndex]
|
||||
: null;
|
||||
}
|
||||
|
||||
public static T ReadNetworkBehaviour<T>(this NetworkReader reader) where T : NetworkBehaviour
|
||||
{
|
||||
return reader.ReadNetworkBehaviour() as T;
|
||||
}
|
||||
|
||||
public static NetworkBehaviourSyncVar ReadNetworkBehaviourSyncVar(this NetworkReader reader)
|
||||
{
|
||||
uint netId = reader.ReadUInt();
|
||||
byte componentIndex = default;
|
||||
|
||||
// if netId is not 0, then index is also sent to read before returning
|
||||
if (netId != 0)
|
||||
{
|
||||
componentIndex = reader.ReadByte();
|
||||
}
|
||||
|
||||
return new NetworkBehaviourSyncVar(netId, componentIndex);
|
||||
}
|
||||
|
||||
public static Transform ReadTransform(this NetworkReader reader)
|
||||
{
|
||||
// Don't use null propagation here as it could lead to MissingReferenceException
|
||||
NetworkIdentity networkIdentity = reader.ReadNetworkIdentity();
|
||||
return networkIdentity != null ? networkIdentity.transform : null;
|
||||
}
|
||||
|
||||
public static GameObject ReadGameObject(this NetworkReader reader)
|
||||
{
|
||||
// Don't use null propagation here as it could lead to MissingReferenceException
|
||||
NetworkIdentity networkIdentity = reader.ReadNetworkIdentity();
|
||||
return networkIdentity != null ? networkIdentity.gameObject : null;
|
||||
}
|
||||
|
||||
// while SyncList<T> is recommended for NetworkBehaviours,
|
||||
// structs may have .List<T> members which weaver needs to be able to
|
||||
// fully serialize for NetworkMessages etc.
|
||||
// note that Weaver/Readers/GenerateReader() handles this manually.
|
||||
public static List<T> ReadList<T>(this NetworkReader reader)
|
||||
{
|
||||
int length = reader.ReadInt();
|
||||
|
||||
// 'null' is encoded as '-1'
|
||||
if (length < 0) return null;
|
||||
|
||||
// prevent allocation attacks with a reasonable limit.
|
||||
// server shouldn't allocate too much on client devices.
|
||||
// client shouldn't allocate too much on server in ClientToServer [SyncVar]s.
|
||||
if (length > NetworkReader.AllocationLimit)
|
||||
{
|
||||
// throw EndOfStream for consistency with ReadBlittable when out of data
|
||||
throw new EndOfStreamException($"NetworkReader attempted to allocate a List<{typeof(T)}> {length} elements, which is larger than the allowed limit of {NetworkReader.AllocationLimit}.");
|
||||
}
|
||||
|
||||
List<T> result = new List<T>(length);
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
result.Add(reader.Read<T>());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// while SyncSet<T> is recommended for NetworkBehaviours,
|
||||
// structs may have .Set<T> members which weaver needs to be able to
|
||||
// fully serialize for NetworkMessages etc.
|
||||
// note that Weaver/Readers/GenerateReader() handles this manually.
|
||||
// TODO writer not found. need to adjust weaver first. see tests.
|
||||
/*
|
||||
public static HashSet<T> ReadHashSet<T>(this NetworkReader reader)
|
||||
{
|
||||
int length = reader.ReadInt();
|
||||
if (length < 0)
|
||||
return null;
|
||||
HashSet<T> result = new HashSet<T>();
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
result.Add(reader.Read<T>());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
*/
|
||||
|
||||
public static T[] ReadArray<T>(this NetworkReader reader)
|
||||
{
|
||||
int length = reader.ReadInt();
|
||||
|
||||
// 'null' is encoded as '-1'
|
||||
if (length < 0) return null;
|
||||
|
||||
// prevent allocation attacks with a reasonable limit.
|
||||
// server shouldn't allocate too much on client devices.
|
||||
// client shouldn't allocate too much on server in ClientToServer [SyncVar]s.
|
||||
if (length > NetworkReader.AllocationLimit)
|
||||
{
|
||||
// throw EndOfStream for consistency with ReadBlittable when out of data
|
||||
throw new EndOfStreamException($"NetworkReader attempted to allocate an Array<{typeof(T)}> with {length} elements, which is larger than the allowed limit of {NetworkReader.AllocationLimit}.");
|
||||
}
|
||||
|
||||
// we can't check if reader.Remaining < length,
|
||||
// because we don't know sizeof(T) since it's a managed type.
|
||||
// if (length > reader.Remaining) throw new EndOfStreamException($"Received array that is too large: {length}");
|
||||
|
||||
T[] result = new T[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
result[i] = reader.Read<T>();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Uri ReadUri(this NetworkReader reader)
|
||||
{
|
||||
string uriString = reader.ReadString();
|
||||
return (string.IsNullOrWhiteSpace(uriString) ? null : new Uri(uriString));
|
||||
}
|
||||
|
||||
public static Texture2D ReadTexture2D(this NetworkReader reader)
|
||||
{
|
||||
// support 'null' textures for [SyncVar]s etc.
|
||||
// https://github.com/vis2k/Mirror/issues/3144
|
||||
short width = reader.ReadShort();
|
||||
if (width == -1) return null;
|
||||
|
||||
// read height
|
||||
short height = reader.ReadShort();
|
||||
|
||||
// prevent allocation attacks with a reasonable limit.
|
||||
// server shouldn't allocate too much on client devices.
|
||||
// client shouldn't allocate too much on server in ClientToServer [SyncVar]s.
|
||||
// log an error and return default.
|
||||
// we don't want attackers to be able to trigger exceptions.
|
||||
int totalSize = width * height;
|
||||
if (totalSize > NetworkReader.AllocationLimit)
|
||||
{
|
||||
Debug.LogWarning($"NetworkReader attempted to allocate a Texture2D with total size (width * height) of {totalSize}, which is larger than the allowed limit of {NetworkReader.AllocationLimit}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
Texture2D texture2D = new Texture2D(width, height);
|
||||
|
||||
// read pixel content
|
||||
Color32[] pixels = reader.ReadArray<Color32>();
|
||||
texture2D.SetPixels32(pixels);
|
||||
texture2D.Apply();
|
||||
return texture2D;
|
||||
}
|
||||
|
||||
public static Sprite ReadSprite(this NetworkReader reader)
|
||||
{
|
||||
// support 'null' textures for [SyncVar]s etc.
|
||||
// https://github.com/vis2k/Mirror/issues/3144
|
||||
Texture2D texture = reader.ReadTexture2D();
|
||||
if (texture == null) return null;
|
||||
|
||||
// otherwise create a valid sprite
|
||||
return Sprite.Create(texture, reader.ReadRect(), reader.ReadVector2());
|
||||
}
|
||||
|
||||
public static DateTime ReadDateTime(this NetworkReader reader) => DateTime.FromOADate(reader.ReadDouble());
|
||||
public static DateTime? ReadDateTimeNullable(this NetworkReader reader) => reader.ReadBool() ? ReadDateTime(reader) : default(DateTime?);
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkReaderExtensions.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkReaderExtensions.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 364a9f7ccd5541e19aa2ae0b81f0b3cf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
45
Assets/Mirror/Core/NetworkReaderPool.cs
Normal file
45
Assets/Mirror/Core/NetworkReaderPool.cs
Normal file
@ -0,0 +1,45 @@
|
||||
// API consistent with Microsoft's ObjectPool<T>.
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>Pool of NetworkReaders to avoid allocations.</summary>
|
||||
public static class NetworkReaderPool
|
||||
{
|
||||
// reuse Pool<T>
|
||||
// we still wrap it in NetworkReaderPool.Get/Recyle so we can reset the
|
||||
// position and array before reusing.
|
||||
static readonly Pool<NetworkReaderPooled> Pool = new Pool<NetworkReaderPooled>(
|
||||
// byte[] will be assigned in GetReader
|
||||
() => new NetworkReaderPooled(new byte[]{}),
|
||||
// initial capacity to avoid allocations in the first few frames
|
||||
1000
|
||||
);
|
||||
|
||||
/// <summary>Get the next reader in the pool. If pool is empty, creates a new Reader</summary>
|
||||
public static NetworkReaderPooled Get(byte[] bytes)
|
||||
{
|
||||
// grab from pool & set buffer
|
||||
NetworkReaderPooled reader = Pool.Get();
|
||||
reader.SetBuffer(bytes);
|
||||
return reader;
|
||||
}
|
||||
|
||||
/// <summary>Get the next reader in the pool. If pool is empty, creates a new Reader</summary>
|
||||
public static NetworkReaderPooled Get(ArraySegment<byte> segment)
|
||||
{
|
||||
// grab from pool & set buffer
|
||||
NetworkReaderPooled reader = Pool.Get();
|
||||
reader.SetBuffer(segment);
|
||||
return reader;
|
||||
}
|
||||
|
||||
/// <summary>Returns a reader to the pool.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Return(NetworkReaderPooled reader)
|
||||
{
|
||||
Pool.Return(reader);
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkReaderPool.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkReaderPool.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bacff63613ad634a98f9e4d15d29dbf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
15
Assets/Mirror/Core/NetworkReaderPooled.cs
Normal file
15
Assets/Mirror/Core/NetworkReaderPooled.cs
Normal file
@ -0,0 +1,15 @@
|
||||
// "NetworkReaderPooled" instead of "PooledNetworkReader" to group files, for
|
||||
// easier IDE workflow and more elegant code.
|
||||
using System;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>Pooled NetworkReader, automatically returned to pool when using 'using'</summary>
|
||||
// TODO make sealed again after removing obsolete NetworkReaderPooled!
|
||||
public class NetworkReaderPooled : NetworkReader, IDisposable
|
||||
{
|
||||
internal NetworkReaderPooled(byte[] bytes) : base(bytes) {}
|
||||
internal NetworkReaderPooled(ArraySegment<byte> segment) : base(segment) {}
|
||||
public void Dispose() => NetworkReaderPool.Return(this);
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkReaderPooled.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkReaderPooled.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: faafa97c32e44adf8e8888de817a370a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
1981
Assets/Mirror/Core/NetworkServer.cs
Normal file
1981
Assets/Mirror/Core/NetworkServer.cs
Normal file
File diff suppressed because it is too large
Load Diff
11
Assets/Mirror/Core/NetworkServer.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkServer.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a5f5ec068f5604c32b160bc49ee97b75
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
21
Assets/Mirror/Core/NetworkStartPosition.cs
Normal file
21
Assets/Mirror/Core/NetworkStartPosition.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>Start position for player spawning, automatically registers itself in the NetworkManager.</summary>
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Start Position")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-start-position")]
|
||||
public class NetworkStartPosition : MonoBehaviour
|
||||
{
|
||||
public void Awake()
|
||||
{
|
||||
NetworkManager.RegisterStartPosition(transform);
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
NetworkManager.UnRegisterStartPosition(transform);
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkStartPosition.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkStartPosition.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 41f84591ce72545258ea98cb7518d8b9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
243
Assets/Mirror/Core/NetworkTime.cs
Normal file
243
Assets/Mirror/Core/NetworkTime.cs
Normal file
@ -0,0 +1,243 @@
|
||||
// NetworkTime now uses NetworkClient's snapshot interpolated timeline.
|
||||
// this gives ideal results & ensures everything is on the same timeline.
|
||||
// previously, NetworkTransforms were on separate timelines.
|
||||
//
|
||||
// however, some of the old NetworkTime code remains for ping time (rtt).
|
||||
// some users may still be using that.
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
#if !UNITY_2020_3_OR_NEWER
|
||||
using Stopwatch = System.Diagnostics.Stopwatch;
|
||||
#endif
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>Synchronizes server time to clients.</summary>
|
||||
public static class NetworkTime
|
||||
{
|
||||
/// <summary>Ping message interval, used to calculate latency / RTT and predicted time.</summary>
|
||||
// 2s was enough to get a good average RTT.
|
||||
// for prediction, we want to react to latency changes more rapidly.
|
||||
const float DefaultPingInterval = 0.1f; // for resets
|
||||
public static float PingInterval = DefaultPingInterval;
|
||||
|
||||
/// <summary>Average out the last few results from Ping</summary>
|
||||
// const because it's used immediately in _rtt constructor.
|
||||
public const int PingWindowSize = 50; // average over 50 * 100ms = 5s
|
||||
|
||||
static double lastPingTime;
|
||||
|
||||
static ExponentialMovingAverage _rtt = new ExponentialMovingAverage(PingWindowSize);
|
||||
|
||||
/// <summary>Returns double precision clock time _in this system_, unaffected by the network.</summary>
|
||||
#if UNITY_2020_3_OR_NEWER
|
||||
public static double localTime
|
||||
{
|
||||
// NetworkTime uses unscaled time and ignores Time.timeScale.
|
||||
// fixes Time.timeScale getting server & client time out of sync:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3409
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => Time.unscaledTimeAsDouble;
|
||||
}
|
||||
#else
|
||||
// need stopwatch for older Unity versions, but it's quite slow.
|
||||
// CAREFUL: unlike Time.time, the stopwatch time is not a FRAME time.
|
||||
// it changes during the frame, so we have an extra step to "cache" it in EarlyUpdate.
|
||||
static readonly Stopwatch stopwatch = new Stopwatch();
|
||||
static NetworkTime() => stopwatch.Start();
|
||||
static double localFrameTime;
|
||||
public static double localTime => localFrameTime;
|
||||
#endif
|
||||
|
||||
/// <summary>The time in seconds since the server started.</summary>
|
||||
// via global NetworkClient snapshot interpolated timeline (if client).
|
||||
// on server, this is simply Time.timeAsDouble.
|
||||
//
|
||||
// I measured the accuracy of float and I got this:
|
||||
// for the same day, accuracy is better than 1 ms
|
||||
// after 1 day, accuracy goes down to 7 ms
|
||||
// after 10 days, accuracy is 61 ms
|
||||
// after 30 days , accuracy is 238 ms
|
||||
// after 60 days, accuracy is 454 ms
|
||||
// in other words, if the server is running for 2 months,
|
||||
// and you cast down to float, then the time will jump in 0.4s intervals.
|
||||
public static double time
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => NetworkServer.active
|
||||
? localTime
|
||||
: NetworkClient.localTimeline;
|
||||
}
|
||||
|
||||
// prediction //////////////////////////////////////////////////////////
|
||||
// NetworkTime.time is server time, behind by bufferTime.
|
||||
// for prediction, we want server time, ahead by latency.
|
||||
// so that client inputs at predictedTime=2 arrive on server at time=2.
|
||||
// the more accurate this is, the more closesly will corrections be
|
||||
// be applied and the less jitter we will see.
|
||||
//
|
||||
// we'll use a two step process to calculate predicted time:
|
||||
// 1. move snapshot interpolated time to server time, without being behind by bufferTime
|
||||
// 2. constantly send this time to server (included in ping message)
|
||||
// server replies with how far off it was.
|
||||
// client averages that offset and applies it to predictedTime to get ever closer.
|
||||
//
|
||||
// this is also very easy to test & verify:
|
||||
// - add LatencySimulation with 50ms latency
|
||||
// - log predictionError on server in OnServerPing, see if it gets closer to 0
|
||||
//
|
||||
// credits: FakeByte, imer, NinjaKickja, mischa
|
||||
// const because it's used immediately in _predictionError constructor.
|
||||
|
||||
static int PredictionErrorWindowSize = 20; // average over 20 * 100ms = 2s
|
||||
static ExponentialMovingAverage _predictionErrorUnadjusted = new ExponentialMovingAverage(PredictionErrorWindowSize);
|
||||
public static double predictionErrorUnadjusted => _predictionErrorUnadjusted.Value;
|
||||
public static double predictionErrorAdjusted { get; private set; } // for debugging
|
||||
|
||||
/// <summary>Predicted timeline in order for client inputs to be timestamped with the exact time when they will most likely arrive on the server. This is the basis for all prediction like PredictedRigidbody.</summary>
|
||||
// on client, this is based on localTime (aka Time.time) instead of the snapshot interpolated timeline.
|
||||
// this gives much better and immediately accurate results.
|
||||
// -> snapshot interpolation timeline tries to emulate a server timeline without hard offset corrections.
|
||||
// -> predictedTime does have hard offset corrections, so might as well use Time.time directly for this.
|
||||
//
|
||||
// note that predictedTime over unreliable is enough!
|
||||
// even with reliable components, it gives better results than if we were
|
||||
// to implemented predictedTime over reliable channel.
|
||||
public static double predictedTime
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => NetworkServer.active
|
||||
? localTime // server always uses it's own timeline
|
||||
: localTime + predictionErrorUnadjusted; // add the offset that the server told us we are off by
|
||||
}
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// <summary>Clock difference in seconds between the client and the server. Always 0 on server.</summary>
|
||||
// original implementation used 'client - server' time. keep it this way.
|
||||
// TODO obsolete later. people shouldn't worry about this.
|
||||
public static double offset => localTime - time;
|
||||
|
||||
/// <summary>Round trip time (in seconds) that it takes a message to go client->server->client.</summary>
|
||||
public static double rtt => _rtt.Value;
|
||||
|
||||
/// <Summary>Round trip time variance aka jitter, in seconds.</Summary>
|
||||
// "rttVariance" instead of "rttVar" for consistency with older versions.
|
||||
public static double rttVariance => _rtt.Variance;
|
||||
|
||||
// RuntimeInitializeOnLoadMethod -> fast playmode without domain reload
|
||||
[RuntimeInitializeOnLoadMethod]
|
||||
public static void ResetStatics()
|
||||
{
|
||||
PingInterval = DefaultPingInterval;
|
||||
lastPingTime = 0;
|
||||
_rtt = new ExponentialMovingAverage(PingWindowSize);
|
||||
#if !UNITY_2020_3_OR_NEWER
|
||||
stopwatch.Restart();
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static void UpdateClient()
|
||||
{
|
||||
// localTime (double) instead of Time.time for accuracy over days
|
||||
if (localTime >= lastPingTime + PingInterval)
|
||||
SendPing();
|
||||
}
|
||||
|
||||
// Separate method so we can call it from NetworkClient directly.
|
||||
internal static void SendPing()
|
||||
{
|
||||
// send raw predicted time without the offset applied yet.
|
||||
// we then apply the offset to it after.
|
||||
NetworkPingMessage pingMessage = new NetworkPingMessage
|
||||
(
|
||||
localTime,
|
||||
predictedTime
|
||||
);
|
||||
NetworkClient.Send(pingMessage, Channels.Unreliable);
|
||||
lastPingTime = localTime;
|
||||
}
|
||||
|
||||
// client rtt calculation //////////////////////////////////////////////
|
||||
// executed at the server when we receive a ping message
|
||||
// reply with a pong containing the time from the client
|
||||
// and time from the server
|
||||
internal static void OnServerPing(NetworkConnectionToClient conn, NetworkPingMessage message)
|
||||
{
|
||||
// calculate the prediction offset that the client needs to apply to unadjusted time to reach server time.
|
||||
// this will be sent back to client for corrections.
|
||||
double unadjustedError = localTime - message.localTime;
|
||||
|
||||
// to see how well the client's final prediction worked, compare with adjusted time.
|
||||
// this is purely for debugging.
|
||||
// >0 means: server is ... seconds ahead of client's prediction (good if small)
|
||||
// <0 means: server is ... seconds behind client's prediction.
|
||||
// in other words, client is predicting too far ahead (not good)
|
||||
double adjustedError = localTime - message.predictedTimeAdjusted;
|
||||
// Debug.Log($"[Server] unadjustedError:{(unadjustedError*1000):F1}ms adjustedError:{(adjustedError*1000):F1}ms");
|
||||
|
||||
// Debug.Log($"OnServerPing conn:{conn}");
|
||||
NetworkPongMessage pongMessage = new NetworkPongMessage
|
||||
(
|
||||
message.localTime,
|
||||
unadjustedError,
|
||||
adjustedError
|
||||
);
|
||||
conn.Send(pongMessage, Channels.Unreliable);
|
||||
}
|
||||
|
||||
// Executed at the client when we receive a Pong message
|
||||
// find out how long it took since we sent the Ping
|
||||
// and update time offset & prediction offset.
|
||||
internal static void OnClientPong(NetworkPongMessage message)
|
||||
{
|
||||
// prevent attackers from sending timestamps which are in the future
|
||||
if (message.localTime > localTime) return;
|
||||
|
||||
// how long did this message take to come back
|
||||
double newRtt = localTime - message.localTime;
|
||||
_rtt.Add(newRtt);
|
||||
|
||||
// feed unadjusted prediction error into our exponential moving average
|
||||
// store adjusted prediction error for debug / GUI purposes
|
||||
_predictionErrorUnadjusted.Add(message.predictionErrorUnadjusted);
|
||||
predictionErrorAdjusted = message.predictionErrorAdjusted;
|
||||
// Debug.Log($"[Client] predictionError avg={(_predictionErrorUnadjusted.Value*1000):F1} ms");
|
||||
}
|
||||
|
||||
// server rtt calculation //////////////////////////////////////////////
|
||||
// Executed at the client when we receive a ping message from the server.
|
||||
// in other words, this is for server sided ping + rtt calculation.
|
||||
// reply with a pong containing the time from the server
|
||||
internal static void OnClientPing(NetworkPingMessage message)
|
||||
{
|
||||
// Debug.Log($"OnClientPing conn:{conn}");
|
||||
NetworkPongMessage pongMessage = new NetworkPongMessage
|
||||
(
|
||||
message.localTime,
|
||||
0, 0 // server doesn't predict
|
||||
);
|
||||
NetworkClient.Send(pongMessage, Channels.Unreliable);
|
||||
}
|
||||
|
||||
// Executed at the server when we receive a Pong message back.
|
||||
// find out how long it took since we sent the Ping
|
||||
// and update time offset
|
||||
internal static void OnServerPong(NetworkConnectionToClient conn, NetworkPongMessage message)
|
||||
{
|
||||
// prevent attackers from sending timestamps which are in the future
|
||||
if (message.localTime > localTime) return;
|
||||
|
||||
// how long did this message take to come back
|
||||
double newRtt = localTime - message.localTime;
|
||||
conn._rtt.Add(newRtt);
|
||||
}
|
||||
|
||||
internal static void EarlyUpdate()
|
||||
{
|
||||
#if !UNITY_2020_3_OR_NEWER
|
||||
localFrameTime = stopwatch.Elapsed.TotalSeconds;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkTime.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkTime.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09a0c241fc4a5496dbf4a0ab6e9a312c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
249
Assets/Mirror/Core/NetworkWriter.cs
Normal file
249
Assets/Mirror/Core/NetworkWriter.cs
Normal file
@ -0,0 +1,249 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>Network Writer for most simple types like floats, ints, buffers, structs, etc. Use NetworkWriterPool.GetReader() to avoid allocations.</summary>
|
||||
public class NetworkWriter
|
||||
{
|
||||
// the limit of ushort is so we can write string size prefix as only 2 bytes.
|
||||
// -1 so we can still encode 'null' into it too.
|
||||
public const ushort MaxStringLength = ushort.MaxValue - 1;
|
||||
|
||||
// create writer immediately with it's own buffer so no one can mess with it and so that we can resize it.
|
||||
// note: BinaryWriter allocates too much, so we only use a MemoryStream
|
||||
// => 1500 bytes by default because on average, most packets will be <= MTU
|
||||
public const int DefaultCapacity = 1500;
|
||||
internal byte[] buffer = new byte[DefaultCapacity];
|
||||
|
||||
/// <summary>Next position to write to the buffer</summary>
|
||||
public int Position;
|
||||
|
||||
/// <summary>Current capacity. Automatically resized if necessary.</summary>
|
||||
public int Capacity => buffer.Length;
|
||||
|
||||
// cache encoding for WriteString instead of creating it each time.
|
||||
// 1000 readers before: 1MB GC, 30ms
|
||||
// 1000 readers after: 0.8MB GC, 18ms
|
||||
// not(!) static for thread safety.
|
||||
//
|
||||
// throwOnInvalidBytes is true.
|
||||
// writer should throw and user should fix if this ever happens.
|
||||
// unlike reader, which needs to expect it to happen from attackers.
|
||||
internal readonly UTF8Encoding encoding = new UTF8Encoding(false, true);
|
||||
|
||||
/// <summary>Reset both the position and length of the stream</summary>
|
||||
// Leaves the capacity the same so that we can reuse this writer without
|
||||
// extra allocations
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
Position = 0;
|
||||
}
|
||||
|
||||
// NOTE that our runtime resizing comes at no extra cost because:
|
||||
// 1. 'has space' checks are necessary even for fixed sized writers.
|
||||
// 2. all writers will eventually be large enough to stop resizing.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal void EnsureCapacity(int value)
|
||||
{
|
||||
if (buffer.Length < value)
|
||||
{
|
||||
int capacity = Math.Max(value, buffer.Length * 2);
|
||||
Array.Resize(ref buffer, capacity);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Copies buffer until 'Position' to a new array.</summary>
|
||||
// Try to use ToArraySegment instead to avoid allocations!
|
||||
public byte[] ToArray()
|
||||
{
|
||||
byte[] data = new byte[Position];
|
||||
Array.ConstrainedCopy(buffer, 0, data, 0, Position);
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>Returns allocation-free ArraySegment until 'Position'.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ArraySegment<byte> ToArraySegment() =>
|
||||
new ArraySegment<byte>(buffer, 0, Position);
|
||||
|
||||
// implicit conversion for convenience
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator ArraySegment<byte>(NetworkWriter w) =>
|
||||
w.ToArraySegment();
|
||||
|
||||
// WriteBlittable<T> from DOTSNET.
|
||||
// this is extremely fast, but only works for blittable types.
|
||||
//
|
||||
// Benchmark:
|
||||
// WriteQuaternion x 100k, Macbook Pro 2015 @ 2.2Ghz, Unity 2018 LTS (debug mode)
|
||||
//
|
||||
// | Median | Min | Max | Avg | Std | (ms)
|
||||
// before | 30.35 | 29.86 | 48.99 | 32.54 | 4.93 |
|
||||
// blittable* | 5.69 | 5.52 | 27.51 | 7.78 | 5.65 |
|
||||
//
|
||||
// * without IsBlittable check
|
||||
// => 4-6x faster!
|
||||
//
|
||||
// WriteQuaternion x 100k, Macbook Pro 2015 @ 2.2Ghz, Unity 2020.1 (release mode)
|
||||
//
|
||||
// | Median | Min | Max | Avg | Std | (ms)
|
||||
// before | 9.41 | 8.90 | 23.02 | 10.72 | 3.07 |
|
||||
// blittable* | 1.48 | 1.40 | 16.03 | 2.60 | 2.71 |
|
||||
//
|
||||
// * without IsBlittable check
|
||||
// => 6x faster!
|
||||
//
|
||||
// Note:
|
||||
// WriteBlittable assumes same endianness for server & client.
|
||||
// All Unity 2018+ platforms are little endian.
|
||||
// => run NetworkWriterTests.BlittableOnThisPlatform() to verify!
|
||||
//
|
||||
// This is not safe to expose to random structs.
|
||||
// * StructLayout.Sequential is the default, which is safe.
|
||||
// if the struct contains a reference type, it is converted to Auto.
|
||||
// but since all structs here are unmanaged blittable, it's safe.
|
||||
// see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.layoutkind?view=netframework-4.8#system-runtime-interopservices-layoutkind-sequential
|
||||
// * StructLayout.Pack depends on CPU word size.
|
||||
// this may be different 4 or 8 on some ARM systems, etc.
|
||||
// this is not safe, and would cause bytes/shorts etc. to be padded.
|
||||
// see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.structlayoutattribute.pack?view=net-6.0
|
||||
// * If we force pack all to '1', they would have no padding which is
|
||||
// great for bandwidth. but on some android systems, CPU can't read
|
||||
// unaligned memory.
|
||||
// see also: https://github.com/vis2k/Mirror/issues/3044
|
||||
// * The only option would be to force explicit layout with multiples
|
||||
// of word size. but this requires lots of weaver checking and is
|
||||
// still questionable (IL2CPP etc.).
|
||||
//
|
||||
// Note: inlining WriteBlittable is enough. don't inline WriteInt etc.
|
||||
// we don't want WriteBlittable to be copied in place everywhere.
|
||||
internal unsafe void WriteBlittable<T>(T value)
|
||||
where T : unmanaged
|
||||
{
|
||||
// check if blittable for safety
|
||||
#if UNITY_EDITOR
|
||||
if (!UnsafeUtility.IsBlittable(typeof(T)))
|
||||
{
|
||||
Debug.LogError($"{typeof(T)} is not blittable!");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// calculate size
|
||||
// sizeof(T) gets the managed size at compile time.
|
||||
// Marshal.SizeOf<T> gets the unmanaged size at runtime (slow).
|
||||
// => our 1mio writes benchmark is 6x slower with Marshal.SizeOf<T>
|
||||
// => for blittable types, sizeof(T) is even recommended:
|
||||
// https://docs.microsoft.com/en-us/dotnet/standard/native-interop/best-practices
|
||||
int size = sizeof(T);
|
||||
|
||||
// ensure capacity
|
||||
// NOTE that our runtime resizing comes at no extra cost because:
|
||||
// 1. 'has space' checks are necessary even for fixed sized writers.
|
||||
// 2. all writers will eventually be large enough to stop resizing.
|
||||
EnsureCapacity(Position + size);
|
||||
|
||||
// write blittable
|
||||
fixed (byte* ptr = &buffer[Position])
|
||||
{
|
||||
#if UNITY_ANDROID
|
||||
// on some android systems, assigning *(T*)ptr throws a NRE if
|
||||
// the ptr isn't aligned (i.e. if Position is 1,2,3,5, etc.).
|
||||
// here we have to use memcpy.
|
||||
//
|
||||
// => we can't get a pointer of a struct in C# without
|
||||
// marshalling allocations
|
||||
// => instead, we stack allocate an array of type T and use that
|
||||
// => stackalloc avoids GC and is very fast. it only works for
|
||||
// value types, but all blittable types are anyway.
|
||||
//
|
||||
// this way, we can still support blittable reads on android.
|
||||
// see also: https://github.com/vis2k/Mirror/issues/3044
|
||||
// (solution discovered by AIIO, FakeByte, mischa)
|
||||
T* valueBuffer = stackalloc T[1]{value};
|
||||
UnsafeUtility.MemCpy(ptr, valueBuffer, size);
|
||||
#else
|
||||
// cast buffer to T* pointer, then assign value to the area
|
||||
*(T*)ptr = value;
|
||||
#endif
|
||||
}
|
||||
Position += size;
|
||||
}
|
||||
|
||||
// blittable'?' template for code reuse
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal void WriteBlittableNullable<T>(T? value)
|
||||
where T : unmanaged
|
||||
{
|
||||
// bool isn't blittable. write as byte.
|
||||
WriteByte((byte)(value.HasValue ? 0x01 : 0x00));
|
||||
|
||||
// only write value if exists. saves bandwidth.
|
||||
if (value.HasValue)
|
||||
WriteBlittable(value.Value);
|
||||
}
|
||||
|
||||
public void WriteByte(byte value) => WriteBlittable(value);
|
||||
|
||||
// for byte arrays with consistent size, where the reader knows how many to read
|
||||
// (like a packet opcode that's always the same)
|
||||
public void WriteBytes(byte[] array, int offset, int count)
|
||||
{
|
||||
EnsureCapacity(Position + count);
|
||||
Array.ConstrainedCopy(array, offset, this.buffer, Position, count);
|
||||
Position += count;
|
||||
}
|
||||
// write an unsafe byte* array.
|
||||
// useful for bit tree compression, etc.
|
||||
public unsafe bool WriteBytes(byte* ptr, int offset, int size)
|
||||
{
|
||||
EnsureCapacity(Position + size);
|
||||
|
||||
fixed (byte* destination = &buffer[Position])
|
||||
{
|
||||
// write 'size' bytes at position
|
||||
// 10 mio writes: 868ms
|
||||
// Array.Copy(value.Array, value.Offset, buffer, Position, value.Count);
|
||||
// 10 mio writes: 775ms
|
||||
// Buffer.BlockCopy(value.Array, value.Offset, buffer, Position, value.Count);
|
||||
// 10 mio writes: 637ms
|
||||
UnsafeUtility.MemCpy(destination, ptr + offset, size);
|
||||
}
|
||||
|
||||
Position += size;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Writes any type that mirror supports. Uses weaver populated Writer(T).write.</summary>
|
||||
public void Write<T>(T value)
|
||||
{
|
||||
Action<NetworkWriter, T> writeDelegate = Writer<T>.write;
|
||||
if (writeDelegate == null)
|
||||
{
|
||||
Debug.LogError($"No writer found for {typeof(T)}. This happens either if you are missing a NetworkWriter extension for your custom type, or if weaving failed. Try to reimport a script to weave again.");
|
||||
}
|
||||
else
|
||||
{
|
||||
writeDelegate(this, value);
|
||||
}
|
||||
}
|
||||
|
||||
// print with buffer content for easier debugging.
|
||||
// [content, position / capacity].
|
||||
// showing "position / space" would be too confusing.
|
||||
public override string ToString() =>
|
||||
$"[{ToArraySegment().ToHexString()} @ {Position}/{Capacity}]";
|
||||
}
|
||||
|
||||
/// <summary>Helper class that weaver populates with all writer types.</summary>
|
||||
// Note that c# creates a different static variable for each type
|
||||
// -> Weaver.ReaderWriterProcessor.InitializeReaderAndWriters() populates it
|
||||
public static class Writer<T>
|
||||
{
|
||||
public static Action<NetworkWriter, T> write;
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkWriter.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkWriter.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 48d2207bcef1f4477b624725f075f9bd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
430
Assets/Mirror/Core/NetworkWriterExtensions.cs
Normal file
430
Assets/Mirror/Core/NetworkWriterExtensions.cs
Normal file
@ -0,0 +1,430 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// Mirror's Weaver automatically detects all NetworkWriter function types,
|
||||
// but they do all need to be extensions.
|
||||
public static class NetworkWriterExtensions
|
||||
{
|
||||
public static void WriteByte(this NetworkWriter writer, byte value) => writer.WriteBlittable(value);
|
||||
public static void WriteByteNullable(this NetworkWriter writer, byte? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteSByte(this NetworkWriter writer, sbyte value) => writer.WriteBlittable(value);
|
||||
public static void WriteSByteNullable(this NetworkWriter writer, sbyte? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
// char is not blittable. convert to ushort.
|
||||
public static void WriteChar(this NetworkWriter writer, char value) => writer.WriteBlittable((ushort)value);
|
||||
public static void WriteCharNullable(this NetworkWriter writer, char? value) => writer.WriteBlittableNullable((ushort?)value);
|
||||
|
||||
// bool is not blittable. convert to byte.
|
||||
public static void WriteBool(this NetworkWriter writer, bool value) => writer.WriteBlittable((byte)(value ? 1 : 0));
|
||||
public static void WriteBoolNullable(this NetworkWriter writer, bool? value) => writer.WriteBlittableNullable(value.HasValue ? ((byte)(value.Value ? 1 : 0)) : new byte?());
|
||||
|
||||
public static void WriteShort(this NetworkWriter writer, short value) => writer.WriteBlittable(value);
|
||||
public static void WriteShortNullable(this NetworkWriter writer, short? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteUShort(this NetworkWriter writer, ushort value) => writer.WriteBlittable(value);
|
||||
public static void WriteUShortNullable(this NetworkWriter writer, ushort? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteInt(this NetworkWriter writer, int value) => writer.WriteBlittable(value);
|
||||
public static void WriteIntNullable(this NetworkWriter writer, int? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteUInt(this NetworkWriter writer, uint value) => writer.WriteBlittable(value);
|
||||
public static void WriteUIntNullable(this NetworkWriter writer, uint? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteLong(this NetworkWriter writer, long value) => writer.WriteBlittable(value);
|
||||
public static void WriteLongNullable(this NetworkWriter writer, long? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteULong(this NetworkWriter writer, ulong value) => writer.WriteBlittable(value);
|
||||
public static void WriteULongNullable(this NetworkWriter writer, ulong? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteFloat(this NetworkWriter writer, float value) => writer.WriteBlittable(value);
|
||||
public static void WriteFloatNullable(this NetworkWriter writer, float? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteDouble(this NetworkWriter writer, double value) => writer.WriteBlittable(value);
|
||||
public static void WriteDoubleNullable(this NetworkWriter writer, double? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteDecimal(this NetworkWriter writer, decimal value) => writer.WriteBlittable(value);
|
||||
public static void WriteDecimalNullable(this NetworkWriter writer, decimal? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteString(this NetworkWriter writer, string value)
|
||||
{
|
||||
// write 0 for null support, increment real size by 1
|
||||
// (note: original HLAPI would write "" for null strings, but if a
|
||||
// string is null on the server then it should also be null
|
||||
// on the client)
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteUShort(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// WriteString copies into the buffer manually.
|
||||
// need to ensure capacity here first, manually.
|
||||
int maxSize = writer.encoding.GetMaxByteCount(value.Length);
|
||||
writer.EnsureCapacity(writer.Position + 2 + maxSize); // 2 bytes position + N bytes encoding
|
||||
|
||||
// encode it into the buffer first.
|
||||
// reserve 2 bytes for header after we know how much was written.
|
||||
int written = writer.encoding.GetBytes(value, 0, value.Length, writer.buffer, writer.Position + 2);
|
||||
|
||||
// check if within max size, otherwise Reader can't read it.
|
||||
if (written > NetworkWriter.MaxStringLength)
|
||||
throw new IndexOutOfRangeException($"NetworkWriter.WriteString - Value too long: {written} bytes. Limit: {NetworkWriter.MaxStringLength} bytes");
|
||||
|
||||
// .Position is unchanged, so fill in the size header now.
|
||||
// we already ensured that max size fits into ushort.max-1.
|
||||
writer.WriteUShort(checked((ushort)(written + 1))); // Position += 2
|
||||
|
||||
// now update position by what was written above
|
||||
writer.Position += written;
|
||||
}
|
||||
|
||||
// Weaver needs a write function with just one byte[] parameter
|
||||
// (we don't name it .Write(byte[]) because it's really a WriteBytesAndSize since we write size / null info too)
|
||||
public static void WriteBytesAndSize(this NetworkWriter writer, byte[] buffer)
|
||||
{
|
||||
// buffer might be null, so we can't use .Length in that case
|
||||
writer.WriteBytesAndSize(buffer, 0, buffer != null ? buffer.Length : 0);
|
||||
}
|
||||
|
||||
// for byte arrays with dynamic size, where the reader doesn't know how many will come
|
||||
// (like an inventory with different items etc.)
|
||||
public static void WriteBytesAndSize(this NetworkWriter writer, byte[] buffer, int offset, int count)
|
||||
{
|
||||
// null is supported because [SyncVar]s might be structs with null byte[] arrays
|
||||
// write 0 for null array, increment normal size by 1 to save bandwidth
|
||||
// (using size=-1 for null would limit max size to 32kb instead of 64kb)
|
||||
if (buffer == null)
|
||||
{
|
||||
writer.WriteUInt(0u);
|
||||
return;
|
||||
}
|
||||
writer.WriteUInt(checked((uint)count) + 1u);
|
||||
writer.WriteBytes(buffer, offset, count);
|
||||
}
|
||||
|
||||
// writes ArraySegment of byte (most common type) and size header
|
||||
public static void WriteArraySegmentAndSize(this NetworkWriter writer, ArraySegment<byte> segment)
|
||||
{
|
||||
writer.WriteBytesAndSize(segment.Array, segment.Offset, segment.Count);
|
||||
}
|
||||
|
||||
// writes ArraySegment of any type, and size header
|
||||
public static void WriteArraySegment<T>(this NetworkWriter writer, ArraySegment<T> segment)
|
||||
{
|
||||
int length = segment.Count;
|
||||
writer.WriteInt(length);
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
writer.Write(segment.Array[segment.Offset + i]);
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteVector2(this NetworkWriter writer, Vector2 value) => writer.WriteBlittable(value);
|
||||
public static void WriteVector2Nullable(this NetworkWriter writer, Vector2? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteVector3(this NetworkWriter writer, Vector3 value) => writer.WriteBlittable(value);
|
||||
public static void WriteVector3Nullable(this NetworkWriter writer, Vector3? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteVector4(this NetworkWriter writer, Vector4 value) => writer.WriteBlittable(value);
|
||||
public static void WriteVector4Nullable(this NetworkWriter writer, Vector4? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteVector2Int(this NetworkWriter writer, Vector2Int value) => writer.WriteBlittable(value);
|
||||
public static void WriteVector2IntNullable(this NetworkWriter writer, Vector2Int? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteVector3Int(this NetworkWriter writer, Vector3Int value) => writer.WriteBlittable(value);
|
||||
public static void WriteVector3IntNullable(this NetworkWriter writer, Vector3Int? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteColor(this NetworkWriter writer, Color value) => writer.WriteBlittable(value);
|
||||
public static void WriteColorNullable(this NetworkWriter writer, Color? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteColor32(this NetworkWriter writer, Color32 value) => writer.WriteBlittable(value);
|
||||
public static void WriteColor32Nullable(this NetworkWriter writer, Color32? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteQuaternion(this NetworkWriter writer, Quaternion value) => writer.WriteBlittable(value);
|
||||
public static void WriteQuaternionNullable(this NetworkWriter writer, Quaternion? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
// Rect is a struct with properties instead of fields
|
||||
public static void WriteRect(this NetworkWriter writer, Rect value)
|
||||
{
|
||||
writer.WriteVector2(value.position);
|
||||
writer.WriteVector2(value.size);
|
||||
}
|
||||
public static void WriteRectNullable(this NetworkWriter writer, Rect? value)
|
||||
{
|
||||
writer.WriteBool(value.HasValue);
|
||||
if (value.HasValue)
|
||||
writer.WriteRect(value.Value);
|
||||
}
|
||||
|
||||
// Plane is a struct with properties instead of fields
|
||||
public static void WritePlane(this NetworkWriter writer, Plane value)
|
||||
{
|
||||
writer.WriteVector3(value.normal);
|
||||
writer.WriteFloat(value.distance);
|
||||
}
|
||||
public static void WritePlaneNullable(this NetworkWriter writer, Plane? value)
|
||||
{
|
||||
writer.WriteBool(value.HasValue);
|
||||
if (value.HasValue)
|
||||
writer.WritePlane(value.Value);
|
||||
}
|
||||
|
||||
// Ray is a struct with properties instead of fields
|
||||
public static void WriteRay(this NetworkWriter writer, Ray value)
|
||||
{
|
||||
writer.WriteVector3(value.origin);
|
||||
writer.WriteVector3(value.direction);
|
||||
}
|
||||
public static void WriteRayNullable(this NetworkWriter writer, Ray? value)
|
||||
{
|
||||
writer.WriteBool(value.HasValue);
|
||||
if (value.HasValue)
|
||||
writer.WriteRay(value.Value);
|
||||
}
|
||||
|
||||
// LayerMask is a struct with properties instead of fields
|
||||
public static void WriteLayerMask(this NetworkWriter writer, LayerMask layerMask)
|
||||
{
|
||||
// 32 layers as a flags enum, max value of 496, we only need a UShort.
|
||||
writer.WriteUShort((ushort)layerMask.value);
|
||||
}
|
||||
public static void WriteLayerMaskNullable(this NetworkWriter writer, LayerMask? layerMask)
|
||||
{
|
||||
writer.WriteBool(layerMask.HasValue);
|
||||
if (layerMask.HasValue)
|
||||
writer.WriteLayerMask(layerMask.Value);
|
||||
}
|
||||
|
||||
public static void WriteMatrix4x4(this NetworkWriter writer, Matrix4x4 value) => writer.WriteBlittable(value);
|
||||
public static void WriteMatrix4x4Nullable(this NetworkWriter writer, Matrix4x4? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteGuid(this NetworkWriter writer, Guid value)
|
||||
{
|
||||
#if !UNITY_2021_3_OR_NEWER
|
||||
// Unity 2019 doesn't have Span yet
|
||||
byte[] data = value.ToByteArray();
|
||||
writer.WriteBytes(data, 0, data.Length);
|
||||
#else
|
||||
// WriteBlittable(Guid) isn't safe. see WriteBlittable comments.
|
||||
// Guid is Sequential, but we can't guarantee packing.
|
||||
// TryWriteBytes is safe and allocation free.
|
||||
writer.EnsureCapacity(writer.Position + 16);
|
||||
value.TryWriteBytes(new Span<byte>(writer.buffer, writer.Position, 16));
|
||||
writer.Position += 16;
|
||||
#endif
|
||||
}
|
||||
public static void WriteGuidNullable(this NetworkWriter writer, Guid? value)
|
||||
{
|
||||
writer.WriteBool(value.HasValue);
|
||||
if (value.HasValue)
|
||||
writer.WriteGuid(value.Value);
|
||||
}
|
||||
|
||||
public static void WriteNetworkIdentity(this NetworkWriter writer, NetworkIdentity value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteUInt(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// users might try to use unspawned / prefab GameObjects in
|
||||
// rpcs/cmds/syncvars/messages. they would be null on the other
|
||||
// end, and it might not be obvious why. let's make it obvious.
|
||||
// https://github.com/vis2k/Mirror/issues/2060
|
||||
//
|
||||
// => warning (instead of exception) because we also use a warning
|
||||
// if a GameObject doesn't have a NetworkIdentity component etc.
|
||||
if (value.netId == 0)
|
||||
Debug.LogWarning($"Attempted to serialize unspawned GameObject: {value.name}. Prefabs and unspawned GameObjects would always be null on the other side. Please spawn it before using it in [SyncVar]s/Rpcs/Cmds/NetworkMessages etc.");
|
||||
|
||||
writer.WriteUInt(value.netId);
|
||||
}
|
||||
|
||||
public static void WriteNetworkBehaviour(this NetworkWriter writer, NetworkBehaviour value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteUInt(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// users might try to use unspawned / prefab NetworkBehaviours in
|
||||
// rpcs/cmds/syncvars/messages. they would be null on the other
|
||||
// end, and it might not be obvious why. let's make it obvious.
|
||||
// https://github.com/vis2k/Mirror/issues/2060
|
||||
// and more recently https://github.com/MirrorNetworking/Mirror/issues/3399
|
||||
//
|
||||
// => warning (instead of exception) because we also use a warning
|
||||
// when writing an unspawned NetworkIdentity
|
||||
if (value.netId == 0)
|
||||
{
|
||||
Debug.LogWarning($"Attempted to serialize unspawned NetworkBehaviour: of type {value.GetType()} on GameObject {value.name}. Prefabs and unspawned GameObjects would always be null on the other side. Please spawn it before using it in [SyncVar]s/Rpcs/Cmds/NetworkMessages etc.");
|
||||
writer.WriteUInt(0);
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteUInt(value.netId);
|
||||
writer.WriteByte(value.ComponentIndex);
|
||||
}
|
||||
|
||||
public static void WriteTransform(this NetworkWriter writer, Transform value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteUInt(0);
|
||||
return;
|
||||
}
|
||||
if (value.TryGetComponent(out NetworkIdentity identity))
|
||||
{
|
||||
writer.WriteUInt(identity.netId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// if users attempt to pass a transform without NetworkIdentity
|
||||
// to a [Command] or [SyncVar], it should show an obvious warning.
|
||||
Debug.LogWarning($"Attempted to sync a Transform ({value}) which isn't networked. Transforms without a NetworkIdentity component can't be synced.");
|
||||
writer.WriteUInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteGameObject(this NetworkWriter writer, GameObject value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteUInt(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// warn if the GameObject doesn't have a NetworkIdentity,
|
||||
if (!value.TryGetComponent(out NetworkIdentity identity))
|
||||
Debug.LogWarning($"Attempted to sync a GameObject ({value}) which isn't networked. GameObject without a NetworkIdentity component can't be synced.");
|
||||
|
||||
// serialize the correct amount of data in any case to make sure
|
||||
// that the other end can read the expected amount of data too.
|
||||
writer.WriteNetworkIdentity(identity);
|
||||
}
|
||||
|
||||
// while SyncList<T> is recommended for NetworkBehaviours,
|
||||
// structs may have .List<T> members which weaver needs to be able to
|
||||
// fully serialize for NetworkMessages etc.
|
||||
// note that Weaver/Writers/GenerateWriter() handles this manually.
|
||||
public static void WriteList<T>(this NetworkWriter writer, List<T> list)
|
||||
{
|
||||
// 'null' is encoded as '-1'
|
||||
if (list is null)
|
||||
{
|
||||
writer.WriteInt(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if within max size, otherwise Reader can't read it.
|
||||
if (list.Count > NetworkReader.AllocationLimit)
|
||||
throw new IndexOutOfRangeException($"NetworkWriter.WriteList - List<{typeof(T)}> too big: {list.Count} elements. Limit: {NetworkReader.AllocationLimit}");
|
||||
|
||||
writer.WriteInt(list.Count);
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
writer.Write(list[i]);
|
||||
}
|
||||
|
||||
// while SyncSet<T> is recommended for NetworkBehaviours,
|
||||
// structs may have .Set<T> members which weaver needs to be able to
|
||||
// fully serialize for NetworkMessages etc.
|
||||
// note that Weaver/Writers/GenerateWriter() handles this manually.
|
||||
// TODO writer not found. need to adjust weaver first. see tests.
|
||||
/*
|
||||
public static void WriteHashSet<T>(this NetworkWriter writer, HashSet<T> hashSet)
|
||||
{
|
||||
if (hashSet is null)
|
||||
{
|
||||
writer.WriteInt(-1);
|
||||
return;
|
||||
}
|
||||
writer.WriteInt(hashSet.Count);
|
||||
foreach (T item in hashSet)
|
||||
writer.Write(item);
|
||||
}
|
||||
*/
|
||||
|
||||
public static void WriteArray<T>(this NetworkWriter writer, T[] array)
|
||||
{
|
||||
// 'null' is encoded as '-1'
|
||||
if (array is null)
|
||||
{
|
||||
writer.WriteInt(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if within max size, otherwise Reader can't read it.
|
||||
if (array.Length > NetworkReader.AllocationLimit)
|
||||
throw new IndexOutOfRangeException($"NetworkWriter.WriteArray - Array<{typeof(T)}> too big: {array.Length} elements. Limit: {NetworkReader.AllocationLimit}");
|
||||
|
||||
writer.WriteInt(array.Length);
|
||||
for (int i = 0; i < array.Length; i++)
|
||||
writer.Write(array[i]);
|
||||
}
|
||||
|
||||
public static void WriteUri(this NetworkWriter writer, Uri uri)
|
||||
{
|
||||
writer.WriteString(uri?.ToString());
|
||||
}
|
||||
|
||||
public static void WriteTexture2D(this NetworkWriter writer, Texture2D texture2D)
|
||||
{
|
||||
// TODO allocation protection when sending textures to server.
|
||||
// currently can allocate 32k x 32k x 4 byte = 3.8 GB
|
||||
|
||||
// support 'null' textures for [SyncVar]s etc.
|
||||
// https://github.com/vis2k/Mirror/issues/3144
|
||||
// simply send -1 for width.
|
||||
if (texture2D == null)
|
||||
{
|
||||
writer.WriteShort(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if within max size, otherwise Reader can't read it.
|
||||
int totalSize = texture2D.width * texture2D.height;
|
||||
if (totalSize > NetworkReader.AllocationLimit)
|
||||
throw new IndexOutOfRangeException($"NetworkWriter.WriteTexture2D - Texture2D total size (width*height) too big: {totalSize}. Limit: {NetworkReader.AllocationLimit}");
|
||||
|
||||
// write dimensions first so reader can create the texture with size
|
||||
// 32k x 32k short is more than enough
|
||||
writer.WriteShort((short)texture2D.width);
|
||||
writer.WriteShort((short)texture2D.height);
|
||||
writer.WriteArray(texture2D.GetPixels32());
|
||||
}
|
||||
|
||||
public static void WriteSprite(this NetworkWriter writer, Sprite sprite)
|
||||
{
|
||||
// support 'null' textures for [SyncVar]s etc.
|
||||
// https://github.com/vis2k/Mirror/issues/3144
|
||||
// simply send a 'null' for texture content.
|
||||
if (sprite == null)
|
||||
{
|
||||
writer.WriteTexture2D(null);
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteTexture2D(sprite.texture);
|
||||
writer.WriteRect(sprite.rect);
|
||||
writer.WriteVector2(sprite.pivot);
|
||||
}
|
||||
|
||||
public static void WriteDateTime(this NetworkWriter writer, DateTime dateTime)
|
||||
{
|
||||
writer.WriteDouble(dateTime.ToOADate());
|
||||
}
|
||||
|
||||
public static void WriteDateTimeNullable(this NetworkWriter writer, DateTime? dateTime)
|
||||
{
|
||||
writer.WriteBool(dateTime.HasValue);
|
||||
if (dateTime.HasValue)
|
||||
writer.WriteDouble(dateTime.Value.ToOADate());
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkWriterExtensions.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkWriterExtensions.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94259792df2a404892c3e2377f58d0cb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
37
Assets/Mirror/Core/NetworkWriterPool.cs
Normal file
37
Assets/Mirror/Core/NetworkWriterPool.cs
Normal file
@ -0,0 +1,37 @@
|
||||
// API consistent with Microsoft's ObjectPool<T>.
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>Pool of NetworkWriters to avoid allocations.</summary>
|
||||
public static class NetworkWriterPool
|
||||
{
|
||||
// reuse Pool<T>
|
||||
// we still wrap it in NetworkWriterPool.Get/Recycle so we can reset the
|
||||
// position before reusing.
|
||||
// this is also more consistent with NetworkReaderPool where we need to
|
||||
// assign the internal buffer before reusing.
|
||||
static readonly Pool<NetworkWriterPooled> Pool = new Pool<NetworkWriterPooled>(
|
||||
() => new NetworkWriterPooled(),
|
||||
// initial capacity to avoid allocations in the first few frames
|
||||
// 1000 * 1200 bytes = around 1 MB.
|
||||
1000
|
||||
);
|
||||
|
||||
/// <summary>Get a writer from the pool. Creates new one if pool is empty.</summary>
|
||||
public static NetworkWriterPooled Get()
|
||||
{
|
||||
// grab from pool & reset position
|
||||
NetworkWriterPooled writer = Pool.Get();
|
||||
writer.Reset();
|
||||
return writer;
|
||||
}
|
||||
|
||||
/// <summary>Return a writer to the pool.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Return(NetworkWriterPooled writer)
|
||||
{
|
||||
Pool.Return(writer);
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkWriterPool.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkWriterPool.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f34b53bea38e4f259eb8dc211e4fdb6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
13
Assets/Mirror/Core/NetworkWriterPooled.cs
Normal file
13
Assets/Mirror/Core/NetworkWriterPooled.cs
Normal file
@ -0,0 +1,13 @@
|
||||
// "NetworkWriterPooled" instead of "PooledNetworkWriter" to group files, for
|
||||
// easier IDE workflow and more elegant code.
|
||||
using System;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>Pooled NetworkWriter, automatically returned to pool when using 'using'</summary>
|
||||
// TODO make sealed again after removing obsolete NetworkWriterPooled!
|
||||
public class NetworkWriterPooled : NetworkWriter, IDisposable
|
||||
{
|
||||
public void Dispose() => NetworkWriterPool.Return(this);
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/NetworkWriterPooled.cs.meta
Normal file
11
Assets/Mirror/Core/NetworkWriterPooled.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a9fab936bf3c4716a452d94ad5ecbebe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
13
Assets/Mirror/Core/PortTransport.cs
Normal file
13
Assets/Mirror/Core/PortTransport.cs
Normal file
@ -0,0 +1,13 @@
|
||||
// convenience interface for transports which use a port.
|
||||
// useful for cases where someone wants to 'just set the port' independent of
|
||||
// which transport it is.
|
||||
//
|
||||
// note that not all transports have ports, but most do.
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public interface PortTransport
|
||||
{
|
||||
ushort Port { get; set; }
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/PortTransport.cs.meta
Normal file
11
Assets/Mirror/Core/PortTransport.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7c7c2820d7974cb28c7bfe9aae890a0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
3
Assets/Mirror/Core/Prediction.meta
Normal file
3
Assets/Mirror/Core/Prediction.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e8e801f9c7f4b858d9a6c162e64ca84
|
||||
timeCreated: 1694005962
|
176
Assets/Mirror/Core/Prediction/Prediction.cs
Normal file
176
Assets/Mirror/Core/Prediction/Prediction.cs
Normal file
@ -0,0 +1,176 @@
|
||||
// standalone, easy to test algorithms for prediction
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// prediction may capture Rigidbody3D/2D/etc. state
|
||||
// have a common interface.
|
||||
public interface PredictedState
|
||||
{
|
||||
double timestamp { get; }
|
||||
|
||||
// use Vector3 for both Rigidbody3D and Rigidbody2D, that's fine
|
||||
Vector3 position { get; set; }
|
||||
Vector3 positionDelta { get; set; }
|
||||
|
||||
Quaternion rotation { get; set; }
|
||||
Quaternion rotationDelta { get; set; }
|
||||
|
||||
Vector3 velocity { get; set; }
|
||||
Vector3 velocityDelta { get; set; }
|
||||
|
||||
Vector3 angularVelocity { get; set; }
|
||||
Vector3 angularVelocityDelta { get; set; }
|
||||
}
|
||||
|
||||
public static class Prediction
|
||||
{
|
||||
// get the two states closest to a given timestamp.
|
||||
// those can be used to interpolate the exact state at that time.
|
||||
public static bool Sample<T>(
|
||||
SortedList<double, T> history,
|
||||
double timestamp, // current server time
|
||||
out T before,
|
||||
out T after,
|
||||
out int afterIndex,
|
||||
out double t) // interpolation factor
|
||||
{
|
||||
before = default;
|
||||
after = default;
|
||||
t = 0;
|
||||
afterIndex = -1;
|
||||
|
||||
// can't sample an empty history
|
||||
// interpolation needs at least two entries.
|
||||
// can't Lerp(A, A, 1.5). dist(A, A) * 1.5 is always 0.
|
||||
if (history.Count < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// older than oldest
|
||||
if (timestamp < history.Keys[0]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// iterate through the history
|
||||
// TODO this needs to be faster than O(N)
|
||||
// search around that area.
|
||||
// should be O(1) most of the time, unless sampling was off.
|
||||
int index = 0; // manually count when iterating. easier than for-int loop.
|
||||
KeyValuePair<double, T> prev = new KeyValuePair<double, T>();
|
||||
foreach (KeyValuePair<double, T> entry in history) {
|
||||
// exact match?
|
||||
if (timestamp == entry.Key)
|
||||
{
|
||||
before = entry.Value;
|
||||
after = entry.Value;
|
||||
afterIndex = index;
|
||||
t = Mathd.InverseLerp(entry.Key, entry.Key, timestamp);
|
||||
return true;
|
||||
}
|
||||
|
||||
// did we check beyond timestamp? then return the previous two.
|
||||
if (entry.Key > timestamp)
|
||||
{
|
||||
before = prev.Value;
|
||||
after = entry.Value;
|
||||
afterIndex = index;
|
||||
t = Mathd.InverseLerp(prev.Key, entry.Key, timestamp);
|
||||
return true;
|
||||
}
|
||||
|
||||
// remember the last
|
||||
prev = entry;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// inserts a server state into the client's history.
|
||||
// readjust the deltas of the states after the inserted one.
|
||||
// returns the corrected final position.
|
||||
public static T CorrectHistory<T>(
|
||||
SortedList<double, T> stateHistory,
|
||||
int stateHistoryLimit,
|
||||
T corrected, // corrected state with timestamp
|
||||
T before, // state in history before the correction
|
||||
T after, // state in history after the correction
|
||||
int afterIndex) // index of the 'after' value so we don't need to find it again here
|
||||
where T: PredictedState
|
||||
{
|
||||
// respect the limit
|
||||
// TODO unit test to check if it respects max size
|
||||
if (stateHistory.Count >= stateHistoryLimit)
|
||||
stateHistory.RemoveAt(0);
|
||||
|
||||
// insert the corrected state into the history, or overwrite if already exists
|
||||
stateHistory[corrected.timestamp] = corrected;
|
||||
|
||||
// the entry behind the inserted one still has the delta from (before, after).
|
||||
// we need to correct it to (corrected, after).
|
||||
//
|
||||
// for example:
|
||||
// before: (t=1.0, delta=10, position=10)
|
||||
// after: (t=3.0, delta=20, position=30)
|
||||
//
|
||||
// then we insert:
|
||||
// corrected: (t=2.5, delta=__, position=25)
|
||||
//
|
||||
// previous delta was from t=1.0 to t=3.0 => 2.0
|
||||
// inserted delta is from t=2.5 to t=3.0 => 0.5
|
||||
// multiplier is 0.5 / 2.0 = 0.25
|
||||
// multiply 'after.delta(20)' by 0.25 to get the new 'after.delta(5)
|
||||
//
|
||||
// so the new history is:
|
||||
// before: (t=1.0, delta=10, position=10)
|
||||
// corrected: (t=2.5, delta=__, position=25)
|
||||
// after: (t=3.0, delta= 5, position=__)
|
||||
//
|
||||
// so when we apply the correction, the new after.position would be:
|
||||
// corrected.position(25) + after.delta(5) = 30
|
||||
//
|
||||
double previousDeltaTime = after.timestamp - before.timestamp; // 3.0 - 1.0 = 2.0
|
||||
double correctedDeltaTime = after.timestamp - corrected.timestamp; // 3.0 - 2.5 = 0.5
|
||||
|
||||
// fix multiplier becoming NaN if previousDeltaTime is 0:
|
||||
// double multiplier = correctedDeltaTime / previousDeltaTime;
|
||||
double multiplier = previousDeltaTime != 0 ? correctedDeltaTime / previousDeltaTime : 0; // 0.5 / 2.0 = 0.25
|
||||
|
||||
// recalculate 'after.delta' with the multiplier
|
||||
after.positionDelta = Vector3.Lerp(Vector3.zero, after.positionDelta, (float)multiplier);
|
||||
after.velocityDelta = Vector3.Lerp(Vector3.zero, after.velocityDelta, (float)multiplier);
|
||||
after.angularVelocityDelta = Vector3.Lerp(Vector3.zero, after.angularVelocityDelta, (float)multiplier);
|
||||
// Quaternions always need to be normalized in order to be a valid rotation after operations
|
||||
after.rotationDelta = Quaternion.Slerp(Quaternion.identity, after.rotationDelta, (float)multiplier).normalized;
|
||||
|
||||
// changes aren't saved until we overwrite them in the history
|
||||
stateHistory[after.timestamp] = after;
|
||||
|
||||
// second step: readjust all absolute values by rewinding client's delta moves on top of it.
|
||||
T last = corrected;
|
||||
for (int i = afterIndex; i < stateHistory.Count; ++i)
|
||||
{
|
||||
double key = stateHistory.Keys[i];
|
||||
T entry = stateHistory.Values[i];
|
||||
|
||||
// correct absolute position based on last + delta.
|
||||
entry.position = last.position + entry.positionDelta;
|
||||
entry.velocity = last.velocity + entry.velocityDelta;
|
||||
entry.angularVelocity = last.angularVelocity + entry.angularVelocityDelta;
|
||||
// Quaternions always need to be normalized in order to be a valid rotation after operations
|
||||
entry.rotation = (entry.rotationDelta * last.rotation).normalized; // quaternions add delta by multiplying in this order
|
||||
|
||||
// save the corrected entry into history.
|
||||
stateHistory[key] = entry;
|
||||
|
||||
// save last
|
||||
last = entry;
|
||||
}
|
||||
|
||||
// third step: return the final recomputed state.
|
||||
return last;
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/Prediction/Prediction.cs.meta
Normal file
11
Assets/Mirror/Core/Prediction/Prediction.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 216d494d910445ea8a7acc7c889212d5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
151
Assets/Mirror/Core/RemoteCalls.cs
Normal file
151
Assets/Mirror/Core/RemoteCalls.cs
Normal file
@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.RemoteCalls
|
||||
{
|
||||
// invoke type for Cmd/Rpc
|
||||
public enum RemoteCallType { Command, ClientRpc }
|
||||
|
||||
// remote call function delegate
|
||||
public delegate void RemoteCallDelegate(NetworkBehaviour obj, NetworkReader reader, NetworkConnectionToClient senderConnection);
|
||||
|
||||
class Invoker
|
||||
{
|
||||
// GameObjects might have multiple components of TypeA.CommandA().
|
||||
// when invoking, we check if 'TypeA' is an instance of the type.
|
||||
// the hash itself isn't enough because we wouldn't know which component
|
||||
// to invoke it on if there are multiple of the same type.
|
||||
public Type componentType;
|
||||
public RemoteCallType callType;
|
||||
public RemoteCallDelegate function;
|
||||
public bool cmdRequiresAuthority;
|
||||
|
||||
public bool AreEqual(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate invokeFunction) =>
|
||||
this.componentType == componentType &&
|
||||
this.callType == remoteCallType &&
|
||||
this.function == invokeFunction;
|
||||
}
|
||||
|
||||
/// <summary>Used to help manage remote calls for NetworkBehaviours</summary>
|
||||
public static class RemoteProcedureCalls
|
||||
{
|
||||
public const string InvokeRpcPrefix = "InvokeUserCode_";
|
||||
|
||||
// one lookup for all remote calls.
|
||||
// allows us to easily add more remote call types without duplicating code.
|
||||
// note: do not clear those with [RuntimeInitializeOnLoad]
|
||||
//
|
||||
// IMPORTANT: cmd/rpc functions are identified via **HASHES**.
|
||||
// an index would requires half the bandwidth, but introduces issues
|
||||
// where static constructors are lazily called, so index order isn't
|
||||
// guaranteed. keep hashes to avoid:
|
||||
// https://github.com/vis2k/Mirror/pull/3135
|
||||
// https://github.com/vis2k/Mirror/issues/3138
|
||||
// BUT: 2 byte hash is enough if we check for collisions. that's what we
|
||||
// do for NetworkMessage as well.
|
||||
static readonly Dictionary<ushort, Invoker> remoteCallDelegates = new Dictionary<ushort, Invoker>();
|
||||
|
||||
static bool CheckIfDelegateExists(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate func, ushort functionHash)
|
||||
{
|
||||
if (remoteCallDelegates.ContainsKey(functionHash))
|
||||
{
|
||||
// something already registered this hash.
|
||||
// it's okay if it was the same function.
|
||||
Invoker oldInvoker = remoteCallDelegates[functionHash];
|
||||
if (oldInvoker.AreEqual(componentType, remoteCallType, func))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// otherwise notify user. there is a rare chance of string
|
||||
// hash collisions.
|
||||
Debug.LogError($"Function {oldInvoker.componentType}.{oldInvoker.function.GetMethodName()} and {componentType}.{func.GetMethodName()} have the same hash. Please rename one of them. To save bandwidth, we only use 2 bytes for the hash, which has a small chance of collisions.");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// pass full function name to avoid ClassA.Func & ClassB.Func collisions
|
||||
internal static ushort RegisterDelegate(Type componentType, string functionFullName, RemoteCallType remoteCallType, RemoteCallDelegate func, bool cmdRequiresAuthority = true)
|
||||
{
|
||||
// type+func so Inventory.RpcUse != Equipment.RpcUse
|
||||
ushort hash = (ushort)(functionFullName.GetStableHashCode() & 0xFFFF);
|
||||
|
||||
if (CheckIfDelegateExists(componentType, remoteCallType, func, hash))
|
||||
return hash;
|
||||
|
||||
remoteCallDelegates[hash] = new Invoker
|
||||
{
|
||||
callType = remoteCallType,
|
||||
componentType = componentType,
|
||||
function = func,
|
||||
cmdRequiresAuthority = cmdRequiresAuthority
|
||||
};
|
||||
return hash;
|
||||
}
|
||||
|
||||
// pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
|
||||
// need to pass componentType to support invoking on GameObjects with
|
||||
// multiple components of same type with same remote call.
|
||||
public static void RegisterCommand(Type componentType, string functionFullName, RemoteCallDelegate func, bool requiresAuthority) =>
|
||||
RegisterDelegate(componentType, functionFullName, RemoteCallType.Command, func, requiresAuthority);
|
||||
|
||||
// pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
|
||||
// need to pass componentType to support invoking on GameObjects with
|
||||
// multiple components of same type with same remote call.
|
||||
public static void RegisterRpc(Type componentType, string functionFullName, RemoteCallDelegate func) =>
|
||||
RegisterDelegate(componentType, functionFullName, RemoteCallType.ClientRpc, func);
|
||||
|
||||
// to clean up tests
|
||||
internal static void RemoveDelegate(ushort hash) =>
|
||||
remoteCallDelegates.Remove(hash);
|
||||
|
||||
internal static bool GetFunctionMethodName(ushort functionHash, out string methodName)
|
||||
{
|
||||
if (remoteCallDelegates.TryGetValue(functionHash, out Invoker invoker))
|
||||
{
|
||||
methodName = invoker.function.GetMethodName().Replace(InvokeRpcPrefix, "");
|
||||
return true;
|
||||
}
|
||||
methodName = "";
|
||||
return false;
|
||||
}
|
||||
|
||||
// note: no need to throw an error if not found.
|
||||
// an attacker might just try to call a cmd with an rpc's hash etc.
|
||||
// returning false is enough.
|
||||
static bool GetInvokerForHash(ushort functionHash, RemoteCallType remoteCallType, out Invoker invoker) =>
|
||||
remoteCallDelegates.TryGetValue(functionHash, out invoker) &&
|
||||
invoker != null &&
|
||||
invoker.callType == remoteCallType;
|
||||
|
||||
// InvokeCmd/Rpc Delegate can all use the same function here
|
||||
internal static bool Invoke(ushort functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkBehaviour component, NetworkConnectionToClient senderConnection = null)
|
||||
{
|
||||
// IMPORTANT: we check if the message's componentIndex component is
|
||||
// actually of the right type. prevents attackers trying
|
||||
// to invoke remote calls on wrong components.
|
||||
if (GetInvokerForHash(functionHash, remoteCallType, out Invoker invoker) &&
|
||||
invoker.componentType.IsInstanceOfType(component))
|
||||
{
|
||||
// invoke function on this component
|
||||
invoker.function(component, reader, senderConnection);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if the command 'requiresAuthority' which is set in the attribute
|
||||
internal static bool CommandRequiresAuthority(ushort cmdHash) =>
|
||||
GetInvokerForHash(cmdHash, RemoteCallType.Command, out Invoker invoker) &&
|
||||
invoker.cmdRequiresAuthority;
|
||||
|
||||
/// <summary>Gets the handler function by hash. Useful for profilers and debuggers.</summary>
|
||||
public static RemoteCallDelegate GetDelegate(ushort functionHash) =>
|
||||
remoteCallDelegates.TryGetValue(functionHash, out Invoker invoker)
|
||||
? invoker.function
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
11
Assets/Mirror/Core/RemoteCalls.cs.meta
Normal file
11
Assets/Mirror/Core/RemoteCalls.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d2cdbcbd1e377d6408a91acbec31ba16
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Core/SnapshotInterpolation.meta
Normal file
8
Assets/Mirror/Core/SnapshotInterpolation.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4468e736f87964eaebb9d55fc3e132f7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
17
Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs
Normal file
17
Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// Snapshot interface so we can reuse it for all kinds of systems.
|
||||
// for example, NetworkTransform, NetworkRigidbody, CharacterController etc.
|
||||
// NOTE: we use '<T>' and 'where T : Snapshot' to avoid boxing.
|
||||
// List<Snapshot> would cause allocations through boxing.
|
||||
namespace Mirror
|
||||
{
|
||||
public interface Snapshot
|
||||
{
|
||||
// the remote timestamp (when it was sent by the remote)
|
||||
double remoteTime { get; set; }
|
||||
|
||||
// the local timestamp (when it was received on our end)
|
||||
// technically not needed for basic snapshot interpolation.
|
||||
// only for dynamic buffer time adjustment.
|
||||
double localTime { get; set; }
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs.meta
Normal file
11
Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12afea28fdb94154868a0a3b7a9df55b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,390 @@
|
||||
// snapshot interpolation V2 by mischa
|
||||
//
|
||||
// Unity independent to be engine agnostic & easy to test.
|
||||
// boxing: in C#, uses <T> does not box! passing the interface would box!
|
||||
//
|
||||
// credits:
|
||||
// glenn fiedler: https://gafferongames.com/post/snapshot_interpolation/
|
||||
// fholm: netcode streams
|
||||
// fakebyte: standard deviation for dynamic adjustment
|
||||
// ninjakicka: math & debugging
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public static class SortedListExtensions
|
||||
{
|
||||
// removes the first 'amount' elements from the sorted list
|
||||
public static void RemoveRange<T, U>(this SortedList<T, U> list, int amount)
|
||||
{
|
||||
// remove the first element 'amount' times.
|
||||
// handles -1 and > count safely.
|
||||
for (int i = 0; i < amount && i < list.Count; ++i)
|
||||
list.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SnapshotInterpolation
|
||||
{
|
||||
// calculate timescale for catch-up / slow-down
|
||||
// note that negative threshold should be <0.
|
||||
// caller should verify (i.e. Unity OnValidate).
|
||||
// improves branch prediction.
|
||||
public static double Timescale(
|
||||
double drift, // how far we are off from bufferTime
|
||||
double catchupSpeed, // in % [0,1]
|
||||
double slowdownSpeed, // in % [0,1]
|
||||
double absoluteCatchupNegativeThreshold, // in seconds (careful, we may run out of snapshots)
|
||||
double absoluteCatchupPositiveThreshold) // in seconds
|
||||
{
|
||||
// if the drift time is too large, it means we are behind more time.
|
||||
// so we need to speed up the timescale.
|
||||
// note the threshold should be sendInterval * catchupThreshold.
|
||||
if (drift > absoluteCatchupPositiveThreshold)
|
||||
{
|
||||
// localTimeline += 0.001; // too simple, this would ping pong
|
||||
return 1 + catchupSpeed; // n% faster
|
||||
}
|
||||
|
||||
// if the drift time is too small, it means we are ahead of time.
|
||||
// so we need to slow down the timescale.
|
||||
// note the threshold should be sendInterval * catchupThreshold.
|
||||
if (drift < absoluteCatchupNegativeThreshold)
|
||||
{
|
||||
// localTimeline -= 0.001; // too simple, this would ping pong
|
||||
return 1 - slowdownSpeed; // n% slower
|
||||
}
|
||||
|
||||
// keep constant timescale while within threshold.
|
||||
// this way we have perfectly smooth speed most of the time.
|
||||
return 1;
|
||||
}
|
||||
|
||||
// calculate dynamic buffer time adjustment
|
||||
public static double DynamicAdjustment(
|
||||
double sendInterval,
|
||||
double jitterStandardDeviation,
|
||||
double dynamicAdjustmentTolerance)
|
||||
{
|
||||
// jitter is equal to delivery time standard variation.
|
||||
// delivery time is made up of 'sendInterval+jitter'.
|
||||
// .Average would be dampened by the constant sendInterval
|
||||
// .StandardDeviation is the changes in 'jitter' that we want
|
||||
// so add it to send interval again.
|
||||
double intervalWithJitter = sendInterval + jitterStandardDeviation;
|
||||
|
||||
// how many multiples of sendInterval is that?
|
||||
// we want to convert to bufferTimeMultiplier later.
|
||||
double multiples = intervalWithJitter / sendInterval;
|
||||
|
||||
// add the tolerance
|
||||
double safezone = multiples + dynamicAdjustmentTolerance;
|
||||
// UnityEngine.Debug.Log($"sendInterval={sendInterval:F3} jitter std={jitterStandardDeviation:F3} => that is ~{multiples:F1} x sendInterval + {dynamicAdjustmentTolerance} => dynamic bufferTimeMultiplier={safezone}");
|
||||
return safezone;
|
||||
}
|
||||
|
||||
// helper function to insert a snapshot if it doesn't exist yet.
|
||||
// extra function so we can use it for both cases:
|
||||
// NetworkClient global timeline insertions & adjustments via Insert<T>.
|
||||
// NetworkBehaviour local insertion without any time adjustments.
|
||||
public static bool InsertIfNotExists<T>(
|
||||
SortedList<double, T> buffer, // snapshot buffer
|
||||
int bufferLimit, // don't grow infinitely
|
||||
T snapshot) // the newly received snapshot
|
||||
where T : Snapshot
|
||||
{
|
||||
// slow clients may not be able to process incoming snapshots fast enough.
|
||||
// infinitely growing snapshots would make it even worse.
|
||||
// for example, run NetworkRigidbodyBenchmark while deep profiling client.
|
||||
// the client just grows and reallocates the buffer forever.
|
||||
if (buffer.Count >= bufferLimit) return false;
|
||||
|
||||
// SortedList does not allow duplicates.
|
||||
// we don't need to check ContainsKey (which is expensive).
|
||||
// simply add and compare count before/after for the return value.
|
||||
|
||||
//if (buffer.ContainsKey(snapshot.remoteTime)) return false; // too expensive
|
||||
// buffer.Add(snapshot.remoteTime, snapshot); // throws if key exists
|
||||
|
||||
int before = buffer.Count;
|
||||
buffer[snapshot.remoteTime] = snapshot; // overwrites if key exists
|
||||
return buffer.Count > before;
|
||||
}
|
||||
|
||||
// clamp timeline for cases where it gets too far behind.
|
||||
// for example, a client app may go into the background and get updated
|
||||
// with 1hz for a while. by the time it's back it's at least 30 frames
|
||||
// behind, possibly more if the transport also queues up. In this
|
||||
// scenario, at 1% catch up it took around 20+ seconds to finally catch
|
||||
// up. For these kinds of scenarios it will be better to snap / clamp.
|
||||
//
|
||||
// to reproduce, try snapshot interpolation demo and press the button to
|
||||
// simulate the client timeline at multiple seconds behind. it'll take
|
||||
// a long time to catch up if the timeline is a long time behind.
|
||||
public static double TimelineClamp(
|
||||
double localTimeline,
|
||||
double bufferTime,
|
||||
double latestRemoteTime)
|
||||
{
|
||||
// we want local timeline to always be 'bufferTime' behind remote.
|
||||
double targetTime = latestRemoteTime - bufferTime;
|
||||
|
||||
// we define a boundary of 'bufferTime' around the target time.
|
||||
// this is where catchup / slowdown will happen.
|
||||
// outside of the area, we clamp.
|
||||
double lowerBound = targetTime - bufferTime; // how far behind we can get
|
||||
double upperBound = targetTime + bufferTime; // how far ahead we can get
|
||||
return Mathd.Clamp(localTimeline, lowerBound, upperBound);
|
||||
}
|
||||
|
||||
// call this for every received snapshot.
|
||||
// adds / inserts it to the list & initializes local time if needed.
|
||||
public static void InsertAndAdjust<T>(
|
||||
SortedList<double, T> buffer, // snapshot buffer
|
||||
int bufferLimit, // don't grow infinitely
|
||||
T snapshot, // the newly received snapshot
|
||||
ref double localTimeline, // local interpolation time based on server time
|
||||
ref double localTimescale, // timeline multiplier to apply catchup / slowdown over time
|
||||
float sendInterval, // for debugging
|
||||
double bufferTime, // offset for buffering
|
||||
double catchupSpeed, // in % [0,1]
|
||||
double slowdownSpeed, // in % [0,1]
|
||||
ref ExponentialMovingAverage driftEma, // for catchup / slowdown
|
||||
float catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots)
|
||||
float catchupPositiveThreshold, // in % of sendInterval
|
||||
ref ExponentialMovingAverage deliveryTimeEma) // for dynamic buffer time adjustment
|
||||
where T : Snapshot
|
||||
{
|
||||
// first snapshot?
|
||||
// initialize local timeline.
|
||||
// we want it to be behind by 'offset'.
|
||||
//
|
||||
// note that the first snapshot may be a lagging packet.
|
||||
// so we would always be behind by that lag.
|
||||
// this requires catchup later.
|
||||
if (buffer.Count == 0)
|
||||
localTimeline = snapshot.remoteTime - bufferTime;
|
||||
|
||||
// insert into the buffer.
|
||||
//
|
||||
// note that we might insert it between our current interpolation
|
||||
// which is fine, it adds another data point for accuracy.
|
||||
//
|
||||
// note that insert may be called twice for the same key.
|
||||
// by default, this would throw.
|
||||
// need to handle it silently.
|
||||
if (InsertIfNotExists(buffer, bufferLimit, snapshot))
|
||||
{
|
||||
// dynamic buffer adjustment needs delivery interval jitter
|
||||
if (buffer.Count >= 2)
|
||||
{
|
||||
// note that this is not entirely accurate for scrambled inserts.
|
||||
//
|
||||
// we always use the last two, not what we just inserted
|
||||
// even if we were to use the diff for what we just inserted,
|
||||
// a scrambled insert would still not be 100% accurate:
|
||||
// => assume a buffer of AC, with delivery time C-A
|
||||
// => we then insert B, with delivery time B-A
|
||||
// => but then technically the first C-A wasn't correct,
|
||||
// as it would have to be C-B
|
||||
//
|
||||
// in practice, scramble is rare and won't make much difference
|
||||
double previousLocalTime = buffer.Values[buffer.Count - 2].localTime;
|
||||
double lastestLocalTime = buffer.Values[buffer.Count - 1].localTime;
|
||||
|
||||
// this is the delivery time since last snapshot
|
||||
double localDeliveryTime = lastestLocalTime - previousLocalTime;
|
||||
|
||||
// feed the local delivery time to the EMA.
|
||||
// this is what the original stream did too.
|
||||
// our final dynamic buffer adjustment is different though.
|
||||
// we use standard deviation instead of average.
|
||||
deliveryTimeEma.Add(localDeliveryTime);
|
||||
}
|
||||
|
||||
// adjust timescale to catch up / slow down after each insertion
|
||||
// because that is when we add new values to our EMA.
|
||||
|
||||
// we want localTimeline to be about 'bufferTime' behind.
|
||||
// for that, we need the delivery time EMA.
|
||||
// snapshots may arrive out of order, we can not use last-timeline.
|
||||
// we need to use the inserted snapshot's time - timeline.
|
||||
double latestRemoteTime = snapshot.remoteTime;
|
||||
|
||||
// ensure timeline stays within a reasonable bound behind/ahead.
|
||||
localTimeline = TimelineClamp(localTimeline, bufferTime, latestRemoteTime);
|
||||
|
||||
// calculate timediff after localTimeline override changes
|
||||
double timeDiff = latestRemoteTime - localTimeline;
|
||||
|
||||
// next, calculate average of a few seconds worth of timediffs.
|
||||
// this gives smoother results.
|
||||
//
|
||||
// to calculate the average, we could simply loop through the
|
||||
// last 'n' seconds worth of timediffs, but:
|
||||
// - our buffer may only store a few snapshots (bufferTime)
|
||||
// - looping through seconds worth of snapshots every time is
|
||||
// expensive
|
||||
//
|
||||
// to solve this, we use an exponential moving average.
|
||||
// https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
|
||||
// which is basically fancy math to do the same but faster.
|
||||
// additionally, it allows us to look at more timeDiff values
|
||||
// than we sould have access to in our buffer :)
|
||||
driftEma.Add(timeDiff);
|
||||
|
||||
// timescale depends on driftEma.
|
||||
// driftEma only changes when inserting.
|
||||
// therefore timescale only needs to be calculated when inserting.
|
||||
// saves CPU cycles in Update.
|
||||
|
||||
// next up, calculate how far we are currently away from bufferTime
|
||||
double drift = driftEma.Value - bufferTime;
|
||||
|
||||
// convert relative thresholds to absolute values based on sendInterval
|
||||
double absoluteNegativeThreshold = sendInterval * catchupNegativeThreshold;
|
||||
double absolutePositiveThreshold = sendInterval * catchupPositiveThreshold;
|
||||
|
||||
// next, set localTimescale to catchup consistently in Update().
|
||||
// we quantize between default/catchup/slowdown,
|
||||
// this way we have 'default' speed most of the time(!).
|
||||
// and only catch up / slow down for a little bit occasionally.
|
||||
// a consistent multiplier would never be exactly 1.0.
|
||||
localTimescale = Timescale(drift, catchupSpeed, slowdownSpeed, absoluteNegativeThreshold, absolutePositiveThreshold);
|
||||
|
||||
// debug logging
|
||||
// UnityEngine.Debug.Log($"sendInterval={sendInterval:F3} bufferTime={bufferTime:F3} drift={drift:F3} driftEma={driftEma.Value:F3} timescale={localTimescale:F3} deliveryIntervalEma={deliveryTimeEma.Value:F3}");
|
||||
}
|
||||
}
|
||||
|
||||
// sample snapshot buffer to find the pair around the given time.
|
||||
// returns indices so we can use it with RemoveRange to clear old snaps.
|
||||
// make sure to use use buffer.Values[from/to], not buffer[from/to].
|
||||
// make sure to only call this is we have > 0 snapshots.
|
||||
public static void Sample<T>(
|
||||
SortedList<double, T> buffer, // snapshot buffer
|
||||
double localTimeline, // local interpolation time based on server time
|
||||
out int from, // the snapshot <= time
|
||||
out int to, // the snapshot >= time
|
||||
out double t) // interpolation factor
|
||||
where T : Snapshot
|
||||
{
|
||||
from = -1;
|
||||
to = -1;
|
||||
t = 0;
|
||||
|
||||
// sample from [0,count-1] so we always have two at 'i' and 'i+1'.
|
||||
for (int i = 0; i < buffer.Count - 1; ++i)
|
||||
{
|
||||
// is local time between these two?
|
||||
T first = buffer.Values[i];
|
||||
T second = buffer.Values[i + 1];
|
||||
if (localTimeline >= first.remoteTime &&
|
||||
localTimeline <= second.remoteTime)
|
||||
{
|
||||
// use these two snapshots
|
||||
from = i;
|
||||
to = i + 1;
|
||||
t = Mathd.InverseLerp(first.remoteTime, second.remoteTime, localTimeline);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// didn't find two snapshots around local time.
|
||||
// so pick either the first or last, depending on which is closer.
|
||||
|
||||
// oldest snapshot ahead of local time?
|
||||
if (buffer.Values[0].remoteTime > localTimeline)
|
||||
{
|
||||
from = to = 0;
|
||||
t = 0;
|
||||
}
|
||||
// otherwise initialize both to the last one
|
||||
else
|
||||
{
|
||||
from = to = buffer.Count - 1;
|
||||
t = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// progress local timeline every update.
|
||||
//
|
||||
// ONLY CALL IF SNAPSHOTS.COUNT > 0!
|
||||
//
|
||||
// decoupled from Step<T> for easier testing and so we can progress
|
||||
// time only once in NetworkClient, while stepping for each component.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void StepTime(
|
||||
double deltaTime, // engine delta time (unscaled)
|
||||
ref double localTimeline, // local interpolation time based on server time
|
||||
double localTimescale) // catchup / slowdown is applied to time every update)
|
||||
{
|
||||
// move local forward in time, scaled with catchup / slowdown applied
|
||||
localTimeline += deltaTime * localTimescale;
|
||||
}
|
||||
|
||||
// sample, clear old.
|
||||
// call this every update.
|
||||
//
|
||||
// ONLY CALL IF SNAPSHOTS.COUNT > 0!
|
||||
//
|
||||
// returns true if there is anything to apply (requires at least 1 snap)
|
||||
// from/to/t are out parameters instead of an interpolated 'computed'.
|
||||
// this allows us to store from/to/t globally (i.e. in NetworkClient)
|
||||
// and have each component apply the interpolation manually.
|
||||
// besides, passing "Func Interpolate" would allocate anyway.
|
||||
public static void StepInterpolation<T>(
|
||||
SortedList<double, T> buffer, // snapshot buffer
|
||||
double localTimeline, // local interpolation time based on server time
|
||||
out T fromSnapshot, // we interpolate 'from' this snapshot
|
||||
out T toSnapshot, // 'to' this snapshot
|
||||
out double t) // at ratio 't' [0,1]
|
||||
where T : Snapshot
|
||||
{
|
||||
// check this in caller:
|
||||
// nothing to do if there are no snapshots at all yet
|
||||
// if (buffer.Count == 0) return false;
|
||||
|
||||
// sample snapshot buffer at local interpolation time
|
||||
Sample(buffer, localTimeline, out int from, out int to, out t);
|
||||
|
||||
// save from/to
|
||||
fromSnapshot = buffer.Values[from];
|
||||
toSnapshot = buffer.Values[to];
|
||||
|
||||
// remove older snapshots that we definitely don't need anymore.
|
||||
// after(!) using the indices.
|
||||
//
|
||||
// if we have 3 snapshots and we are between 2nd and 3rd:
|
||||
// from = 1, to = 2
|
||||
// then we need to remove the first one, which is exactly 'from'.
|
||||
// because 'from-1' = 0 would remove none.
|
||||
buffer.RemoveRange(from);
|
||||
}
|
||||
|
||||
// update time, sample, clear old.
|
||||
// call this every update.
|
||||
//
|
||||
// ONLY CALL IF SNAPSHOTS.COUNT > 0!
|
||||
//
|
||||
// returns true if there is anything to apply (requires at least 1 snap)
|
||||
// from/to/t are out parameters instead of an interpolated 'computed'.
|
||||
// this allows us to store from/to/t globally (i.e. in NetworkClient)
|
||||
// and have each component apply the interpolation manually.
|
||||
// besides, passing "Func Interpolate" would allocate anyway.
|
||||
public static void Step<T>(
|
||||
SortedList<double, T> buffer, // snapshot buffer
|
||||
double deltaTime, // engine delta time (unscaled)
|
||||
ref double localTimeline, // local interpolation time based on server time
|
||||
double localTimescale, // catchup / slowdown is applied to time every update
|
||||
out T fromSnapshot, // we interpolate 'from' this snapshot
|
||||
out T toSnapshot, // 'to' this snapshot
|
||||
out double t) // at ratio 't' [0,1]
|
||||
where T : Snapshot
|
||||
{
|
||||
StepTime(deltaTime, ref localTimeline, localTimescale);
|
||||
StepInterpolation(buffer, localTimeline, out fromSnapshot, out toSnapshot, out t);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 72c16070d85334011853813488ab1431
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user