Files
survival-game/Assets/Mirror/Core/NetworkBehaviourHybrid.cs
2025-06-16 15:14:23 +02:00

484 lines
22 KiB
C#

// base class for "Hybrid" sync components.
// inspired by the Quake networking model, but made to scale.
// https://www.jfedor.org/quake3/
using System;
using UnityEngine;
namespace Mirror
{
public abstract class NetworkBehaviourHybrid : NetworkBehaviour
{
// Is this a client with authority over this transform?
// This component could be on the player object or any object that has been assigned authority to this client.
protected bool IsClientWithAuthority => isClient && authority;
[Tooltip("Occasionally send a full reliable state to delta compress against. This only applies to Components with SyncMethod=Unreliable.")]
public int baselineRate = 1;
public float baselineInterval => baselineRate < int.MaxValue ? 1f / baselineRate : 0; // for 1 Hz, that's 1000ms
protected double lastBaselineTime;
protected double lastDeltaTime;
// delta compression needs to remember 'last' to compress against.
byte lastSerializedBaselineTick = 0;
byte lastDeserializedBaselineTick = 0;
[Tooltip("Enable to send all unreliable messages twice. Only useful for extremely fast-paced games since it doubles bandwidth costs.")]
public bool unreliableRedundancy = false;
[Tooltip("When sending a reliable baseline, should we also send an unreliable delta or rely on the reliable baseline to arrive in a similar time?")]
public bool baselineIsDelta = true;
// change detection: we need to do this carefully in order to get it right.
//
// DONT just check changes in UpdateBaseline(). this would introduce MrG's grid issue:
// server start in A1, reliable baseline sent to client
// server moves to A2, unreliabe delta sent to client
// server moves to A1, nothing is sent to client becuase last baseline position == position
// => client wouldn't know we moved back to A1
//
// INSTEAD: every update() check for changes since baseline:
// UpdateDelta() keeps sending only if changed since _baseline_
// UpdateBaseline() resends if there was any change in the period since last baseline.
// => this avoids the A1->A2->A1 grid issue above
bool changedSinceBaseline = false;
[Header("Debug")]
public bool debugLog = false;
public virtual void ResetState()
{
lastSerializedBaselineTick = 0;
lastDeserializedBaselineTick = 0;
changedSinceBaseline = false;
}
// user callbacks //////////////////////////////////////////////////////
protected abstract void OnSerializeBaseline(NetworkWriter writer);
protected abstract void OnDeserializeBaseline(NetworkReader reader, byte baselineTick);
protected abstract void OnSerializeDelta(NetworkWriter writer);
protected abstract void OnDeserializeDelta(NetworkReader reader, byte baselineTick);
// implementations must store the current baseline state when requested:
// - implementations can use this to compress deltas against
// - implementations can use this to detect changes since baseline
// this is called whenever a baseline was sent.
protected abstract void StoreState();
// implementations may compare current state to the last stored state.
// this way we only need to send another reliable baseline if changed since last.
// this is called every syncInterval, not every baseline sync interval.
// (see comments where this is called).
protected abstract bool StateChanged();
// user callback in case drops due to baseline mismatch need to be logged/visualized/debugged.
protected virtual void OnDrop(byte lastBaselineTick, byte baselineTick, NetworkReader reader) {}
// rpcs / cmds /////////////////////////////////////////////////////////
// reliable baseline.
// include owner in case of server authority.
[ClientRpc(channel = Channels.Reliable)]
void RpcServerToClientBaseline(ArraySegment<byte> data)
{
// baseline is broadcast to all clients.
// ignore if this object is owned by this client.
if (IsClientWithAuthority) return;
// host mode: baseline Rpc is also sent through host's local connection and applied.
// applying host's baseline as last deserialized would overwrite the owner client's data and cause jitter.
// in other words: never apply the rpcs in host mode.
if (isServer) return;
using (NetworkReaderPooled reader = NetworkReaderPool.Get(data))
{
// deserialize
// save last deserialized baseline tick number to compare deltas against
lastDeserializedBaselineTick = reader.ReadByte();
OnDeserializeBaseline(reader, lastDeserializedBaselineTick);
}
}
// unreliable delta.
// include owner in case of server authority.
[ClientRpc(channel = Channels.Unreliable)]
void RpcServerToClientDelta(ArraySegment<byte> data)
{
// delta is broadcast to all clients.
// ignore if this object is owned by this client.
if (IsClientWithAuthority) return;
// host mode: baseline Rpc is also sent through host's local connection and applied.
// applying host's baseline as last deserialized would overwrite the owner client's data and cause jitter.
// in other words: never apply the rpcs in host mode.
if (isServer) return;
// deserialize
using (NetworkReaderPooled reader = NetworkReaderPool.Get(data))
{
// deserialize
byte baselineTick = reader.ReadByte();
// ensure this delta is for our last known baseline.
// we should never apply a delta on top of a wrong baseline.
if (baselineTick != lastDeserializedBaselineTick)
{
OnDrop(lastDeserializedBaselineTick, baselineTick, reader);
// this can happen if unreliable arrives before reliable etc.
// no need to log this except when debugging.
if (debugLog) Debug.Log($"[{name}] Client: received delta for wrong baseline #{baselineTick}. Last was {lastDeserializedBaselineTick}. Ignoring.");
return;
}
OnDeserializeDelta(reader, baselineTick);
}
}
[Command(channel = Channels.Reliable)] // reliable baseline
void CmdClientToServerBaseline(ArraySegment<byte> data)
{
// deserialize
using (NetworkReaderPooled reader = NetworkReaderPool.Get(data))
{
// deserialize
lastDeserializedBaselineTick = reader.ReadByte();
OnDeserializeBaseline(reader, lastDeserializedBaselineTick);
}
}
[Command(channel = Channels.Unreliable)] // unreliable delta
void CmdClientToServerDelta(ArraySegment<byte> data)
{
using (NetworkReaderPooled reader = NetworkReaderPool.Get(data))
{
// deserialize
byte baselineTick = reader.ReadByte();
// ensure this delta is for our last known baseline.
// we should never apply a delta on top of a wrong baseline.
if (baselineTick != lastDeserializedBaselineTick)
{
OnDrop(lastDeserializedBaselineTick, baselineTick, reader);
// this can happen if unreliable arrives before reliable etc.
// no need to log this except when debugging.
if (debugLog) Debug.Log($"[{name}] Server: received delta for wrong baseline #{baselineTick} from: {connectionToClient}. Last was {lastDeserializedBaselineTick}. Ignoring.");
return;
}
OnDeserializeDelta(reader, baselineTick);
}
}
// update server ///////////////////////////////////////////////////////
protected virtual void UpdateServerBaseline(double localTime)
{
// send a reliable baseline every 1 Hz
if (localTime < lastBaselineTime + baselineInterval) return;
// only sync if changed since last reliable baseline
if (!changedSinceBaseline) return;
// save bandwidth by only transmitting what is needed.
// -> ArraySegment with random data is slower since byte[] copying
// -> Vector3? and Quaternion? nullables takes more bandwidth
byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once!
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
// serialize
writer.WriteByte(frameCount);
OnSerializeBaseline(writer);
// send (no need for redundancy since baseline is reliable)
RpcServerToClientBaseline(writer);
}
// save the last baseline's tick number.
// included in baseline to identify which one it was on client
// included in deltas to ensure they are on top of the correct baseline
lastSerializedBaselineTick = frameCount;
lastBaselineTime = NetworkTime.localTime;
// perf. & bandwidth optimization:
// send a delta right after baseline to avoid potential head of
// line blocking, or skip the delta whenever we sent reliable?
// for example:
// 1 Hz baseline
// 10 Hz delta
// => 11 Hz total if we still send delta after reliable
// => 10 Hz total if we skip delta after reliable
// in that case, skip next delta by simply resetting last delta sync's time.
if (baselineIsDelta) lastDeltaTime = localTime;
// request to store last baseline state (i.e. position) for change detection.
StoreState();
// baseline was just sent after a change. reset change detection.
changedSinceBaseline = false;
if (debugLog) Debug.Log($"[{name}] Server: sent baseline #{lastSerializedBaselineTick} to: {connectionToClient} at time: {localTime}");
}
protected virtual void UpdateServerDelta(double localTime)
{
// broadcast to all clients each 'sendInterval'
// (client with authority will drop the rpc)
// NetworkTime.localTime for double precision until Unity has it too
//
// IMPORTANT:
// snapshot interpolation requires constant sending.
// DO NOT only send if position changed. for example:
// ---
// * client sends first position at t=0
// * ... 10s later ...
// * client moves again, sends second position at t=10
// ---
// * server gets first position at t=0
// * server gets second position at t=10
// * server moves from first to second within a time of 10s
// => would be a super slow move, instead of a wait & move.
//
// IMPORTANT:
// DO NOT send nulls if not changed 'since last send' either. we
// send unreliable and don't know which 'last send' the other end
// received successfully.
//
// Checks to ensure server only sends snapshots if object is
// on server authority(!clientAuthority) mode because on client
// authority mode snapshots are broadcasted right after the authoritative
// client updates server in the command function(see above), OR,
// since host does not send anything to update the server, any client
// authoritative movement done by the host will have to be broadcasted
// here by checking IsClientWithAuthority.
// TODO send same time that NetworkServer sends time snapshot?
if (localTime < lastDeltaTime + syncInterval) return;
// look for changes every unreliable sendInterval!
// every reliable interval isn't enough, this would cause MrG's grid issue:
// server start in A1, reliable baseline sent to clients
// server moves to A2, unreliabe delta sent to clients
// server moves back to A1, nothing is sent to clients because last baseline position == position
// => clients wouldn't know we moved back to A1
// every update works, but it's unnecessary overhead since sends only happen every sendInterval
// every unreliable sendInterval is the perfect place to look for changes.
if (StateChanged()) changedSinceBaseline = true;
// only sync on change:
// unreliable isn't guaranteed to be delivered so this depends on reliable baseline.
if (!changedSinceBaseline) return;
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
// serialize
writer.WriteByte(lastSerializedBaselineTick);
OnSerializeDelta(writer);
// send (with optional redundancy to make up for message drops)
RpcServerToClientDelta(writer);
if (unreliableRedundancy)
RpcServerToClientDelta(writer);
}
lastDeltaTime = localTime;
if (debugLog) Debug.Log($"[{name}] Server: sent delta for #{lastSerializedBaselineTick} to: {connectionToClient} at time: {localTime}");
}
protected virtual void UpdateServerSync()
{
// server broadcasts all objects all the time.
// -> not just ServerToClient: ClientToServer need to be broadcast to others too
// perf: only grab NetworkTime.localTime property once.
double localTime = NetworkTime.localTime;
// broadcast
UpdateServerBaseline(localTime);
UpdateServerDelta(localTime);
}
// update client ///////////////////////////////////////////////////////
protected virtual void UpdateClientBaseline(double localTime)
{
// send a reliable baseline every 1 Hz
if (localTime < lastBaselineTime + baselineInterval) return;
// only sync if changed since last reliable baseline
if (!changedSinceBaseline) return;
// save bandwidth by only transmitting what is needed.
// -> ArraySegment with random data is slower since byte[] copying
// -> Vector3? and Quaternion? nullables takes more bandwidth
byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once!
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
// serialize
writer.WriteByte(frameCount);
OnSerializeBaseline(writer);
// send (no need for redundancy since baseline is reliable)
CmdClientToServerBaseline(writer);
}
// save the last baseline's tick number.
// included in baseline to identify which one it was on client
// included in deltas to ensure they are on top of the correct baseline
lastSerializedBaselineTick = frameCount;
lastBaselineTime = NetworkTime.localTime;
// perf. & bandwidth optimization:
// send a delta right after baseline to avoid potential head of
// line blocking, or skip the delta whenever we sent reliable?
// for example:
// 1 Hz baseline
// 10 Hz delta
// => 11 Hz total if we still send delta after reliable
// => 10 Hz total if we skip delta after reliable
// in that case, skip next delta by simply resetting last delta sync's time.
if (baselineIsDelta) lastDeltaTime = localTime;
// request to store last baseline state (i.e. position) for change detection.
// IMPORTANT
// OnSerialize(initial) is called for the spawn payload whenever
// someone starts observing this object. we always must make
// this the new baseline, otherwise this happens:
// - server broadcasts baseline @ t=1
// - server broadcasts delta for baseline @ t=1
// - ... time passes ...
// - new observer -> OnSerialize sends current position @ t=2
// - server broadcasts delta for baseline @ t=1
// => client's baseline is t=2 but receives delta for t=1 _!_
StoreState();
// baseline was just sent after a change. reset change detection.
changedSinceBaseline = false;
if (debugLog) Debug.Log($"[{name}] Client: sent baseline #{lastSerializedBaselineTick} at time: {localTime}");
}
protected virtual void UpdateClientDelta(double localTime)
{
// send to server each 'sendInterval'
// NetworkTime.localTime for double precision until Unity has it too
//
// IMPORTANT:
// snapshot interpolation requires constant sending.
// DO NOT only send if position changed. for example:
// ---
// * client sends first position at t=0
// * ... 10s later ...
// * client moves again, sends second position at t=10
// ---
// * server gets first position at t=0
// * server gets second position at t=10
// * server moves from first to second within a time of 10s
// => would be a super slow move, instead of a wait & move.
//
// IMPORTANT:
// DO NOT send nulls if not changed 'since last send' either. we
// send unreliable and don't know which 'last send' the other end
// received successfully.
if (localTime < lastDeltaTime + syncInterval) return;
// look for changes every unreliable sendInterval!
// every reliable interval isn't enough, this would cause MrG's grid issue:
// client start in A1, reliable baseline sent to server and other clients
// client moves to A2, unreliabe delta sent to server and other clients
// client moves back to A1, nothing is sent to server because last baseline position == position
// => server / other clients wouldn't know we moved back to A1
// every update works, but it's unnecessary overhead since sends only happen every sendInterval
// every unreliable sendInterval is the perfect place to look for changes.
if (StateChanged()) changedSinceBaseline = true;
// only sync on change:
// unreliable isn't guaranteed to be delivered so this depends on reliable baseline.
if (!changedSinceBaseline) return;
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
// serialize
writer.WriteByte(lastSerializedBaselineTick);
OnSerializeDelta(writer);
// send (with optional redundancy to make up for message drops)
CmdClientToServerDelta(writer);
if (unreliableRedundancy)
CmdClientToServerDelta(writer);
}
lastDeltaTime = localTime;
if (debugLog) Debug.Log($"[{name}] Client: sent delta for #{lastSerializedBaselineTick} at time: {localTime}");
}
protected virtual void UpdateClientSync()
{
// client authority, and local player (= allowed to move myself)?
if (IsClientWithAuthority)
{
// https://github.com/vis2k/Mirror/pull/2992/
if (!NetworkClient.ready) return;
// perf: only grab NetworkTime.localTime property once.
double localTime = NetworkTime.localTime;
UpdateClientBaseline(localTime);
UpdateClientDelta(localTime);
}
}
// Update() without LateUpdate() split: otherwise perf. is cut in half!
protected virtual void Update()
{
// if server then always sync to others.
if (isServer) UpdateServerSync();
// 'else if' because host mode shouldn't send anything to server.
// it is the server. don't overwrite anything there.
else if (isClient) UpdateClientSync();
}
// OnSerialize(initial) is called every time when a player starts observing us.
// note this is _not_ called just once on spawn.
// call this from inheriting classes immediately in OnSerialize().
public override void OnSerialize(NetworkWriter writer, bool initialState)
{
if (initialState)
{
// always include the tick for deltas to compare against.
byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once!
writer.WriteByte(frameCount);
// IMPORTANT
// OnSerialize(initial) is called for the spawn payload whenever
// someone starts observing this object. we always must make
// this the new baseline, otherwise this happens:
// - server broadcasts baseline @ t=1
// - server broadcasts delta for baseline @ t=1
// - ... time passes ...
// - new observer -> OnSerialize sends current position @ t=2
// - server broadcasts delta for baseline @ t=1
// => client's baseline is t=2 but receives delta for t=1 _!_
lastSerializedBaselineTick = (byte)Time.frameCount;
lastBaselineTime = NetworkTime.localTime;
// request to store last baseline state (i.e. position) for change detection.
StoreState();
}
}
// call this from inheriting classes immediately in OnDeserialize().
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
if (initialState)
{
// save last deserialized baseline tick number to compare deltas against
lastDeserializedBaselineTick = reader.ReadByte();
}
}
}
}