remoteTime based on first snapshot.timestamp += deltaTime instead of sequence ushort

This commit is contained in:
vis2k 2021-03-17 10:35:25 +08:00
parent a91604d109
commit b9a6ef38bb
3 changed files with 103 additions and 63 deletions

View File

@ -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 its older then its 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 Ive 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();
}
}

View File

@ -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 its older then its 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;

View File

@ -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<BufferEntry> queue = new Queue<BufferEntry>();
Queue<Snapshot> queue = new Queue<Snapshot>();
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;