Files
PacManUdon/Assets/Scripts/NetworkManager.cs

927 lines
32 KiB
C#

using System;
using System.Text;
using TMPro;
using UdonSharp;
using UnityEngine;
using VRC.SDK3.UdonNetworkCalling;
using VRC.SDKBase;
using VRC.Udon.Common;
namespace Marro.PacManUdon
{
public enum NetworkEventType
{
FullSyncForced = 0,
FullSync = 1,
PacManTurn = 2,
StartGameButtonPressed = 3,
}
public class NetworkManager : UdonSharpBehaviour
{
// The network manager works by serializing event and state data into a byte array, including a timestamp for each event.
// If user is owner, this data is created and stored in a buffer which is cleared upon transmission.
// If user is not owner, this data is read into the same buffer and replayed based on the included timestamp.
// Data replay is delayed with an offset on the timestamp to hide inconsistency in latency.
// Each event contains a header. The first part contains the size of the event as a ushort, allowing a maximum size of 65,536 bytes.
// After that timestamp is transferred in seconds since the owner started the game as a float, matching Unity's time representation.
// Afterwards is a message id, a single byte which increments (with overflow) for every event to confirm data completeness.
// Lastly, a single byte indicates the type of event. 0 is a full sync, which is reserved for syncing up from any unknown state.
// A byte array is used as a DataList or DataDictionary can only be transmitted as JSON which is much less efficient.
// As Udon does not support instantiating objects, I have not created classes to represent the data being sent.
// Correct parsing is dependend upon everything being read out identially to how it was created.
// An event has the following structure:
// [0-1]: (ushort) Size of event.
// [2-5]: (float) Time in seconds at which event occured.
// [6]: (byte) Message id, increments by one for each message with the same timestamp.
// [7]: (byte) Type of event. 0 = Full Sync, which is used to sync up from an undefinted state.
// [+]: Event-specific data.
#region Settings
/// <summary>
/// The root from which this <see cref="NetworkManager"/> will look for <see cref="SyncedObject"/> to control.
/// </summary>
[SerializeField] private GameObject root;
/// <summary>
/// The delay at which the receiving side replays events.
/// </summary>
[SerializeField] private float delay = 1f;
/// <summary>
/// The maximum amount of times a message is sent.
/// </summary>
[SerializeField] private int maxEventSendTries = 3;
/// <summary>
/// How long to wait since last message to send next ping.
/// </summary>
[SerializeField] private float pingDelay = 0.3f;
#endregion
#region Constants
/// <summary>
/// The maximum amount of events in the buffer.
/// </summary>
private const int BufferMaxTotalEvents = 255;
/// <summary>
/// The index in an event where the event size is stored.
/// </summary>
private const ushort HeaderEventSizeIndex = 0;
/// <summary>
/// The index in an event where the timestamp is stored.
/// </summary>
private const ushort HeaderTimestampIndex = 2;
/// <summary>
/// The index in an event where the event id is stored.
/// </summary>
private const ushort HeaderEventIdIndex = 6;
/// <summary>
/// The index in an event where the event type is stored.
/// </summary>
private const ushort HeaderEventTypeIndex = 7;
/// <summary>
/// The total length of the header of an event, in bytes.
/// </summary>
private const ushort HeaderLength = 8;
#endregion
#region Private attributes
/// <summary>
/// Objects which are controlled by this <see cref="NetworkManager"/>.
/// </summary>
private SyncedObject[] syncedObjects;
/// <summary>
/// Offset from system time to network time, including delay.
/// </summary>
private float offsetTime;
/// <summary>
/// Time since last full sync, captured when this FixedUpdate started, with network delay applied.
/// </summary>
private float internalTime;
/// <summary>
/// Time at which next received event occured.
/// </summary>
private float nextEventTime;
/// <summary>
/// Amounot of retries in a row without a successful sync.
/// </summary>
private int retriesWithoutSuccess;
/// <summary>
/// For receiver: True if there's a full sync in the queue and we are not synced, otherwise false.
/// For transmitter: True if there's a full sync in the queue which has not yet been transmitted, otherwise false.
/// </summary>
private bool hasFullSyncReady;
/// <summary>
/// True if serialization has been requestsed
/// </summary>
private bool serializationRequested;
/// <summary>
/// Queue of events to be transmitted or processed.
/// </summary>
private byte[][] eventsQueue;
/// <summary>
/// Index of <see cref="eventsQueue"/>.
/// </summary>
private int eventsQueueIndex;
/// <summary>
/// The value of <see cref="eventsQueueIndex"/> at the last transmission.
/// </summary>
private int eventsQueueIndexAtLastTransmission;
/// <summary>
/// Counts of new events at recent transmissions.
/// </summary>
private int[] eventTransmissionHistory;
/// <summary>
/// Index of <see cref="eventTransmissionHistoryIndex"/>.
/// </summary>
private int eventTransmissionHistoryIndex;
/// <summary>
/// Time of last event transmission.
/// </summary>
private float lastEventTransmissionTime;
/// <summary>
/// The message id of the most recent event created or received.
/// </summary>
private byte lastEventId;
/// <summary>
/// Data which is currently available on the network.
/// </summary>
[UdonSynced] private byte[] networkedData = new byte[0];
#endregion
#region Public fields
/// <summary>
/// Whether this <see cref="NetworkManager"/> is ready to transmit or receive data.
/// If false, networking is disabled and this <see cref="NetworkManager"/> acts as a pass-through.
/// </summary>
public bool Ready { get; private set; } = false;
/// <summary>
/// Whether the current perspective is synced with the owner. (Always true if current perspective is owner.)
/// </summary>
public bool Synced { get; private set; } = false;
/// <summary>
/// The time since last full sync which is currently being simulated.
/// </summary>
public float SyncedTime { get; private set; }
/// <summary>
/// Time since the last simulation, in seconds.
/// </summary>
public float SyncedDeltaTime { get; private set; }
/// <summary>
/// Is the current simulation to prepare for applying a network event?
/// True = Yes, This update is preparing for a network update.
/// False = No, this update is after the network update or there was no network update this cycle.
/// </summary>
public bool IsEventUpdate { get; private set; }
/// <summary>
/// Is the local user owner?
/// </summary>
public bool IsOwner { get; private set; }
#endregion
#region General
public void Awake()
{
// I would set the instance variable here, but it's somehow null by the time Initialize() is called... Udon moment?
var syncedObjects = root.GetComponentsInChildren<SyncedObject>(includeInactive: true);
foreach (var obj in syncedObjects)
{
obj.networkManager = this;
}
}
public void Initialize()
{
if (!BitConverter.IsLittleEndian)
{
Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Fatal: NetworkManager only supports little endian! Network sync will not be possible.");
Ready = false;
return;
}
syncedObjects = root.GetComponentsInChildren<SyncedObject>(includeInactive: true);
foreach (var obj in syncedObjects)
{
obj.networkManager = this;
}
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Found {syncedObjects.Length} {nameof(SyncedObject)} in children of {root.name}.");
SetOwner(Networking.IsOwner(gameObject));
ClearBuffer();
Synced = IsOwner; // Owner is always synced
retriesWithoutSuccess = 0;
hasFullSyncReady = false;
offsetTime = Time.fixedTime;
internalTime = 0;
SyncedTime = 0;
SyncedDeltaTime = Time.fixedDeltaTime;
Ready = true;
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Initialized, time offset: {offsetTime}");
}
public void FixedUpdate()
{
if (!Ready)
{
return;
}
// Fetch the current time
UpdateInternalTime();
if (Ready)
{
if (IsOwner)
{
ProgressPingTime(); // See if we need to send a ping
}
else
{
ProgressEventTime(); // See if there's events that need to be replayed
}
}
// Forwards simulated time at the FixedUpdate pace
PerformFixedSyncedUpdate();
}
private void UpdateInternalTime()
{
internalTime = Time.fixedTime - offsetTime;
}
private void PerformFixedSyncedUpdate()
{
IsEventUpdate = false;
ProgressSyncedTime(internalTime);
for (int i = 0; i < syncedObjects.Length; i++)
{
var obj = syncedObjects[i];
if (obj.gameObject.activeInHierarchy)
{
obj.SyncedUpdate();
}
}
}
private void HandleError(bool clearBuffer)
{
retriesWithoutSuccess++;
if (retriesWithoutSuccess > 3)
{
Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Fatal: Retried 3 times without success.");
Ready = false;
return;
}
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Encountered data error, attempting to recover via full sync.");
if (clearBuffer)
{
ClearBuffer();
}
Synced = false;
if (!IsOwner)
{
RequestEvent(NetworkEventType.FullSync);
}
else
{
SendEvent(NetworkEventType.FullSyncForced);
}
}
private void SetOwner(bool isOwner)
{
IsOwner = isOwner;
if (DebugImageToIndicateOwner != null)
{
DebugImageToIndicateOwner.SetFloat("Color", isOwner ? 1 : 0);
}
}
#endregion
#region Sender
public void SendEvent(NetworkEventType eventType)
{
if (!Ready)
{
return;
}
if (!IsOwner)
{
Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(SendEvent)} while not the owner!");
return;
}
var timestamp = SyncedTime;
var eventId = GetNextEventId(lastEventId);
InitializeEvent(eventType, timestamp, eventId, BufferMaxTotalEvents, out byte[][] data, out var index);
foreach (var obj in syncedObjects)
{
obj.AppendSyncedData(data, ref index, eventType);
}
var result = Flatten(data, 0, index);
// Validate and fill in event size
var eventSize = result.Length;
if (eventSize > ushort.MaxValue || eventSize < 0)
{
Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) New event is too large or negative! Size is {eventSize}, maximum allowed is {ushort.MaxValue}");
HandleError(false);
return;
}
var eventSizeBytes = BitConverter.GetBytes((ushort)eventSize);
Array.Copy(eventSizeBytes, 0, result, HeaderEventSizeIndex, eventSizeBytes.Length);
if (IsFullSync(eventType))
{
hasFullSyncReady = true;
}
QueueEventInBuffer(result);
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Prepared event of type {eventType} with {eventSize} bytes, timestamp {timestamp} and id {eventId} for serialization.");
RequestSerializationForEvents();
lastEventId = eventId;
retriesWithoutSuccess = 0; // We had success!
}
private static void InitializeEvent(NetworkEventType eventType, float timestamp, byte eventId, int maxSize, out byte[][] data, out int index)
{
// Create header
var timestampBytes = BitConverter.GetBytes(timestamp);
var header = new byte[HeaderLength];
// Event size is added later
Array.Copy(timestampBytes, 0, header, HeaderTimestampIndex, timestampBytes.Length);
header[HeaderEventIdIndex] = eventId;
header[HeaderEventTypeIndex] = Int32ToByte((int)eventType);
// Initialize event container
data = new byte[maxSize][];
data[0] = header;
index = 1;
}
private void RequestSerializationForEvents()
{
RequestSerialization();
serializationRequested = true;
}
[NetworkCallable]
public void RequestEventReceived(NetworkEventType eventType)
{
if (!Ready)
{
return;
}
if (IsFullSync(eventType) && hasFullSyncReady)
{
return; // Don't send another full sync if we're already preparing to send one
}
if (eventType == NetworkEventType.FullSyncForced)
{
SendEvent(NetworkEventType.FullSync); // Remote is not allowed to request a forced full sync
}
SendEvent(eventType);
}
private void ProgressPingTime()
{
if (eventsQueueIndex > 0
&& internalTime - lastEventTransmissionTime >= pingDelay)
{
RequestSerializationForEvents();
}
}
public override void OnPreSerialization()
{
if (!Ready || !IsOwner || !serializationRequested || eventsQueue == null || eventsQueueIndex == 0)
{
return;
}
networkedData = Flatten(eventsQueue, 0, eventsQueueIndex);
eventTransmissionHistory[eventTransmissionHistoryIndex] = eventsQueueIndex - eventsQueueIndexAtLastTransmission;
eventTransmissionHistoryIndex += 1;
if (eventTransmissionHistoryIndex >= eventTransmissionHistory.Length)
{
eventTransmissionHistoryIndex = 0;
}
serializationRequested = false;
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Serializing {eventsQueueIndex} event(s), eventTransmissionHistory is now {ArrayToString(eventTransmissionHistory)} with index {eventTransmissionHistoryIndex}");
}
public override void OnPostSerialization(SerializationResult result)
{
if (!Ready || !IsOwner || networkedData.Length == 0)
{
return;
}
if (!result.success)
{
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Serialization failed! Tried to send {result.byteCount} bytes.");
return;
}
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Serialized with {networkedData.Length} bytes.\nBytes sent:\n{ArrayToString(networkedData)}");
// Remove data from the buffer that no longer needs to be (re)transmitted
DequeueEventsFromBuffer(eventTransmissionHistory[eventTransmissionHistoryIndex]);
eventTransmissionHistory[eventTransmissionHistoryIndex] = 0; // Prevent trying to remove the same events again
eventsQueueIndexAtLastTransmission = eventsQueueIndex;
networkedData = new byte[0];
// If there was a full sync in the queue, it has now been transmitted at least once
hasFullSyncReady = false;
lastEventTransmissionTime = internalTime;
}
#endregion
#region Receiver
public void RequestEvent(NetworkEventType eventType)
{
if (!Ready)
{
return;
}
if (IsOwner)
{
Debug.LogError($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Attempted {nameof(RequestEvent)} while we are the owner!");
return;
}
SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.Owner, "RequestEventReceived", eventType);
}
private void StoreIncomingData()
{
if (networkedData == null || networkedData.Length == 0)
{
return; // Nothing to store
}
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Received {networkedData.Length} bytes!\nBytes received:\n{ArrayToString(networkedData)}");
var length = networkedData.Length;
int index = 0;
int eventSize = 0; // Store event size here so we can increment the index no matter how we increment the loop
while ((index += eventSize) < length)
{
if (length - index < HeaderLength)
{
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) {nameof(StoreIncomingData)}: Remaining data in networkedData is not long enough to form a complete event! remaining: {length - index}.");
HandleError(false);
return;
}
eventSize = GetEventSizeFromHeader(networkedData, index);
if (length - index < eventSize)
{
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) {nameof(StoreIncomingData)}: Event size is larger than total remaining data! {nameof(eventSize)}: {eventSize}, remaining: {length - index}.");
HandleError(false);
return;
}
if (length - index < HeaderLength)
{
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) {nameof(StoreIncomingData)}: Event size is not long enough to form a complete event! {nameof(eventSize)}: {eventSize}, minimum needed: {HeaderLength}.");
HandleError(false);
return;
}
var @event = GetArrayPart(networkedData, index, eventSize);
var timestamp = GetTimestampFromHeader(@event);
var eventId = GetEventIdFromHeader(@event);
var eventType = GetEventTypeFromHeader(@event);
if (eventType != NetworkEventType.FullSyncForced && (Synced || hasFullSyncReady))
{
// Check if event id is sequential
if (eventId != GetNextEventId(lastEventId))
{
if (index + eventSize >= length // If this is the last event of the batch
&& eventId != lastEventId) // Unless the eventId is the same, then this is probably just a duplicate
{
// The last event of the batch has to be synced up with our current event id count, else we've missed an event
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) EventIds were not sequential! Did we miss an event? Timestamp: {timestamp}, eventId: {eventId}, lastEventId: {lastEventId}.");
HandleError(false);
return;
}
// We've likely already processed this event, ignore it
continue;
}
QueueEventInBuffer(@event);
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)} Queued event with id {eventId}");
}
else
{
// If we're not yet synced, we only care about full sync events.
if (IsFullSync(eventType))
{
QueueFullSyncForReplay(@event); // Immediately process full sync
}
}
lastEventId = eventId;
}
if (Synced)
{
UpdateNextEventTime();
}
}
private void QueueFullSyncForReplay(byte[] @event)
{
// Clear buffer and put the full sync into it
ClearBuffer();
QueueEventInBuffer(@event);
// Set this event to play after the default delay
nextEventTime = internalTime + delay;
hasFullSyncReady = true;
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Queued full sync in buffer, should execute at {nextEventTime}.");
}
private void ProgressEventTime()
{
IsEventUpdate = true;
while (eventsQueueIndex > 0 && nextEventTime <= internalTime)
{
var success = PerformEvent(eventsQueue[0]);
if (!success)
{
return;
}
DequeueEventsFromBuffer(1);
UpdateNextEventTime();
}
}
private bool PerformEvent(byte[] @event)
{
var timestamp = GetTimestampFromHeader(@event);
var eventType = GetEventTypeFromHeader(@event);
var isFullSync = IsFullSync(eventType);
if (!Synced || eventType == NetworkEventType.FullSyncForced)
{
SyncToTimestamp(timestamp);
}
var index = (int)HeaderLength; // Skip header
ProgressSyncedTime(timestamp);
foreach (var obj in syncedObjects)
{
obj.SyncedUpdate();
}
foreach (var obj in syncedObjects)
{
var success = obj.SetSyncedData(@event, ref index, eventType);
if (!success)
{
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Malformed data reported by {obj.name} during event type {eventType}!");
HandleError(true);
return false;
}
}
var eventSize = GetEventSizeFromHeader(@event);
if (index != eventSize)
{
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Amount of data read does not match event size! Expected {eventSize}, read {index}.");
HandleError(true);
return false;
}
if (!Synced && isFullSync)
{
hasFullSyncReady = false;
Synced = true;
}
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Processed incoming event! Total {index} bytes.");
retriesWithoutSuccess = 0; // We had success!
return true;
}
public override void OnDeserialization()
{
if (!Ready || IsOwner)
{
return;
}
StoreIncomingData();
}
#endregion
#region Buffer
private void ClearBuffer()
{
eventsQueue = new byte[BufferMaxTotalEvents][];
eventsQueueIndex = 0;
eventTransmissionHistory = new int[maxEventSendTries];
eventTransmissionHistoryIndex = 0;
eventsQueueIndexAtLastTransmission = 0;
lastEventId = 0;
}
private void DequeueEventsFromBuffer(int eventCount)
{
if (eventCount > eventsQueueIndex) // Never remove more events than are in the queue
{
eventCount = eventsQueueIndex;
}
var oldBuffer = eventsQueue;
eventsQueueIndex -= eventCount;
eventsQueue = new byte[BufferMaxTotalEvents][];
Array.Copy(oldBuffer, eventCount, eventsQueue, 0, eventsQueueIndex);
}
private bool QueueEventInBuffer(byte[] @event)
{
if (eventsQueueIndex >= BufferMaxTotalEvents)
{
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Buffer not large enough to store event! Maximum event count: {BufferMaxTotalEvents}.");
HandleError(true);
return false;
}
eventsQueue[eventsQueueIndex++] = @event;
return true;
}
#endregion
#region Time
private void ProgressSyncedTime(float newTime)
{
//Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) updating SyncedTime from {SyncedTime} to {newTime}");
SyncedDeltaTime = newTime - SyncedTime;
if (SyncedDeltaTime < 0)
{
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Negative Dt: {SyncedDeltaTime}! Going from {SyncedTime} to {newTime}, IsEventUpdate: {IsEventUpdate}");
}
SyncedTime = newTime;
}
private void SyncToTimestamp(float timestamp)
{
var oldOffset = offsetTime;
offsetTime = Time.fixedTime - timestamp;
var delta = offsetTime - oldOffset;
internalTime -= delta;
SyncedTime -= delta;
nextEventTime -= delta;
Debug.Log($"({nameof(PacManUdon)} {nameof(NetworkManager)}) Synced to timestamp {timestamp}, current time is {Time.fixedTime}, offsetTime is now {offsetTime}, internalTime is now {internalTime}, SyncedTime is now {SyncedTime}, nextEventTime is now {nextEventTime}");
}
private void UpdateNextEventTime()
{
if (eventsQueueIndex == 0)
{
return;
}
var nextEventTime = GetTimestampFromHeader(eventsQueue[0]);
if (nextEventTime < this.nextEventTime)
{
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) New event is earlier than previous event!");
HandleError(true);
return;
}
if (nextEventTime < SyncedTime)
{
Debug.LogWarning($"({nameof(PacManUdon)} {nameof(NetworkManager)}) New event timestamp is earlier than our current synced time by {SyncedTime - nextEventTime} seconds! nextEventTime: {nextEventTime} SyncedTime: {SyncedTime}, internalTime: {internalTime}");
HandleError(true);
return;
}
this.nextEventTime = nextEventTime;
}
#endregion
#region Header
private static byte GetNextEventId(byte currentEventId)
{
if (currentEventId == byte.MaxValue) // Udon forces overflow checks
{
return 0;
}
currentEventId += 1;
return currentEventId;
}
private static bool IsFullSync(NetworkEventType eventType)
=> eventType == NetworkEventType.FullSync || eventType == NetworkEventType.FullSyncForced;
private static ushort GetEventSizeFromHeader(byte[] @event, int eventIndex = 0)
=> BitConverter.ToUInt16(@event, eventIndex + HeaderEventSizeIndex);
private static NetworkEventType GetEventTypeFromHeader(byte[] @event, int eventIndex = 0) =>
(NetworkEventType)@event[eventIndex + HeaderEventTypeIndex];
private static float GetTimestampFromHeader(byte[] @event, int eventIndex = 0) =>
BitConverter.ToSingle(@event, eventIndex + HeaderTimestampIndex);
private static byte GetEventIdFromHeader(byte[] @event, int eventIndex = 0) =>
@event[eventIndex + HeaderEventIdIndex];
#endregion
#region VRC events
public override void OnOwnershipTransferred(VRCPlayerApi newOwner)
{
if (!Ready)
{
return;
}
bool newOwnerIsLocalPlayer = newOwner == Networking.LocalPlayer;
SetOwner(newOwnerIsLocalPlayer);
if (newOwnerIsLocalPlayer)
{
SendEvent(NetworkEventType.FullSyncForced);
}
}
#endregion
#region Utils
public string ArrayToString(byte[] bytes)
{
var sb = new StringBuilder("[ ");
foreach (var b in bytes)
{
sb.Append(b + " ");
}
sb.Append("]");
return sb.ToString();
}
public string ArrayToString(int[] bytes)
{
var sb = new StringBuilder("[ ");
foreach (var b in bytes)
{
sb.Append(b + " ");
}
sb.Append("]");
return sb.ToString();
}
private static int GetFlattenedSize(byte[][] data, int start, int length)
{
var size = 0;
for (int i = start; i < start + length; i++)
{
size += data[i].Length;
}
return size;
}
private static byte[] Flatten(byte[][] data, int start, int length)
{
var finalLength = GetFlattenedSize(data, start, length);
var result = new byte[finalLength];
int resultIndex = 0;
for (int sourceIndex = start; sourceIndex < start + length; sourceIndex++)
{
var array = data[sourceIndex];
Array.Copy(array, 0, result, resultIndex, array.Length);
resultIndex += array.Length;
}
return result;
}
private static byte[] GetArrayPart(byte[] data, int start, int length)
{
var result = new byte[length];
Array.Copy(data, start, result, 0, length);
return result;
}
public static byte Int32ToByte(int value) =>
(byte)value; // Doing this inline causes an error...?
#endregion
#region Debug
public void SimulateSyncToTimestamp(float timestamp)
{
SyncToTimestamp(timestamp);
}
public void WriteDebugOutput(TMP_InputField debugOutput)
{
debugOutput.text += $"{nameof(NetworkManager)}:\n" +
$"IsOwner: {IsOwner}\n" +
$"Ready: {Ready}\n" +
$"Synced: {Synced}\n" +
$"hasFullSyncReady: {hasFullSyncReady}\n" +
$"lastEventId: {lastEventId}" +
$"Time.fixedTime: {Time.fixedTime}\n" +
$"offsetTime: {offsetTime}\n" +
$"internalTime: {internalTime}\n" +
$"SyncedTime: {SyncedTime}\n" +
$"Dt: {SyncedDeltaTime}\n" +
$"BufferIndex: {eventsQueueIndex}\n" +
$"BufferIndexHistory: {ArrayToString(eventTransmissionHistory)}\n" +
$"\n";
}
/// <summary>
/// An animator which visualizes whether the current perspective is the owner.
/// </summary>
[SerializeField] private Animator DebugImageToIndicateOwner;
public void DoFullSync()
{
SendEvent(NetworkEventType.FullSync);
}
#endregion
}
}