Commit f6ed3c6f authored by cann-alberto's avatar cann-alberto
Browse files

first project commit

parent 4487b14f
fileFormatVersion: 2
guid: 04f0b2088c857414393bab3b80356776
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
// PredictedRigidbody stores a history of its rigidbody states.
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Mirror
{
// inline everything because this is performance critical!
public struct RigidbodyState : PredictedState
{
public double timestamp { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] private set; }
// we want to store position delta (last + delta = current), and current.
// this way we can apply deltas on top of corrected positions to get the corrected final position.
public Vector3 positionDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this position
public Vector3 position { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; }
public Quaternion rotationDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this rotation
public Quaternion rotation { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; }
public Vector3 velocityDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this velocity
public Vector3 velocity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; }
public Vector3 angularVelocityDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this velocity
public Vector3 angularVelocity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; }
public RigidbodyState(
double timestamp,
Vector3 positionDelta,
Vector3 position,
Quaternion rotationDelta,
Quaternion rotation,
Vector3 velocityDelta,
Vector3 velocity,
Vector3 angularVelocityDelta,
Vector3 angularVelocity)
{
this.timestamp = timestamp;
this.positionDelta = positionDelta;
this.position = position;
this.rotationDelta = rotationDelta;
this.rotation = rotation;
this.velocityDelta = velocityDelta;
this.velocity = velocity;
this.angularVelocityDelta = angularVelocityDelta;
this.angularVelocity = angularVelocity;
}
public static RigidbodyState Interpolate(RigidbodyState a, RigidbodyState b, float t)
{
return new RigidbodyState
{
position = Vector3.Lerp(a.position, b.position, t),
// Quaternions always need to be normalized in order to be a valid rotation after operations
rotation = Quaternion.Slerp(a.rotation, b.rotation, t).normalized,
velocity = Vector3.Lerp(a.velocity, b.velocity, t),
angularVelocity = Vector3.Lerp(a.angularVelocity, b.angularVelocity, t)
};
}
}
}
fileFormatVersion: 2
guid: ed0e1c0c874c4c9db6be2d5885bb7bee
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
// remote statistics panel from Mirror II to show connections, load, etc.
// server syncs statistics to clients if authenticated.
//
// attach this to a player.
// requires NetworkStatistics component on the Network object.
//
// Unity's OnGUI is the easiest to use solution at the moment.
// * playfab is super complex to set up
// * http servers would be nice, but still need to open ports, live refresh, etc
//
// for safety reasons, let's keep this read-only.
// at least until there's safe authentication.
using System;
using System.IO;
using UnityEngine;
namespace Mirror
{
// server -> client
struct Stats
{
// general
public int connections;
public double uptime;
public int configuredTickRate;
public int actualTickRate;
// traffic
public long sentBytesPerSecond;
public long receiveBytesPerSecond;
// cpu
public float serverTickInterval;
public double fullUpdateAvg;
public double serverEarlyAvg;
public double serverLateAvg;
public double transportEarlyAvg;
public double transportLateAvg;
// C# boilerplate
public Stats(
// general
int connections,
double uptime,
int configuredTickRate,
int actualTickRate,
// traffic
long sentBytesPerSecond,
long receiveBytesPerSecond,
// cpu
float serverTickInterval,
double fullUpdateAvg,
double serverEarlyAvg,
double serverLateAvg,
double transportEarlyAvg,
double transportLateAvg
)
{
// general
this.connections = connections;
this.uptime = uptime;
this.configuredTickRate = configuredTickRate;
this.actualTickRate = actualTickRate;
// traffic
this.sentBytesPerSecond = sentBytesPerSecond;
this.receiveBytesPerSecond = receiveBytesPerSecond;
// cpu
this.serverTickInterval = serverTickInterval;
this.fullUpdateAvg = fullUpdateAvg;
this.serverEarlyAvg = serverEarlyAvg;
this.serverLateAvg = serverLateAvg;
this.transportEarlyAvg = transportEarlyAvg;
this.transportLateAvg = transportLateAvg;
}
}
// [RequireComponent(typeof(NetworkStatistics))] <- needs to be on Network GO, not on NI
public class RemoteStatistics : NetworkBehaviour
{
// components ("fake statics" for similar API)
protected NetworkStatistics NetworkStatistics;
// broadcast to client.
// stats are quite huge, let's only send every few seconds via TargetRpc.
// instead of sending multiple times per second via NB.OnSerialize.
[Tooltip("Send stats every 'interval' seconds to client.")]
public float sendInterval = 1;
double lastSendTime;
[Header("GUI")]
public bool showGui;
public KeyCode hotKey = KeyCode.BackQuote;
Rect windowRect = new Rect(0, 0, 400, 400);
// password can't be stored in code or in Unity project.
// it would be available in clients otherwise.
// this is not perfectly secure. that's why RemoteStatistics is read-only.
[Header("Authentication")]
public string passwordFile = "remote_statistics.txt";
protected bool serverAuthenticated; // client needs to authenticate
protected bool clientAuthenticated; // show GUI until authenticated
protected string serverPassword = null; // null means not found, auth impossible
protected string clientPassword = ""; // for GUI
// statistics synced to client
Stats stats;
void LoadPassword()
{
// TODO only load once, not for all players?
// let's avoid static state for now.
// load the password
string path = Path.GetFullPath(passwordFile);
if (File.Exists(path))
{
// don't spam the server logs for every player's loaded file
// Debug.Log($"RemoteStatistics: loading password file: {path}");
try
{
serverPassword = File.ReadAllText(path);
}
catch (Exception exception)
{
Debug.LogWarning($"RemoteStatistics: failed to read password file: {exception}");
}
}
else
{
Debug.LogWarning($"RemoteStatistics: password file has not been created. Authentication will be impossible. Please save the password in: {path}");
}
}
protected override void OnValidate()
{
base.OnValidate();
syncMode = SyncMode.Owner;
}
// make sure to call base function when overwriting!
// public so it can also be called from tests (and be overwritten by users)
public override void OnStartServer()
{
NetworkStatistics = NetworkManager.singleton.GetComponent<NetworkStatistics>();
if (NetworkStatistics == null) throw new Exception($"RemoteStatistics requires a NetworkStatistics component on {NetworkManager.singleton.name}!");
// server needs to load the password
LoadPassword();
}
public override void OnStartLocalPlayer()
{
// center the window initially
windowRect.x = Screen.width / 2 - windowRect.width / 2;
windowRect.y = Screen.height / 2 - windowRect.height / 2;
}
[TargetRpc]
void TargetRpcSync(Stats v)
{
// store stats and flag as authenticated
clientAuthenticated = true;
stats = v;
}
[Command]
public void CmdAuthenticate(string v)
{
// was a valid password loaded on the server,
// and did the client send the correct one?
if (!string.IsNullOrWhiteSpace(serverPassword) &&
serverPassword.Equals(v))
{
serverAuthenticated = true;
Debug.Log($"RemoteStatistics: connectionId {connectionToClient.connectionId} authenticated with player {name}");
}
}
void UpdateServer()
{
// only sync if client has authenticated on the server
if (!serverAuthenticated) return;
// NetworkTime.localTime has defines for 2019 / 2020 compatibility
if (NetworkTime.localTime >= lastSendTime + sendInterval)
{
lastSendTime = NetworkTime.localTime;
// target rpc to owner client
TargetRpcSync(new Stats(
// general
NetworkServer.connections.Count,
NetworkTime.time,
NetworkServer.tickRate,
NetworkServer.actualTickRate,
// traffic
NetworkStatistics.serverSentBytesPerSecond,
NetworkStatistics.serverReceivedBytesPerSecond,
// cpu
NetworkServer.tickInterval,
NetworkServer.fullUpdateDuration.average,
NetworkServer.earlyUpdateDuration.average,
NetworkServer.lateUpdateDuration.average,
0, // TODO ServerTransport.earlyUpdateDuration.average,
0 // TODO ServerTransport.lateUpdateDuration.average
));
}
}
void UpdateClient()
{
if (Input.GetKeyDown(hotKey))
showGui = !showGui;
}
void Update()
{
if (isServer) UpdateServer();
if (isLocalPlayer) UpdateClient();
}
void OnGUI()
{
if (!isLocalPlayer) return;
if (!showGui) return;
windowRect = GUILayout.Window(0, windowRect, OnWindow, "Remote Statistics");
windowRect = Utils.KeepInScreen(windowRect);
}
// Text: value
void GUILayout_TextAndValue(string text, string value)
{
GUILayout.BeginHorizontal();
GUILayout.Label(text);
GUILayout.FlexibleSpace();
GUILayout.Label(value);
GUILayout.EndHorizontal();
}
// fake a progress bar via horizontal scroll bar with ratio as width
void GUILayout_ProgressBar(double ratio, int width)
{
// clamp ratio, otherwise >1 would make it extremely large
ratio = Mathd.Clamp01(ratio);
GUILayout.HorizontalScrollbar(0, (float)ratio, 0, 1, GUILayout.Width(width));
}
// need to specify progress bar & caption width,
// otherwise differently sized captions would always misalign the
// progress bars.
void GUILayout_TextAndProgressBar(string text, double ratio, int progressbarWidth, string caption, int captionWidth, Color captionColor)
{
GUILayout.BeginHorizontal();
GUILayout.Label(text);
GUILayout.FlexibleSpace();
GUILayout_ProgressBar(ratio, progressbarWidth);
// coloring the caption is enough. otherwise it's too much.
GUI.color = captionColor;
GUILayout.Label(caption, GUILayout.Width(captionWidth));
GUI.color = Color.white;
GUILayout.EndHorizontal();
}
void GUI_Authenticate()
{
GUILayout.BeginVertical("Box"); // start general
GUILayout.Label("<b>Authentication</b>");
// warning if insecure connection
// if (ClientTransport.IsEncrypted())
// {
// GUILayout.Label("<i>Connection is encrypted!</i>");
// }
// else
// {
GUILayout.Label("<i>Connection is not encrypted. Use with care!</i>");
// }
// input
clientPassword = GUILayout.PasswordField(clientPassword, '*');
// button
GUI.enabled = !string.IsNullOrWhiteSpace(clientPassword);
if (GUILayout.Button("Authenticate"))
{
CmdAuthenticate(clientPassword);
}
GUI.enabled = true;
GUILayout.EndVertical(); // end general
}
void GUI_General(
int connections,
double uptime,
int configuredTickRate,
int actualTickRate)
{
GUILayout.BeginVertical("Box"); // start general
GUILayout.Label("<b>General</b>");
// connections
GUILayout_TextAndValue("Connections:", $"<b>{connections}</b>");
// uptime
GUILayout_TextAndValue("Uptime:", $"<b>{Utils.PrettySeconds(uptime)}</b>"); // TODO
// tick rate
// might be lower under heavy load.
// might be higher in editor if targetFrameRate can't be set.
GUI.color = actualTickRate < configuredTickRate ? Color.red : Color.green;
GUILayout_TextAndValue("Tick Rate:", $"<b>{actualTickRate} Hz / {configuredTickRate} Hz</b>");
GUI.color = Color.white;
GUILayout.EndVertical(); // end general
}
void GUI_Traffic(
long serverSentBytesPerSecond,
long serverReceivedBytesPerSecond)
{
GUILayout.BeginVertical("Box");
GUILayout.Label("<b>Network</b>");
GUILayout_TextAndValue("Outgoing:", $"<b>{Utils.PrettyBytes(serverSentBytesPerSecond) }/s</b>");
GUILayout_TextAndValue("Incoming:", $"<b>{Utils.PrettyBytes(serverReceivedBytesPerSecond)}/s</b>");
GUILayout.EndVertical();
}
void GUI_Cpu(
float serverTickInterval,
double fullUpdateAvg,
double serverEarlyAvg,
double serverLateAvg,
double transportEarlyAvg,
double transportLateAvg)
{
const int barWidth = 120;
const int captionWidth = 90;
GUILayout.BeginVertical("Box");
GUILayout.Label("<b>CPU</b>");
// unity update
// happens every 'tickInterval'. progress bar shows it in relation.
// <= 90% load is green, otherwise red
double fullRatio = fullUpdateAvg / serverTickInterval;
GUILayout_TextAndProgressBar(
"World Update Avg:",
fullRatio,
barWidth, $"<b>{fullUpdateAvg * 1000:F1} ms</b>",
captionWidth,
fullRatio <= 0.9 ? Color.green : Color.red);
// server update
// happens every 'tickInterval'. progress bar shows it in relation.
// <= 90% load is green, otherwise red
double serverRatio = (serverEarlyAvg + serverLateAvg) / serverTickInterval;
GUILayout_TextAndProgressBar(
"Server Update Avg:",
serverRatio,
barWidth, $"<b>{serverEarlyAvg * 1000:F1} + {serverLateAvg * 1000:F1} ms</b>",
captionWidth,
serverRatio <= 0.9 ? Color.green : Color.red);
// transport: early + late update milliseconds.
// for threaded transport, this is the thread's update time.
// happens every 'tickInterval'. progress bar shows it in relation.
// <= 90% load is green, otherwise red
// double transportRatio = (transportEarlyAvg + transportLateAvg) / serverTickInterval;
// GUILayout_TextAndProgressBar(
// "Transport Avg:",
// transportRatio,
// barWidth,
// $"<b>{transportEarlyAvg * 1000:F1} + {transportLateAvg * 1000:F1} ms</b>",
// captionWidth,
// transportRatio <= 0.9 ? Color.green : Color.red);
GUILayout.EndVertical();
}
void GUI_Notice()
{
// for security reasons, let's keep this read-only for now.
// single line keeps input & visuals simple
// GUILayout.BeginVertical("Box");
// GUILayout.Label("<b>Global Notice</b>");
// notice = GUILayout.TextField(notice);
// if (GUILayout.Button("Send"))
// {
// // TODO
// }
// GUILayout.EndVertical();
}
void OnWindow(int windowID)
{
if (!clientAuthenticated)
{
GUI_Authenticate();
}
else
{
GUI_General(
stats.connections,
stats.uptime,
stats.configuredTickRate,
stats.actualTickRate
);
GUI_Traffic(
stats.sentBytesPerSecond,
stats.receiveBytesPerSecond
);
GUI_Cpu(
stats.serverTickInterval,
stats.fullUpdateAvg,
stats.serverEarlyAvg,
stats.serverLateAvg,
stats.transportEarlyAvg,
stats.transportLateAvg
);
GUI_Notice();
}
// dragable window in any case
GUI.DragWindow(new Rect(0, 0, 10000, 10000));
}
}
}
fileFormatVersion: 2
guid: ba360e4ff6b44fc6898f56322b90c6c8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
fileFormatVersion: 2
guid: 9f4328ccc5f724e45afe2215d275b5d5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
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")]
[assembly: InternalsVisibleTo("Mirror.Components")]
fileFormatVersion: 2
guid: e28d5f410e25b42e6a76a2ffc10e4675
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
using System;
using UnityEngine;
namespace Mirror
{
/// <summary>
/// SyncVars are used to automatically synchronize a variable between the server and all clients. The direction of synchronization depends on the Sync Direction property, ServerToClient by default.
/// <para>
/// When Sync Direction is equal to ServerToClient, the value should be changed on the server side and synchronized to all clients.
/// Otherwise, the value should be changed on the client side and synchronized to server and other clients.
/// </para>
/// <para>Hook parameter allows you to define a method to be invoked when gets an value update. Notice that the hook method will not be called on the change side.</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 {}
}
fileFormatVersion: 2
guid: c04c722ee2ffd49c8a56ab33667b10b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
fileFormatVersion: 2
guid: 1c38e1bebe9947f8b842a8a57aa2b71c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
// 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;
}
// return all batches to the pool for cleanup
public void Clear()
{
// return batch in progress
if (batch != null)
{
NetworkWriterPool.Return(batch);
batch = null;
}
// return all queued batches
foreach (NetworkWriterPooled queued in batches)
NetworkWriterPool.Return(queued);
batches.Clear();
}
}
}
fileFormatVersion: 2
guid: 0afaaa611a2142d48a07bdd03b68b2b3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
// 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;
}
}
}
fileFormatVersion: 2
guid: 328562d71e1c45c58581b958845aa7a4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
// 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;
}
}
}
fileFormatVersion: 2
guid: ff663b880e33e4606b545c8b497041c2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
// 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();
}
}
}
fileFormatVersion: 2
guid: d27175a08d5341fc97645b49ee533d5a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
// 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);
}
}
}
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment