Last active
October 21, 2024 11:00
-
-
Save FREEZX/7b4eb1f5719f4ba0992e0a3bab36d3c7 to your computer and use it in GitHub Desktop.
FishNet Stopwatch that tries to account for client-server drift and only moves forward
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #if FISHNET_STABLE_MODE | |
| using FishNet.Documenting; | |
| using FishNet.Managing.Timing; | |
| using FishNet.Object.Synchronizing.Internal; | |
| using FishNet.Serializing; | |
| using System.Collections.Generic; | |
| using System.Runtime.CompilerServices; | |
| namespace FishNet.Object.Synchronizing | |
| { | |
| public class AdvancedSyncStopwatch : SyncBase, ICustomSync | |
| { | |
| #region Type. | |
| /// <summary> | |
| /// Information about how the Stopwatch has changed. | |
| /// </summary> | |
| private struct ChangeData | |
| { | |
| public readonly SyncStopwatchOperation Operation; | |
| public readonly float Previous; | |
| public readonly PreciseTick TickStart; | |
| public readonly float DriftTime; | |
| public ChangeData(SyncStopwatchOperation operation, float previous, PreciseTick tickStart, float driftTime) | |
| { | |
| Operation = operation; | |
| Previous = previous; | |
| TickStart = tickStart; | |
| DriftTime = driftTime; | |
| } | |
| } | |
| #endregion | |
| #region Public. | |
| /// <summary> | |
| /// Delegate signature for when the Stopwatch operation occurs. | |
| /// </summary> | |
| /// <param name="op">Operation which was performed.</param> | |
| /// <param name="prev">Previous value of the Stopwatch. This will be -1f is the value is not available.</param> | |
| /// <param name="asServer">True if occurring on server.</param> | |
| public delegate void SyncTypeChanged(SyncStopwatchOperation op, float prev, PreciseTick tickStart, float driftTime, bool asServer); | |
| /// <summary> | |
| /// Called when a Stopwatch operation occurs. | |
| /// </summary> | |
| public event SyncTypeChanged OnChange; | |
| /// <summary> | |
| /// How much time has passed since the Stopwatch started. | |
| /// </summary> | |
| public float ElapsedStart { get; private set; } = -1f; | |
| /// <summary> | |
| /// The server tick on which the stopwatch started, to sync in case of latency | |
| /// </summary> | |
| public PreciseTick TickStart { get; private set; } | |
| /// <summary> | |
| /// The offset time that accounts for pause or changed timescale | |
| /// </summary> | |
| public float DriftTime { get; private set; } | |
| /// <summary> | |
| /// True if the SyncStopwatch is currently paused. Calls to Update(float) will be ignored when paused. | |
| /// </summary> | |
| public bool Paused { get; private set; } | |
| /// <summary> | |
| /// Gets the real elapsed time by calculating the difference on the ticks to account for latency. | |
| /// </summary> | |
| public double Elapsed | |
| { | |
| get | |
| { | |
| if (ElapsedStart == -1f || base.NetworkManager == null || base.NetworkManager.TimeManager == null) | |
| return -1f; | |
| return elapsed; | |
| } | |
| } | |
| #endregion | |
| #region Private. | |
| protected double lastTimePassed; | |
| protected double lastElapsed; | |
| protected double elapsed; | |
| /// <summary> | |
| /// Changed data which will be sent next tick. | |
| /// </summary> | |
| private List<ChangeData> _changed = new List<ChangeData>(); | |
| /// <summary> | |
| /// Server OnChange events waiting for start callbacks. | |
| /// </summary> | |
| private List<ChangeData> _serverOnChanges = new List<ChangeData>(); | |
| /// <summary> | |
| /// Client OnChange events waiting for start callbacks. | |
| /// </summary> | |
| private List<ChangeData> _clientOnChanges = new List<ChangeData>(); | |
| #endregion | |
| #region Constructors | |
| public AdvancedSyncStopwatch(SyncTypeSettings settings = new SyncTypeSettings()) : base(settings) { } | |
| #endregion | |
| /// <summary> | |
| /// Called when the SyncType has been registered, but not yet initialized over the network. | |
| /// </summary> | |
| protected override void Initialized() | |
| { | |
| base.Initialized(); | |
| //Initialize collections if needed. OdinInspector can cause them to become deinitialized. | |
| #if ODIN_INSPECTOR | |
| if (_changed == null) _changed = new(); | |
| if (_serverOnChanges == null) _serverOnChanges = new(); | |
| if (_clientOnChanges == null) _clientOnChanges = new(); | |
| #endif | |
| } | |
| /// <summary> | |
| /// Starts a Stopwatch. If called when a Stopwatch is already active then StopStopwatch will automatically be sent. | |
| /// </summary> | |
| /// <param name="remaining">Time in which the Stopwatch should start with.</param> | |
| /// <param name="sendElapsedOnStop">True to include remaining time when automatically sending StopStopwatch.</param> | |
| public void StartStopwatch(bool sendElapsedOnStop = true) | |
| { | |
| if (!base.CanNetworkSetValues(true)) | |
| return; | |
| if (ElapsedStart > 0f) | |
| StopStopwatch(sendElapsedOnStop); | |
| lastTimePassed = 0; | |
| ElapsedStart = 0f; | |
| TickStart = base.NetworkManager.TimeManager.GetPreciseTick(TickType.Tick); | |
| AddOperation(SyncStopwatchOperation.Start, 0f, base.NetworkManager.TimeManager.GetPreciseTick(TickType.Tick), DriftTime); | |
| } | |
| /// <summary> | |
| /// Pauses the Stopwatch. Calling while already paused will be result in no action. | |
| /// </summary> | |
| /// <param name="sendElapsed">True to send Remaining with this operation.</param> | |
| public void PauseStopwatch(bool sendElapsed = false) | |
| { | |
| if (ElapsedStart < 0f) | |
| return; | |
| if (Paused) | |
| return; | |
| if (!base.CanNetworkSetValues(true)) | |
| return; | |
| Paused = true; | |
| float prev; | |
| SyncStopwatchOperation op; | |
| if (sendElapsed) | |
| { | |
| prev = ElapsedStart; | |
| op = SyncStopwatchOperation.PauseUpdated; | |
| } | |
| else | |
| { | |
| prev = -1f; | |
| op = SyncStopwatchOperation.Pause; | |
| } | |
| AddOperation(op, prev, TickStart, DriftTime); | |
| } | |
| /// <summary> | |
| /// Unpauses the Stopwatch. Calling while already unpaused will be result in no action. | |
| /// </summary> | |
| public void UnpauseStopwatch() | |
| { | |
| if (ElapsedStart < 0f) | |
| return; | |
| if (!Paused) | |
| return; | |
| if (!base.CanNetworkSetValues(true)) | |
| return; | |
| Paused = false; | |
| AddOperation(SyncStopwatchOperation.Unpause, -1f, TickStart, DriftTime); | |
| } | |
| /// <summary> | |
| /// Stops and resets the Stopwatch. | |
| /// </summary> | |
| public void StopStopwatch(bool sendElapsed = false) | |
| { | |
| if (ElapsedStart < 0f) | |
| return; | |
| if (!base.CanNetworkSetValues(true)) | |
| return; | |
| float prev = (sendElapsed) ? -1f : ElapsedStart; | |
| StopStopwatch_Internal(true); | |
| SyncStopwatchOperation op = (sendElapsed) ? SyncStopwatchOperation.StopUpdated : SyncStopwatchOperation.Stop; | |
| AddOperation(op, prev, new PreciseTick(0, 0), DriftTime); | |
| } | |
| /// <summary> | |
| /// Adds an operation to synchronize. | |
| /// </summary> | |
| private void AddOperation(SyncStopwatchOperation operation, float prev, PreciseTick tickStart, float driftTime) | |
| { | |
| if (!base.IsInitialized) | |
| return; | |
| bool asServerInvoke = !base.IsNetworkInitialized || base.NetworkBehaviour.IsServerStarted; | |
| if (asServerInvoke) | |
| { | |
| if (base.Dirty()) | |
| { | |
| ChangeData change = new ChangeData(operation, prev, tickStart, driftTime); | |
| _changed.Add(change); | |
| } | |
| } | |
| OnChange?.Invoke(operation, prev, tickStart, driftTime, asServerInvoke); | |
| } | |
| /// <summary> | |
| /// Writes all changed values. | |
| /// </summary> | |
| ///<param name="resetSyncTick">True to set the next time data may sync.</param> | |
| internal protected override void WriteDelta(PooledWriter writer, bool resetSyncTick = true) | |
| { | |
| base.WriteDelta(writer, resetSyncTick); | |
| writer.WriteInt32(_changed.Count); | |
| for (int i = 0; i < _changed.Count; i++) | |
| { | |
| ChangeData change = _changed[i]; | |
| writer.WriteUInt8Unpacked((byte)change.Operation); | |
| if (change.Operation == SyncStopwatchOperation.Start) | |
| WriteStartStopwatch(writer, 0f, TickStart, DriftTime, false); | |
| //Pause and unpause updated need current value written. | |
| //Updated stop also writes current value. | |
| else if (change.Operation == SyncStopwatchOperation.Pause || change.Operation == SyncStopwatchOperation.Unpause) | |
| { | |
| writer.WriteSingle(DriftTime); | |
| } | |
| else if (change.Operation == SyncStopwatchOperation.PauseUpdated || change.Operation == SyncStopwatchOperation.StopUpdated) | |
| { | |
| writer.WriteSingle(change.Previous); | |
| writer.WriteSingle(change.DriftTime); | |
| } | |
| } | |
| _changed.Clear(); | |
| } | |
| /// <summary> | |
| /// Writes all values. | |
| /// </summary> | |
| internal protected override void WriteFull(PooledWriter writer) | |
| { | |
| //Only write full if a Stopwatch is running. | |
| if (ElapsedStart < 0f) | |
| return; | |
| base.WriteDelta(writer, false); | |
| //There will be 1 or 2 entries. If paused 2, if not 1. | |
| int entries = (Paused) ? 2 : 1; | |
| writer.WriteInt32(entries); | |
| //And the operations. | |
| WriteStartStopwatch(writer, ElapsedStart, TickStart, DriftTime, true); | |
| if (Paused) | |
| { | |
| writer.WriteUInt8Unpacked((byte)SyncStopwatchOperation.Pause); | |
| writer.WriteSingle(DriftTime); | |
| } | |
| } | |
| /// <summary> | |
| /// Writers a start with elapsed time. | |
| /// </summary> | |
| /// <param name="elapsed"></param> | |
| private void WriteStartStopwatch(Writer w, float elapsed, PreciseTick tickStart, float driftTime, bool includeOperationByte) | |
| { | |
| if (includeOperationByte) | |
| w.WriteUInt8Unpacked((byte)SyncStopwatchOperation.Start); | |
| w.WriteSingle(elapsed); | |
| w.WritePreciseTick(tickStart); | |
| w.WriteSingle(driftTime); | |
| } | |
| /// <summary> | |
| /// Reads and sets the current values for server or client. | |
| /// </summary> | |
| [APIExclude] | |
| internal protected override void Read(PooledReader reader, bool asServer) | |
| { | |
| base.SetReadArguments(reader, asServer, out bool newChangeId, out bool asClientHost, out bool canModifyValues); | |
| int changes = reader.ReadInt32(); | |
| for (int i = 0; i < changes; i++) | |
| { | |
| SyncStopwatchOperation op = (SyncStopwatchOperation)reader.ReadUInt8Unpacked(); | |
| if (op == SyncStopwatchOperation.Start) | |
| { | |
| float elapsed = reader.ReadSingle(); | |
| PreciseTick tickStart = reader.ReadPreciseTick(); | |
| float driftTime = reader.ReadSingle(); | |
| lastTimePassed = elapsed; | |
| if (canModifyValues) | |
| { | |
| ElapsedStart = elapsed; | |
| TickStart = tickStart; | |
| DriftTime = driftTime; | |
| } | |
| if (newChangeId) | |
| InvokeOnChange(op, elapsed, tickStart, driftTime, asServer); | |
| } | |
| else if (op == SyncStopwatchOperation.Pause) | |
| { | |
| float driftTime = reader.ReadSingle(); | |
| if (canModifyValues) | |
| { | |
| Paused = true; | |
| DriftTime = driftTime; | |
| } | |
| if (newChangeId) | |
| InvokeOnChange(op, -1f, TickStart, driftTime, asServer); | |
| } | |
| else if (op == SyncStopwatchOperation.PauseUpdated) | |
| { | |
| float prev = reader.ReadSingle(); | |
| float driftTime = reader.ReadSingle(); | |
| if (canModifyValues) | |
| { | |
| Paused = true; | |
| DriftTime = driftTime; | |
| } | |
| if (newChangeId) | |
| InvokeOnChange(op, prev, TickStart, driftTime, asServer); | |
| } | |
| else if (op == SyncStopwatchOperation.Unpause) | |
| { | |
| float driftTime = reader.ReadSingle(); | |
| if (canModifyValues) | |
| { | |
| Paused = false; | |
| DriftTime = driftTime; | |
| } | |
| if (newChangeId) | |
| InvokeOnChange(op, -1f, TickStart, driftTime, asServer); | |
| } | |
| else if (op == SyncStopwatchOperation.Stop) | |
| { | |
| lastTimePassed = 0; | |
| if (canModifyValues) | |
| StopStopwatch_Internal(asServer); | |
| if (newChangeId) | |
| InvokeOnChange(op, -1f, new PreciseTick(0, 0), 0f, false); | |
| } | |
| else if (op == SyncStopwatchOperation.StopUpdated) | |
| { | |
| float prev = reader.ReadSingle(); | |
| float driftTime = reader.ReadSingle(); | |
| lastTimePassed = prev; | |
| if (canModifyValues) | |
| { | |
| ElapsedStart = prev; | |
| DriftTime = driftTime; | |
| StopStopwatch_Internal(asServer); | |
| } | |
| if (newChangeId) | |
| InvokeOnChange(op, prev, TickStart, driftTime, asServer); | |
| } | |
| } | |
| if (newChangeId && changes > 0) | |
| InvokeOnChange(SyncStopwatchOperation.Complete, -1f, new PreciseTick(0, 0), 0f, asServer); | |
| } | |
| /// <summary> | |
| /// Stops the Stopwatch and resets. | |
| /// </summary> | |
| private void StopStopwatch_Internal(bool asServer) | |
| { | |
| Paused = false; | |
| ElapsedStart = -1f; | |
| } | |
| /// <summary> | |
| /// Invokes OnChanged callback. | |
| /// </summary> | |
| private void InvokeOnChange(SyncStopwatchOperation operation, float prev, PreciseTick tickStart, float driftTime, bool asServer) | |
| { | |
| if (asServer) | |
| { | |
| if (base.NetworkBehaviour.OnStartServerCalled) | |
| OnChange?.Invoke(operation, prev, tickStart, driftTime, asServer); | |
| else | |
| _serverOnChanges.Add(new ChangeData(operation, prev, tickStart, driftTime)); | |
| } | |
| else | |
| { | |
| if (base.NetworkBehaviour.OnStartClientCalled) | |
| OnChange?.Invoke(operation, prev, tickStart, driftTime, asServer); | |
| else | |
| _clientOnChanges.Add(new ChangeData(operation, prev, tickStart, driftTime)); | |
| } | |
| } | |
| /// <summary> | |
| /// Called after OnStartXXXX has occurred. | |
| /// </summary> | |
| /// <param name="asServer">True if OnStartServer was called, false if OnStartClient.</param> | |
| internal protected override void OnStartCallback(bool asServer) | |
| { | |
| base.OnStartCallback(asServer); | |
| List<ChangeData> collection = (asServer) ? _serverOnChanges : _clientOnChanges; | |
| if (OnChange != null) | |
| { | |
| foreach (ChangeData item in collection) | |
| OnChange.Invoke(item.Operation, item.Previous, item.TickStart, item.DriftTime, asServer); | |
| } | |
| collection.Clear(); | |
| } | |
| /// <summary> | |
| /// Adds delta from Remaining for server and client. | |
| /// </summary> | |
| /// <param name="delta">Value to remove from Remaining.</param> | |
| public void Update(float delta) | |
| { | |
| //Not enabled. | |
| if (ElapsedStart == -1f) | |
| return; | |
| if (Paused) | |
| { | |
| DriftTime += delta; | |
| return; | |
| } | |
| var timeManager = base.NetworkManager.TimeManager; | |
| float roundTrip = !base.NetworkManager.IsServerStarted ? timeManager.RoundTripTime / 2000f : 0f; | |
| double timePassed = timeManager.TimePassed(TickStart) + timeManager.GetTickPercentAsDouble() * timeManager.TickDelta; | |
| if(timePassed < lastTimePassed) { | |
| timePassed = lastTimePassed; | |
| } | |
| elapsed = ElapsedStart + timePassed - roundTrip - DriftTime; | |
| if(elapsed < lastElapsed) { | |
| elapsed = lastElapsed; | |
| } | |
| lastTimePassed = timePassed; | |
| if (UnityEngine.Time.timeScale != 1) | |
| { | |
| DriftTime += UnityEngine.Time.unscaledDeltaTime - delta; | |
| } | |
| } | |
| /// <summary> | |
| /// Return the serialized type. | |
| /// </summary> | |
| /// <returns></returns> | |
| public object GetSerializedType() => null; | |
| } | |
| } | |
| #endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment