From b9a6ef38bb7b38537cfc4fe2255d1bd7c89751bb Mon Sep 17 00:00:00 2001 From: vis2k Date: Wed, 17 Mar 2021 10:35:25 +0800 Subject: [PATCH] remoteTime based on first snapshot.timestamp += deltaTime instead of sequence ushort --- .../Experimental/Oumuamua/OumuamuaBase.cs | 100 ++++++++++++------ .../Experimental/Oumuamua/Snapshot.cs | 26 +++-- .../Experimental/Oumuamua/SnapshotBuffer.cs | 40 +++---- 3 files changed, 103 insertions(+), 63 deletions(-) diff --git a/Assets/Mirror/Components/Experimental/Oumuamua/OumuamuaBase.cs b/Assets/Mirror/Components/Experimental/Oumuamua/OumuamuaBase.cs index 0e2ca3a12..3bed918bd 100644 --- a/Assets/Mirror/Components/Experimental/Oumuamua/OumuamuaBase.cs +++ b/Assets/Mirror/Components/Experimental/Oumuamua/OumuamuaBase.cs @@ -25,15 +25,26 @@ public abstract class OumuamuaBase : NetworkBehaviour float lastClientSendTime; float lastServerSendTime; - // "When we send snapshot data in packets, we include at the top a 16 bit - // sequence number. This sequence number starts at zero and increases - // with each packet sent. We use this sequence number on receive to - // determine if the snapshot in a packet is newer or older than the most - // recent snapshot received. If it’s older then it’s thrown away." - ushort serverSendSequence; - ushort clientSendSequence; - ushort serverReceivedSequence; - ushort clientReceivedSequence; + // keep track of last received timestamps and throw out any snapshots + // older than that (according to the article). + // TODO seems like any snapshot that still fits in the buffer before we + // process it seems worth keeping? + // TODO consider double for precision over days + float serverLastReceivedTimestamp; + float clientLastReceivedTimestamp; + + // snapshot timestamps are _remote_ time + // we need to interpolate and calculate buffer lifetimes based on it. + // -> we don't know remote's current time + // -> NetworkTime.time fluctuates too much, that's no good + // -> we _could_ calculate an offset when the first snapshot arrives, + // but if there was high latency then we'll always calculate time + // with high latency + // -> at any given time, we are interpolating from snapshot A to B + // => seems like A.timestamp += deltaTime is a good way to do it + // => let's store it in two variables: + float serverRemoteClientTime; + float clientRemoteServerTime; // "Experimentally I’ve found that the amount of delay that works best // at 2-5% packet loss is 3X the packet send rate" @@ -53,11 +64,11 @@ void CmdClientToServerSync(Snapshot snapshot) if (clientAuthority) { // newer than most recent received snapshot? - if (snapshot.sequence > serverReceivedSequence) + if (snapshot.timestamp > serverLastReceivedTimestamp) { // add to buffer - serverBuffer.Enqueue(snapshot, Time.time); - serverReceivedSequence = snapshot.sequence; + serverBuffer.Enqueue(snapshot); + serverLastReceivedTimestamp = snapshot.timestamp; } } } @@ -70,11 +81,11 @@ void RpcServerToClientSync(Snapshot snapshot) if (!IsClientWithAuthority) { // newer than most recent received snapshot? - if (snapshot.sequence > clientReceivedSequence) + if (snapshot.timestamp > clientLastReceivedTimestamp) { // add to buffer - clientBuffer.Enqueue(snapshot, Time.time); - clientReceivedSequence = snapshot.sequence; + clientBuffer.Enqueue(snapshot); + clientLastReceivedTimestamp = snapshot.timestamp; } } } @@ -91,17 +102,47 @@ void ApplySnapshot(Snapshot snapshot) // helper function to apply snapshots. // we use the same one on server and client. // => called every Update() depending on authority. - void ApplySnapshots(SnapshotBuffer buffer) + void ApplySnapshots(ref float remoteTime, SnapshotBuffer buffer) { Debug.Log($"{name} snapshotbuffer={buffer.Count}"); + // we buffer snapshots for 'bufferTime' // for example: // * we buffer for 3 x sendInterval = 300ms // * the idea is to wait long enough so we at least have a few // snapshots to interpolate between // * we process anything older 100ms immediately - if (buffer.DequeueIfOldEnough(Time.time, bufferTime, out Snapshot snapshot)) + // + // IMPORTANT: snapshot timestamps are _remote_ time + // we need to interpolate and calculate buffer lifetimes based on it. + // -> we don't know remote's current time + // -> NetworkTime.time fluctuates too much, that's no good + // -> we _could_ calculate an offset when the first snapshot arrives, + // but if there was high latency then we'll always calculate time + // with high latency + // -> at any given time, we are interpolating from snapshot A to B + // => seems like A.timestamp += deltaTime is a good way to do it + + // if remote time wasn't initialized yet + if (remoteTime == 0) + { + // then set it to first snapshot received (if any) + if (buffer.Peek(out Snapshot first)) + { + remoteTime = first.timestamp; + Debug.LogWarning("remoteTime initialized to " + first.timestamp); + } + // otherwise wait for the first one + else return; + } + + // move remote time along deltaTime + // TODO consider double for precision over days + // (probably need to speed this up based on buffer size later) + remoteTime += Time.deltaTime; + + if (buffer.DequeueIfOldEnough(remoteTime, bufferTime, out Snapshot snapshot)) ApplySnapshot(snapshot); } @@ -114,9 +155,8 @@ void Update() // (client with authority will drop the rpc) if (Time.time >= lastServerSendTime + sendInterval) { - ++serverSendSequence; Snapshot snapshot = new Snapshot( - serverSendSequence, + Time.time, targetComponent.localPosition, targetComponent.localRotation, targetComponent.localScale @@ -135,7 +175,7 @@ void Update() if (clientAuthority && !isLocalPlayer) { // apply snapshots - ApplySnapshots(serverBuffer); + ApplySnapshots(ref serverRemoteClientTime, serverBuffer); } } // 'else if' because host mode shouldn't send anything to server. @@ -148,9 +188,8 @@ void Update() // send to server each 'sendInterval' if (Time.time >= lastClientSendTime + sendInterval) { - ++clientSendSequence; Snapshot snapshot = new Snapshot( - clientSendSequence, + Time.time, targetComponent.localPosition, targetComponent.localRotation, targetComponent.localScale @@ -165,25 +204,24 @@ void Update() else { // apply snapshots - ApplySnapshots(clientBuffer); + ApplySnapshots(ref clientRemoteServerTime, clientBuffer); } } } - void OnDisable() + void Reset() { // disabled objects aren't updated anymore. // so let's clear the buffers. serverBuffer.Clear(); clientBuffer.Clear(); + + // and reset remoteTime so it's initialized to first snapshot again + clientRemoteServerTime = 0; + serverRemoteClientTime = 0; } - void OnEnable() - { - // just in case we received anything while disabled... - // it's outdated now anyway. clear the buffers. - serverBuffer.Clear(); - clientBuffer.Clear(); - } + void OnDisable() => Reset(); + void OnEnable() => Reset(); } } diff --git a/Assets/Mirror/Components/Experimental/Oumuamua/Snapshot.cs b/Assets/Mirror/Components/Experimental/Oumuamua/Snapshot.cs index 1d7d0187e..6617bdebc 100644 --- a/Assets/Mirror/Components/Experimental/Oumuamua/Snapshot.cs +++ b/Assets/Mirror/Components/Experimental/Oumuamua/Snapshot.cs @@ -7,20 +7,30 @@ namespace Mirror.Experimental { internal struct Snapshot { - // "When we send snapshot data in packets, we include at the top a 16 bit - // sequence number. This sequence number starts at zero and increases - // with each packet sent. We use this sequence number on receive to - // determine if the snapshot in a packet is newer or older than the most - // recent snapshot received. If it’s older then it’s thrown away." - internal ushort sequence; + // time or sequence are needed to throw away older snapshots. + // + // glenn fiedler starts with a 16 bit sequence number. + // supposedly this is meant as a simplified example. + // in the end we need the remote timestamp for accurate interpolation + // and buffering over time. + // + // note: in theory, IF server sends exactly(!) at the same interval then + // the 16 bit ushort timestamp would be enough to calculate the + // remote time (sequence * sendInterval). but Unity's update is + // not guaranteed to run on the exact intervals / do catchup. + // => remote timestamp is better for now + // TODO consider double for precision over days + // + // [REMOTE TIME, NOT LOCAL TIME] + internal float timestamp; internal Vector3 position; internal Quaternion rotation; internal Vector3 scale; - internal Snapshot(ushort sequence, Vector3 position, Quaternion rotation, Vector3 scale) + internal Snapshot(float timestamp, Vector3 position, Quaternion rotation, Vector3 scale) { - this.sequence = sequence; + this.timestamp = timestamp; this.position = position; this.rotation = rotation; this.scale = scale; diff --git a/Assets/Mirror/Components/Experimental/Oumuamua/SnapshotBuffer.cs b/Assets/Mirror/Components/Experimental/Oumuamua/SnapshotBuffer.cs index 5f560c16f..ff38e15f8 100644 --- a/Assets/Mirror/Components/Experimental/Oumuamua/SnapshotBuffer.cs +++ b/Assets/Mirror/Components/Experimental/Oumuamua/SnapshotBuffer.cs @@ -2,29 +2,11 @@ namespace Mirror.Experimental { - // need to store snapshots with timestamp. - // can't put .timestamp into snapshot because we don't want to sync it. - internal struct BufferEntry - { - internal Snapshot snapshot; - internal float timestamp; - - internal BufferEntry(Snapshot snapshot, float timestamp) - { - this.snapshot = snapshot; - this.timestamp = timestamp; - } - } - internal class SnapshotBuffer { - Queue queue = new Queue(); + Queue queue = new Queue(); - internal void Enqueue(Snapshot snapshot, float timestamp) - { - BufferEntry entry = new BufferEntry(snapshot, timestamp); - queue.Enqueue(entry); - } + internal void Enqueue(Snapshot snapshot) => queue.Enqueue(snapshot); // dequeue the first snapshot if it's older enough. // for example, currentTime = 100, bufferInterval = 0.3 @@ -37,11 +19,9 @@ internal bool DequeueIfOldEnough(float currentTime, float bufferInterval, out Sn float thresholdTime = currentTime - bufferInterval; // peek and compare time - BufferEntry entry = queue.Peek(); - if (entry.timestamp <= thresholdTime) + if (queue.Peek().timestamp <= thresholdTime) { - snapshot = entry.snapshot; - queue.Dequeue(); + snapshot = queue.Dequeue(); return true; } } @@ -49,6 +29,18 @@ internal bool DequeueIfOldEnough(float currentTime, float bufferInterval, out Sn return false; } + // peek + internal bool Peek(out Snapshot snapshot) + { + if (queue.Count > 0) + { + snapshot = queue.Peek(); + return true; + } + snapshot = default; + return false; + } + // count queue size independent of time internal int Count => queue.Count;