quake jitter single commit, no compression, always sync, no corrections

This commit is contained in:
mischa 2024-09-24 12:48:53 +02:00
parent 5cc8050d47
commit e29ab56a20
14 changed files with 1128 additions and 195 deletions

View File

@ -0,0 +1,303 @@
// NetworkTransform V3 based on NetworkTransformUnreliable, using Mirror's new
// Unreliable quake style networking model with delta compression.
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Mirror
{
[AddComponentMenu("Network/Network Transform (Unreliable Compressed)")]
public class NetworkTransformUnreliableCompressed : NetworkTransformBase
{
[Header("Debug")]
public bool debugDraw = false;
// Used to store last sent snapshots
protected TransformSnapshot last;
// validation //////////////////////////////////////////////////////////
// Configure is called from OnValidate and Awake
protected override void Configure()
{
base.Configure();
// force syncMethod to unreliable
syncMethod = SyncMethod.Unreliable;
// Unreliable ignores syncInterval. don't need to force anymore:
// sendIntervalMultiplier = 1;
}
// update //////////////////////////////////////////////////////////////
void Update()
{
// if server then always sync to others.
if (isServer) UpdateServer();
// 'else if' because host mode shouldn't send anything to server.
// it is the server. don't overwrite anything there.
else if (isClient) UpdateClient();
}
void LateUpdate()
{
// set dirty to trigger OnSerialize. either always, or only if changed.
// It has to be checked in LateUpdate() for onlySyncOnChange to avoid
// the possibility of Update() running first before the object's movement
// script's Update(), which then causes NT to send every alternate frame
// instead.
if (isServer || (IsClientWithAuthority && NetworkClient.ready))
{
if (!onlySyncOnChange)
SetDirty();
}
}
protected virtual void UpdateServer()
{
// apply buffered snapshots IF client authority
// -> in server authority, server moves the object
// so no need to apply any snapshots there.
// -> don't apply for host mode player objects either, even if in
// client authority mode. if it doesn't go over the network,
// then we don't need to do anything.
// -> connectionToClient is briefly null after scene changes:
// https://github.com/MirrorNetworking/Mirror/issues/3329
if (syncDirection == SyncDirection.ClientToServer &&
connectionToClient != null &&
!isOwned)
{
if (serverSnapshots.Count > 0)
{
// step the transform interpolation without touching time.
// NetworkClient is responsible for time globally.
SnapshotInterpolation.StepInterpolation(
serverSnapshots,
connectionToClient.remoteTimeline,
out TransformSnapshot from,
out TransformSnapshot to,
out double t);
// interpolate & apply
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
Apply(computed, to);
}
}
}
protected virtual void UpdateClient()
{
// client authority, and local player (= allowed to move myself)?
if (!IsClientWithAuthority)
{
// only while we have snapshots
if (clientSnapshots.Count > 0)
{
// step the interpolation without touching time.
// NetworkClient is responsible for time globally.
SnapshotInterpolation.StepInterpolation(
clientSnapshots,
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
out TransformSnapshot from,
out TransformSnapshot to,
out double t);
// interpolate & apply
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
Apply(computed, to);
if (debugDraw)
{
Debug.DrawLine(from.position, to.position, Color.white, 10f);
Debug.DrawLine(computed.position, computed.position + Vector3.up, Color.white, 10f);
}
}
}
}
// Unreliable OnSerialize:
// - initial=true sends reliable full state
// - initial=false sends unreliable delta states
public override void OnSerialize(NetworkWriter writer, bool initialState)
{
// get current snapshot for broadcasting.
TransformSnapshot snapshot = Construct();
// ClientToServer optimization:
// for interpolated client owned identities,
// always broadcast the latest known snapshot so other clients can
// interpolate immediately instead of catching up too
// TODO dirty mask? [compression is very good w/o it already]
// each vector's component is delta compressed.
// an unchanged component would still require 1 byte.
// let's use a dirty bit mask to filter those out as well.
// Debug.Log($"NT OnSerialize: initial={initialState} method={syncMethod}");
// reliable full state
if (initialState)
{
// TODO initialState is now sent multiple times. find a new fix for this:
// If there is a last serialized snapshot, we use it.
// This prevents the new client getting a snapshot that is different
// from what the older clients last got. If this happens, and on the next
// regular serialisation the delta compression will get wrong values.
// Notes:
// 1. Interestingly only the older clients have it wrong, because at the end
// of this function, last = snapshot which is the initial state's snapshot
// 2. Regular NTR gets by this bug because it sends every frame anyway so initialstate
// snapshot constructed would have been the same as the last anyway.
// if (last.remoteTime > 0) snapshot = last;
int startPosition = writer.Position;
if (syncPosition) writer.WriteVector3(snapshot.position);
if (syncRotation)
{
// if smallest-three quaternion compression is enabled,
// then we don't need baseline rotation since delta always
// sends an absolute value.
if (!compressRotation)
{
writer.WriteQuaternion(snapshot.rotation);
}
}
if (syncScale) writer.WriteVector3(snapshot.scale);
// set 'last'
last = snapshot;
}
// unreliable delta: compress against last full reliable state
else
{
int startPosition = writer.Position;
if (syncPosition) writer.WriteVector3(snapshot.position);
if (syncRotation)
{
// (optional) smallest three compression for now. no delta.
if (compressRotation)
{
writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation));
}
else writer.WriteQuaternion(snapshot.rotation);
}
if (syncScale) writer.WriteVector3(snapshot.scale);
}
}
// Unreliable OnDeserialize:
// - initial=true sends reliable full state
// - initial=false sends unreliable delta states
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
Vector3? position = null;
Quaternion? rotation = null;
Vector3? scale = null;
// reliable full state
if (initialState)
{
if (syncPosition)
{
position = reader.ReadVector3();
if (debugDraw) Debug.DrawLine(position.Value, position.Value + Vector3.up , Color.green, 10.0f);
}
if (syncRotation)
{
// if smallest-three quaternion compression is enabled,
// then we don't need baseline rotation since delta always
// sends an absolute value.
if (!compressRotation)
{
rotation = reader.ReadQuaternion();
}
}
if (syncScale) scale = reader.ReadVector3();
}
// unreliable delta: decompress against last full reliable state
else
{
// varint -> delta -> quantize
if (syncPosition)
{
position = reader.ReadVector3();
if (debugDraw) Debug.DrawLine(position.Value, position.Value + Vector3.up , Color.yellow, 10.0f);
}
if (syncRotation)
{
// (optional) smallest three compression for now. no delta.
if (compressRotation)
{
rotation = Compression.DecompressQuaternion(reader.ReadUInt());
}
else
{
rotation = reader.ReadQuaternion();
}
}
if (syncScale)
{
scale = reader.ReadVector3();
}
// handle depending on server / client / host.
// server has priority for host mode.
//
// only do this for the unreliable delta states!
// processing the reliable baselines shows noticeable jitter
// around baseline syncs (e.g. tanks demo @ 4 Hz sendRate).
// unreliable deltas are always within the same time delta,
// so this gives perfectly smooth results.
if (isServer) OnClientToServerSync(position, rotation, scale);
else if (isClient) OnServerToClientSync(position, rotation, scale);
}
}
// sync ////////////////////////////////////////////////////////////////
// local authority client sends sync message to server for broadcasting
protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
{
// only apply if in client authority mode
if (syncDirection != SyncDirection.ClientToServer) return;
// protect against ever growing buffer size attacks
if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return;
// add a small timeline offset to account for decoupled arrival of
// NetworkTime and NetworkTransform snapshots.
// needs to be sendInterval. half sendInterval doesn't solve it.
// https://github.com/MirrorNetworking/Mirror/issues/3427
// remove this after LocalWorldState.
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
}
// server broadcasts sync message to all clients
protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
{
// don't apply for local player with authority
if (IsClientWithAuthority) return;
// add a small timeline offset to account for decoupled arrival of
// NetworkTime and NetworkTransform snapshots.
// needs to be sendInterval. half sendInterval doesn't solve it.
// https://github.com/MirrorNetworking/Mirror/issues/3427
// remove this after LocalWorldState.
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
}
// reset state for next session.
// do not ever call this during a session (i.e. after teleport).
// calling this will break delta compression.
public override void ResetState()
{
base.ResetState();
// reset 'last' for delta too
last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d993bac37a92145448c1ea027b3e9ddc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -103,9 +103,32 @@ public struct EntityStateMessage : NetworkMessage
public ArraySegment<byte> payload;
}
// state update for unreliable sync
public struct EntityStateMessageUnreliable : NetworkMessage
// state update for unreliable sync.
// baseline is always sent over Reliable channel.
public struct EntityStateMessageUnreliableBaseline : NetworkMessage
{
// baseline messages send their tick number as byte.
// delta messages are checked against that tick to avoid applying a
// delta on top of the wrong baseline.
// (byte is enough, we just need something small to compare against)
public byte baselineTick;
public uint netId;
// the serialized component data
// -> ArraySegment to avoid unnecessary allocations
public ArraySegment<byte> payload;
}
// state update for unreliable sync
// delta is always sent over Unreliable channel.
public struct EntityStateMessageUnreliableDelta : NetworkMessage
{
// baseline messages send their tick number as byte.
// delta messages are checked against that tick to avoid applying a
// delta on top of the wrong baseline.
// (byte is enough, we just need something small to compare against)
public byte baselineTick;
public uint netId;
// the serialized component data
// -> ArraySegment to avoid unnecessary allocations

View File

@ -236,12 +236,17 @@ public bool IsDirty() =>
// only check time if bits were dirty. this is more expensive.
NetworkTime.localTime - lastSyncTime >= syncInterval;
// true if any SyncVar or SyncObject is dirty
// OR both bitmasks. != 0 if either was dirty.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDirty_BitsOnly() => (syncVarDirtyBits | syncObjectDirtyBits) != 0UL;
/// <summary>Clears all the dirty bits that were set by SetSyncVarDirtyBit() (formally SetDirtyBits)</summary>
// automatically invoked when an update is sent for this object, but can
// be called manually as well.
public void ClearAllDirtyBits()
public void ClearAllDirtyBits(bool clearSyncTime = true)
{
lastSyncTime = NetworkTime.localTime;
if (clearSyncTime) lastSyncTime = NetworkTime.localTime;
syncVarDirtyBits = 0L;
syncObjectDirtyBits = 0L;

View File

@ -36,9 +36,14 @@ public static partial class NetworkClient
// ocassionally send a full reliable state for unreliable components to delta compress against.
// this only applies to Components with SyncMethod=Unreliable.
public static int unreliableBaselineRate => NetworkServer.unreliableBaselineRate;
public static float unreliableBaselineInterval => unreliableBaselineRate < int.MaxValue ? 1f / unreliableBaselineRate : 0; // for 1 Hz, that's 1000ms
public static float unreliableBaselineInterval => NetworkServer.unreliableBaselineInterval;
static double lastUnreliableBaselineTime;
// quake sends unreliable messages twice to make up for message drops.
// this double bandwidth, but allows for smaller buffer time / faster sync.
// best to turn this off unless the game is extremely fast paced.
public static bool unreliableRedundancy => NetworkServer.unreliableRedundancy;
// For security, it is recommended to disconnect a player if a networked
// action triggers an exception\nThis could prevent components being
// accessed in an undefined state, which may be an attack vector for
@ -511,6 +516,8 @@ internal static void RegisterMessageHandlers(bool hostMode)
RegisterHandler<ObjectSpawnFinishedMessage>(_ => { });
// host mode doesn't need state updates
RegisterHandler<EntityStateMessage>(_ => { });
RegisterHandler<EntityStateMessageUnreliableBaseline>(_ => { });
RegisterHandler<EntityStateMessageUnreliableDelta>(_ => { });
}
else
{
@ -522,6 +529,8 @@ internal static void RegisterMessageHandlers(bool hostMode)
RegisterHandler<ObjectSpawnStartedMessage>(OnObjectSpawnStarted);
RegisterHandler<ObjectSpawnFinishedMessage>(OnObjectSpawnFinished);
RegisterHandler<EntityStateMessage>(OnEntityStateMessage);
RegisterHandler<EntityStateMessageUnreliableBaseline>(OnEntityStateMessageUnreliableBaseline);
RegisterHandler<EntityStateMessageUnreliableDelta>(OnEntityStateMessageUnreliableDelta);
}
// These handlers are the same for host and remote clients
@ -1440,6 +1449,90 @@ static void OnEntityStateMessage(EntityStateMessage message)
else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
}
static void OnEntityStateMessageUnreliableBaseline(EntityStateMessageUnreliableBaseline message, int channelId)
{
// safety check: baseline should always arrive over Reliable channel.
if (channelId != Channels.Reliable)
{
Debug.LogError($"Client OnEntityStateMessageUnreliableBaseline arrived on channel {channelId} instead of Reliable. This should never happen!");
return;
}
// Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}");
if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null)
{
// set the last received reliable baseline tick number.
identity.lastUnreliableBaselineReceived = message.baselineTick;
// iniital is always 'true' because unreliable state sync alwasy serializes full
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
{
// full state updates (initial=true) arrive over reliable.
identity.DeserializeClient(reader, true);
}
}
// no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages.
// else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
}
static void OnEntityStateMessageUnreliableDelta(EntityStateMessageUnreliableDelta message, int channelId)
{
// safety check: baseline should always arrive over Reliable channel.
if (channelId != Channels.Unreliable)
{
Debug.LogError($"Client OnEntityStateMessageUnreliableDelta arrived on channel {channelId} instead of Unreliable. This should never happen!");
return;
}
// Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}");
if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null)
{
// unreliable state sync messages may arrive out of order.
// only ever apply state that's newer than the last received state.
// note that we send one EntityStateMessage per Entity,
// so there will be multiple with the same == timestamp.
//
// note that a reliable baseline may arrive before/after a delta.
// that is fine.
if (connection.remoteTimeStamp < identity.lastUnreliableStateTime)
{
// debug log to show that it's working.
// can be tested via LatencySimulation scramble easily.
Debug.Log($"Client caught out of order Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}");
return;
}
// UDP messages may accidentally arrive twice.
// or even intentionally, if unreliableRedundancy is turned on.
else if (connection.remoteTimeStamp == identity.lastUnreliableStateTime)
{
// only log this if unreliableRedundancy is disabled.
// otherwise it's expected and will happen a lot.
if (!unreliableRedundancy) Debug.Log($"Client caught duplicate Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}");
return;
}
// make sure this delta is for the correct baseline.
// we don't want to apply an old delta on top of a new baseline.
if (message.baselineTick != identity.lastUnreliableBaselineReceived)
{
Debug.Log($"Client caught Unreliable state message for old baseline for {identity} with baselineTick={identity.lastUnreliableBaselineReceived} messageBaseline={message.baselineTick}. This is fine.");
return;
}
// set the new last received time for unreliable
identity.lastUnreliableStateTime = connection.remoteTimeStamp;
// iniital is always 'true' because unreliable state sync alwasy serializes full
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
{
// delta state updates (initial=false) arrive over unreliable.
identity.DeserializeClient(reader, false);
}
}
// no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages.
// else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
}
static void OnRPCMessage(RpcMessage message)
{
// Debug.Log($"NetworkClient.OnRPCMessage hash:{message.functionHash} netId:{message.netId}");
@ -1556,7 +1649,7 @@ internal static void NetworkLateUpdate()
bool unreliableBaselineElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, unreliableBaselineInterval, ref lastUnreliableBaselineTime);
if (!Application.isPlaying || sendIntervalElapsed)
{
Broadcast();
Broadcast(unreliableBaselineElapsed);
}
UpdateConnectionQuality();
@ -1620,7 +1713,9 @@ void UpdateConnectionQuality()
// broadcast ///////////////////////////////////////////////////////////
// make sure Broadcast() is only called every sendInterval.
// calling it every update() would require too much bandwidth.
static void Broadcast()
//
// unreliableFullSendIntervalElapsed: indicates that unreliable sync components need a reliable baseline sync this time.
static void Broadcast(bool unreliableBaselineElapsed)
{
// joined the world yet?
if (!connection.isReady) return;
@ -1632,12 +1727,14 @@ static void Broadcast()
Send(new TimeSnapshotMessage(), Channels.Unreliable);
// broadcast client state to server
BroadcastToServer();
BroadcastToServer(unreliableBaselineElapsed);
}
// NetworkServer has BroadcastToConnection.
// NetworkClient has BroadcastToServer.
static void BroadcastToServer()
//
// unreliableFullSendIntervalElapsed: indicates that unreliable sync components need a reliable baseline sync this time.
static void BroadcastToServer(bool unreliableBaselineElapsed)
{
// for each entity that the client owns
foreach (NetworkIdentity identity in connection.owned)
@ -1648,21 +1745,64 @@ static void BroadcastToServer()
// NetworkServer.Destroy)
if (identity != null)
{
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
// 'Reliable' sync: send Reliable components over reliable.
using (NetworkWriterPooled writerReliable = NetworkWriterPool.Get(),
writerUnreliableDelta = NetworkWriterPool.Get(),
writerUnreliableBaseline = NetworkWriterPool.Get())
{
// get serialization for this entity viewed by this connection
// (if anything was serialized this time)
identity.SerializeClient(writer);
if (writer.Position > 0)
// serialize reliable and unreliable components in only one iteration.
// serializing reliable and unreliable separately in two iterations would be too costly.
identity.SerializeClient(writerReliable, writerUnreliableBaseline, writerUnreliableDelta, unreliableBaselineElapsed);
// any reliable components serialization?
if (writerReliable.Position > 0)
{
// send state update message
EntityStateMessage message = new EntityStateMessage
{
netId = identity.netId,
payload = writer.ToArraySegment()
payload = writerReliable.ToArraySegment()
};
Send(message);
}
// any unreliable components serialization?
// we always send unreliable deltas to ensure interpolation always has a data point that arrives immediately.
if (writerUnreliableDelta.Position > 0)
{
EntityStateMessageUnreliableDelta message = new EntityStateMessageUnreliableDelta
{
// baselineTick: the last unreliable baseline to compare against
baselineTick = identity.lastUnreliableBaselineSent,
netId = identity.netId,
payload = writerUnreliableDelta.ToArraySegment()
};
Send(message, Channels.Unreliable);
}
// time for unreliable baseline sync?
// we always send this after the unreliable delta,
// so there's a higher chance that it arrives after the delta.
// in other words: so that the delta can still be used against previous baseline.
if (unreliableBaselineElapsed)
{
if (writerUnreliableBaseline.Position > 0)
{
// remember last sent baseline tick for this entity.
// (byte) to minimize bandwidth. we don't need the full tick,
// just something small to compare against.
identity.lastUnreliableBaselineSent = (byte)Time.frameCount;
// send state update message
EntityStateMessageUnreliableBaseline message = new EntityStateMessageUnreliableBaseline
{
baselineTick = identity.lastUnreliableBaselineSent,
netId = identity.netId,
payload = writerUnreliableBaseline.ToArraySegment()
};
Send(message, Channels.Reliable);
}
}
}
}
// spawned list should have no null entries because we

View File

@ -27,13 +27,26 @@ public struct NetworkIdentitySerialization
{
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks
public int tick;
public NetworkWriter ownerWriter;
public NetworkWriter observersWriter;
// reliable sync
public NetworkWriter ownerWriterReliable;
public NetworkWriter observersWriterReliable;
// unreliable sync
public NetworkWriter ownerWriterUnreliableBaseline;
public NetworkWriter observersWriterUnreliableBaseline;
public NetworkWriter ownerWriterUnreliableDelta;
public NetworkWriter observersWriterUnreliableDelta;
public void ResetWriters()
{
ownerWriter.Position = 0;
observersWriter.Position = 0;
ownerWriterReliable.Position = 0;
observersWriterReliable.Position = 0;
ownerWriterUnreliableBaseline.Position = 0;
observersWriterUnreliableBaseline.Position = 0;
ownerWriterUnreliableDelta.Position = 0;
observersWriterUnreliableDelta.Position = 0;
}
}
@ -225,10 +238,23 @@ public Visibility visible
// => way easier to store them per object
NetworkIdentitySerialization lastSerialization = new NetworkIdentitySerialization
{
ownerWriter = new NetworkWriter(),
observersWriter = new NetworkWriter()
ownerWriterReliable = new NetworkWriter(),
observersWriterReliable = new NetworkWriter(),
ownerWriterUnreliableBaseline = new NetworkWriter(),
observersWriterUnreliableBaseline = new NetworkWriter(),
ownerWriterUnreliableDelta = new NetworkWriter(),
observersWriterUnreliableDelta = new NetworkWriter(),
};
// unreliable state sync messages may arrive out of order, or duplicated.
// keep latest received timestamp so we don't apply older messages.
internal double lastUnreliableStateTime;
// the last baseline we received for this object.
// deltas are based on the baseline, need to make sure we don't apply on an old one.
internal byte lastUnreliableBaselineSent;
internal byte lastUnreliableBaselineReceived;
// Keep track of all sceneIds to detect scene duplicates
static readonly Dictionary<ulong, NetworkIdentity> sceneIds =
new Dictionary<ulong, NetworkIdentity>();
@ -879,10 +905,20 @@ internal void OnStopLocalPlayer()
// build dirty mask for server owner & observers (= all dirty components).
// faster to do it in one iteration instead of iterating separately.
(ulong, ulong) ServerDirtyMasks_Broadcast()
// -> build Reliable and Unreliable masks in one iteration.
// running two loops would be too costly.
void ServerDirtyMasks_Broadcast(
out ulong ownerMaskReliable, out ulong observerMaskReliable,
out ulong ownerMaskUnreliableBaseline, out ulong observerMaskUnreliableBaseline,
out ulong ownerMaskUnreliableDelta, out ulong observerMaskUnreliableDelta)
{
ulong ownerMask = 0;
ulong observerMask = 0;
// clear
ownerMaskReliable = 0;
observerMaskReliable = 0;
ownerMaskUnreliableBaseline = 0;
observerMaskUnreliableBaseline = 0;
ownerMaskUnreliableDelta = 0;
observerMaskUnreliableDelta = 0;
NetworkBehaviour[] components = NetworkBehaviours;
for (int i = 0; i < components.Length; ++i)
@ -890,6 +926,10 @@ internal void OnStopLocalPlayer()
NetworkBehaviour component = components[i];
ulong nthBit = (1u << i);
// RELIABLE COMPONENTS /////////////////////////////////////////
if (component.syncMethod == SyncMethod.Reliable)
{
// check if this component is dirty
bool dirty = component.IsDirty();
// owner needs to be considered for both SyncModes, because
@ -898,7 +938,7 @@ internal void OnStopLocalPlayer()
// for broadcast, only for ServerToClient and only if dirty.
// ClientToServer comes from the owner client.
if (component.syncDirection == SyncDirection.ServerToClient && dirty)
ownerMask |= nthBit;
ownerMaskReliable |= nthBit;
// observers need to be considered only in Observers mode,
// otherwise they receive no sync data of this component ever.
@ -907,18 +947,76 @@ internal void OnStopLocalPlayer()
// for broadcast, only sync to observers if dirty.
// SyncDirection is irrelevant, as both are broadcast to
// observers which aren't the owner.
if (dirty) observerMask |= nthBit;
if (dirty) observerMaskReliable |= nthBit;
}
}
return (ownerMask, observerMask);
}
// build dirty mask for client.
// server always knows initialState, so we don't need it here.
ulong ClientDirtyMask()
// UNRELIABLE COMPONENTS ///////////////////////////////////////
else if (component.syncMethod == SyncMethod.Unreliable)
{
ulong mask = 0;
// UNRELIABLE DELTAS ///////////////////////////////////////
{
// check if this component is dirty.
// delta sync runs @ syncInterval.
// this allows for significant bandwidth savings.
bool dirty = component.IsDirty();
// owner needs to be considered for both SyncModes, because
// Observers mode always includes the Owner.
//
// for broadcast, only for ServerToClient and only if dirty.
// ClientToServer comes from the owner client.
if (component.syncDirection == SyncDirection.ServerToClient && dirty)
ownerMaskUnreliableDelta |= nthBit;
// observers need to be considered only in Observers mode,
// otherwise they receive no sync data of this component ever.
if (component.syncMode == SyncMode.Observers)
{
// for broadcast, only sync to observers if dirty.
// SyncDirection is irrelevant, as both are broadcast to
// observers which aren't the owner.
if (dirty) observerMaskUnreliableDelta |= nthBit;
}
}
// UNRELIABLE BASELINE /////////////////////////////////////
{
// check if this component is dirty.
// baseline sync runs @ 1 Hz (netmanager configurable).
// only consider dirty bits, ignore syncinterval.
bool dirty = component.IsDirty_BitsOnly();
// owner needs to be considered for both SyncModes, because
// Observers mode always includes the Owner.
//
// for broadcast, only for ServerToClient and only if dirty.
// ClientToServer comes from the owner client.
if (component.syncDirection == SyncDirection.ServerToClient && dirty)
ownerMaskUnreliableBaseline |= nthBit;
// observers need to be considered only in Observers mode,
// otherwise they receive no sync data of this component ever.
if (component.syncMode == SyncMode.Observers)
{
// for broadcast, only sync to observers if dirty.
// SyncDirection is irrelevant, as both are broadcast to
// observers which aren't the owner.
if (dirty) observerMaskUnreliableBaseline |= nthBit;
}
}
////////////////////////////////////////////////////////////
}
}
}
// build dirty mask for client components.
// server always knows initialState, so we don't need it here.
// -> build Reliable and Unreliable masks in one iteration.
// running two loops would be too costly.
void ClientDirtyMasks(out ulong dirtyMaskReliable, out ulong dirtyMaskUnreliableBaseline, out ulong dirtyMaskUnreliableDelta)
{
dirtyMaskReliable = 0;
dirtyMaskUnreliableBaseline = 0;
dirtyMaskUnreliableDelta = 0;
NetworkBehaviour[] components = NetworkBehaviours;
for (int i = 0; i < components.Length; ++i)
@ -935,14 +1033,30 @@ ulong ClientDirtyMask()
ulong nthBit = (1u << i);
if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
{
// RELIABLE COMPONENTS /////////////////////////////////////////
if (component.syncMethod == SyncMethod.Reliable)
{
// set the n-th bit if dirty
// shifting from small to large numbers is varint-efficient.
if (component.IsDirty()) mask |= nthBit;
}
if (component.IsDirty()) dirtyMaskReliable |= nthBit;
}
// UNRELIABLE COMPONENTS ///////////////////////////////////////
else if (component.syncMethod == SyncMethod.Unreliable)
{
// set the n-th bit if dirty
// shifting from small to large numbers is varint-efficient.
return mask;
// baseline sync runs @ 1 Hz (netmanager configurable).
// only consider dirty bits, ignore syncinterval.
if (component.IsDirty_BitsOnly()) dirtyMaskUnreliableBaseline |= nthBit;
// delta sync runs @ syncInterval.
// this allows for significant bandwidth savings.
if (component.IsDirty()) dirtyMaskUnreliableDelta |= nthBit;
}
}
}
}
// check if n-th component is dirty.
@ -1023,7 +1137,14 @@ internal void SerializeServer_Spawn(NetworkWriter ownerWriter, NetworkWriter obs
// serialize server components, with delta state for broadcast messages.
// check ownerWritten/observersWritten to know if anything was written
internal void SerializeServer_Broadcast(NetworkWriter ownerWriter, NetworkWriter observersWriter)
//
// serialize Reliable and Unreliable components in one iteration.
// having two separate functions doing two iterations would be too costly.
internal void SerializeServer_Broadcast(
NetworkWriter ownerWriterReliable, NetworkWriter observersWriterReliable,
NetworkWriter ownerWriterUnreliableBaseline, NetworkWriter observersWriterUnreliableBaseline,
NetworkWriter ownerWriterUnreliableDelta, NetworkWriter observersWriterUnreliableDelta,
bool unreliableBaseline)
{
// ensure NetworkBehaviours are valid before usage
ValidateComponents();
@ -1036,16 +1157,29 @@ internal void SerializeServer_Broadcast(NetworkWriter ownerWriter, NetworkWriter
// instead of writing a 1 byte index per component,
// we limit components to 64 bits and write one ulong instead.
// the ulong is also varint compressed for minimum bandwidth.
(ulong ownerMask, ulong observerMask) = ServerDirtyMasks_Broadcast();
ServerDirtyMasks_Broadcast(
out ulong ownerMaskReliable, out ulong observerMaskReliable,
out ulong ownerMaskUnreliableBaseline, out ulong observerMaskUnreliableBaseline,
out ulong ownerMaskUnreliableDelta, out ulong observerMaskUnreliableDelta
);
// if nothing dirty, then don't even write the mask.
// otherwise, every unchanged object would send a 1 byte dirty mask!
if (ownerMask != 0) Compression.CompressVarUInt(ownerWriter, ownerMask);
if (observerMask != 0) Compression.CompressVarUInt(observersWriter, observerMask);
if (ownerMaskReliable != 0) Compression.CompressVarUInt(ownerWriterReliable, ownerMaskReliable);
if (observerMaskReliable != 0) Compression.CompressVarUInt(observersWriterReliable, observerMaskReliable);
if (ownerMaskUnreliableDelta != 0) Compression.CompressVarUInt(ownerWriterUnreliableDelta, ownerMaskUnreliableDelta);
if (observerMaskUnreliableDelta != 0) Compression.CompressVarUInt(observersWriterUnreliableDelta, observerMaskUnreliableDelta);
if (ownerMaskUnreliableBaseline != 0) Compression.CompressVarUInt(ownerWriterUnreliableBaseline, ownerMaskUnreliableBaseline);
if (observerMaskUnreliableBaseline != 0) Compression.CompressVarUInt(observersWriterUnreliableBaseline, observerMaskUnreliableBaseline);
// serialize all components
// perf: only iterate if either dirty mask has dirty bits.
if ((ownerMask | observerMask) != 0)
if ((ownerMaskReliable | observerMaskReliable |
ownerMaskUnreliableBaseline | observerMaskUnreliableBaseline |
ownerMaskUnreliableDelta | observerMaskUnreliableDelta)
!= 0)
{
for (int i = 0; i < components.Length; ++i)
{
@ -1063,9 +1197,15 @@ internal void SerializeServer_Broadcast(NetworkWriter ownerWriter, NetworkWriter
// SyncDirection it's not guaranteed to be in owner anymore.
// so we need to serialize to temporary writer first.
// and then copy as needed.
bool ownerDirty = IsDirty(ownerMask, i);
bool observersDirty = IsDirty(observerMask, i);
if (ownerDirty || observersDirty)
bool ownerDirtyReliable = IsDirty(ownerMaskReliable, i);
bool observersDirtyReliable = IsDirty(observerMaskReliable, i);
bool ownerDirtyUnreliableBaseline = IsDirty(ownerMaskUnreliableBaseline, i);
bool observersDirtyUnreliableBaseline = IsDirty(observerMaskUnreliableBaseline, i);
bool ownerDirtyUnreliableDelta = IsDirty(ownerMaskUnreliableDelta, i);
bool observersDirtyUnreliableDelta = IsDirty(observerMaskUnreliableDelta, i);
// RELIABLE COMPONENTS /////////////////////////////////////
if (ownerDirtyReliable || observersDirtyReliable)
{
// serialize into helper writer
using (NetworkWriterPooled temp = NetworkWriterPool.Get())
@ -1074,20 +1214,57 @@ internal void SerializeServer_Broadcast(NetworkWriter ownerWriter, NetworkWriter
ArraySegment<byte> segment = temp.ToArraySegment();
// copy to owner / observers as needed
if (ownerDirty) ownerWriter.WriteBytes(segment.Array, segment.Offset, segment.Count);
if (observersDirty) observersWriter.WriteBytes(segment.Array, segment.Offset, segment.Count);
if (ownerDirtyReliable) ownerWriterReliable.WriteBytes(segment.Array, segment.Offset, segment.Count);
if (observersDirtyReliable) observersWriterReliable.WriteBytes(segment.Array, segment.Offset, segment.Count);
}
// dirty bits indicate 'changed since last delta sync'.
// clear them after a delta sync here.
comp.ClearAllDirtyBits();
}
// UNRELIABLE DELTA ////////////////////////////////////////
// we always send the unreliable delta no matter what
if (ownerDirtyUnreliableDelta || observersDirtyUnreliableDelta)
{
using (NetworkWriterPooled temp = NetworkWriterPool.Get())
{
comp.Serialize(temp, false);
ArraySegment<byte> segment = temp.ToArraySegment();
// copy to owner / observers as needed
if (ownerDirtyUnreliableDelta) ownerWriterUnreliableDelta.WriteBytes(segment.Array, segment.Offset, segment.Count);
if (observersDirtyUnreliableDelta) observersWriterUnreliableDelta.WriteBytes(segment.Array, segment.Offset, segment.Count);
// clear sync time to only send delta again after syncInterval.
comp.lastSyncTime = NetworkTime.localTime;
}
}
// UNRELIABLE BASELINE /////////////////////////////////////
// sometimes we need the unreliable baseline
// (we always sync deltas, so no 'else if' here)
if (unreliableBaseline && (ownerDirtyUnreliableBaseline || observersDirtyUnreliableBaseline))
{
using (NetworkWriterPooled temp = NetworkWriterPool.Get())
{
comp.Serialize(temp, true);
ArraySegment<byte> segment = temp.ToArraySegment();
// copy to owner / observers as needed
if (ownerDirtyUnreliableBaseline) ownerWriterUnreliableBaseline.WriteBytes(segment.Array, segment.Offset, segment.Count);
if (observersDirtyUnreliableBaseline) observersWriterUnreliableBaseline.WriteBytes(segment.Array, segment.Offset, segment.Count);
}
// for unreliable components, only clear dirty bits after the reliable baseline.
// -> don't clear sync time: that's for delta syncs.
comp.ClearAllDirtyBits(false);
}
}
}
}
// serialize components into writer on the client.
internal void SerializeClient(NetworkWriter writer)
// serialize Reliable and Unreliable components in one iteration.
// having two separate functions doing two iterations would be too costly.
internal void SerializeClient(NetworkWriter writerReliable, NetworkWriter writerUnreliableBaseline, NetworkWriter writerUnreliableDelta, bool unreliableBaseline)
{
// ensure NetworkBehaviours are valid before usage
ValidateComponents();
@ -1100,7 +1277,7 @@ internal void SerializeClient(NetworkWriter writer)
// instead of writing a 1 byte index per component,
// we limit components to 64 bits and write one ulong instead.
// the ulong is also varint compressed for minimum bandwidth.
ulong dirtyMask = ClientDirtyMask();
ClientDirtyMasks(out ulong dirtyMaskReliable, out ulong dirtyMaskUnreliableBaseline, out ulong dirtyMaskUnreliableDelta);
// varint compresses the mask to 1 byte in most cases.
// instead of writing an 8 byte ulong.
@ -1111,25 +1288,28 @@ internal void SerializeClient(NetworkWriter writer)
// if nothing dirty, then don't even write the mask.
// otherwise, every unchanged object would send a 1 byte dirty mask!
if (dirtyMask != 0) Compression.CompressVarUInt(writer, dirtyMask);
if (dirtyMaskReliable != 0) Compression.CompressVarUInt(writerReliable, dirtyMaskReliable);
if (dirtyMaskUnreliableDelta != 0) Compression.CompressVarUInt(writerUnreliableDelta, dirtyMaskUnreliableDelta);
if (dirtyMaskUnreliableBaseline != 0) Compression.CompressVarUInt(writerUnreliableBaseline, dirtyMaskUnreliableBaseline);
// serialize all components
// perf: only iterate if dirty mask has dirty bits.
if (dirtyMask != 0)
if (dirtyMaskReliable != 0 || dirtyMaskUnreliableDelta != 0 || dirtyMaskUnreliableBaseline != 0)
{
// serialize all components
for (int i = 0; i < components.Length; ++i)
{
NetworkBehaviour comp = components[i];
// RELIABLE SERIALIZATION //////////////////////////////////
// is this component dirty?
// reuse the mask instead of calling comp.IsDirty() again here.
if (IsDirty(dirtyMask, i))
if (IsDirty(dirtyMaskReliable, i))
// if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
{
// serialize into writer.
// server always knows initialState, we never need to send it
comp.Serialize(writer, false);
comp.Serialize(writerReliable, false);
// clear dirty bits for the components that we serialized.
// do not clear for _all_ components, only the ones that
@ -1139,13 +1319,39 @@ internal void SerializeClient(NetworkWriter writer)
// was elapsed, as then they wouldn't be synced.
comp.ClearAllDirtyBits();
}
// UNRELIABLE DELTA ////////////////////////////////////////
// we always send the unreliable delta no matter what
if (IsDirty(dirtyMaskUnreliableDelta, i))
// if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
{
comp.Serialize(writerUnreliableDelta, false);
// clear sync time to only send delta again after syncInterval.
comp.lastSyncTime = NetworkTime.localTime;
}
// UNRELIABLE BASELINE /////////////////////////////////////
// sometimes we need the unreliable baseline
// (we always sync deltas, so no 'else if' here)
if (unreliableBaseline && IsDirty(dirtyMaskUnreliableBaseline, i))
// if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
{
comp.Serialize(writerUnreliableBaseline, true);
// for unreliable components, only clear dirty bits after the reliable baseline.
// unreliable deltas aren't guaranteed to be delivered, no point in clearing bits.
// -> don't clear sync time: that's for delta syncs.
comp.ClearAllDirtyBits(false);
}
////////////////////////////////////////////////////////////
}
}
}
// deserialize components from the client on the server.
// there's no 'initialState'. server always knows the initial state.
internal bool DeserializeServer(NetworkReader reader)
// for reliable state sync, server always knows the initial state.
// for unreliable, we always sync full state so we still need the parameter.
internal bool DeserializeServer(NetworkReader reader, bool initialState)
{
// ensure NetworkBehaviours are valid before usage
ValidateComponents();
@ -1169,7 +1375,7 @@ internal bool DeserializeServer(NetworkReader reader)
// deserialize this component
// server always knows the initial state (initial=false)
// disconnect if failed, to prevent exploits etc.
if (!comp.Deserialize(reader, false)) return false;
if (!comp.Deserialize(reader, initialState)) return false;
// server received state from the owner client.
// set dirty so it's broadcast to other clients too.
@ -1213,7 +1419,10 @@ internal void DeserializeClient(NetworkReader reader, bool initialState)
// get cached serialization for this tick (or serialize if none yet).
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks.
// calls SerializeServer, so this function is to be called on server.
internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick)
//
// unreliableBaselineElapsed: indicates that unreliable sync components need a reliable baseline sync this time.
// for reliable components, it just means sync as usual.
internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick, bool unreliableBaselineElapsed)
{
// only rebuild serialization once per tick. reuse otherwise.
// except for tests, where Time.frameCount never increases.
@ -1230,9 +1439,17 @@ internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick)
// reset
lastSerialization.ResetWriters();
// serialize
SerializeServer_Broadcast(lastSerialization.ownerWriter,
lastSerialization.observersWriter);
// serialize both Reliable and Unreliable components in one iteration.
// doing each in their own iteration would be too costly.
SerializeServer_Broadcast(
lastSerialization.ownerWriterReliable,
lastSerialization.observersWriterReliable,
lastSerialization.ownerWriterUnreliableBaseline,
lastSerialization.observersWriterUnreliableBaseline,
lastSerialization.ownerWriterUnreliableDelta,
lastSerialization.observersWriterUnreliableDelta,
unreliableBaselineElapsed
);
// set tick
lastSerialization.tick = tick;

View File

@ -46,6 +46,12 @@ public class NetworkManager : MonoBehaviour
[Tooltip("Ocassionally send a full reliable state for unreliable components to delta compress against. This only applies to Components with SyncMethod=Unreliable.")]
public int unreliableBaselineRate = 1;
// quake sends unreliable messages twice to make up for message drops.
// this double bandwidth, but allows for smaller buffer time / faster sync.
// best to turn this off unless the game is extremely fast paced.
[Tooltip("Send unreliable messages twice to make up for message drops. This doubles bandwidth, but allows for smaller buffer time / faster sync.\nBest to turn this off unless your game is extremely fast paced.")]
public bool unreliableRedundancy = false;
// Deprecated 2023-11-25
// Using SerializeField and HideInInspector to self-correct for being
// replaced by headlessStartMode. This can be removed in the future.
@ -318,6 +324,7 @@ void ApplyConfiguration()
{
NetworkServer.tickRate = sendRate;
NetworkServer.unreliableBaselineRate = unreliableBaselineRate;
NetworkServer.unreliableRedundancy = unreliableRedundancy;
NetworkClient.snapshotSettings = snapshotSettings;
NetworkClient.connectionQualityInterval = evaluationInterval;
NetworkClient.connectionQualityMethod = evaluationMethod;

View File

@ -60,6 +60,11 @@ public static partial class NetworkServer
public static float unreliableBaselineInterval => unreliableBaselineRate < int.MaxValue ? 1f / unreliableBaselineRate : 0; // for 1 Hz, that's 1000ms
static double lastUnreliableBaselineTime;
// quake sends unreliable messages twice to make up for message drops.
// this double bandwidth, but allows for smaller buffer time / faster sync.
// best to turn this off unless the game is extremely fast paced.
public static bool unreliableRedundancy = false;
/// <summary>Connection to host mode client (if any)</summary>
public static LocalConnectionToClient localConnection { get; private set; }
@ -318,6 +323,8 @@ internal static void RegisterMessageHandlers()
RegisterHandler<NetworkPingMessage>(NetworkTime.OnServerPing, false);
RegisterHandler<NetworkPongMessage>(NetworkTime.OnServerPong, false);
RegisterHandler<EntityStateMessage>(OnEntityStateMessage, true);
RegisterHandler<EntityStateMessageUnreliableBaseline>(OnEntityStateMessageUnreliableBaseline, true);
RegisterHandler<EntityStateMessageUnreliableDelta>(OnEntityStateMessageUnreliableDelta, true);
RegisterHandler<TimeSnapshotMessage>(OnTimeSnapshotMessage, false); // unreliable may arrive before reliable authority went through
}
@ -406,7 +413,10 @@ static void OnEntityStateMessage(NetworkConnectionToClient connection, EntitySta
{
// DeserializeServer checks permissions internally.
// failure to deserialize disconnects to prevent exploits.
if (!identity.DeserializeServer(reader))
// -> initialState=false because for Reliable messages,
// initial always comes from server and broadcast
// updates are always deltas.
if (!identity.DeserializeServer(reader, false))
{
if (exceptionsDisconnect)
{
@ -429,6 +439,137 @@ static void OnEntityStateMessage(NetworkConnectionToClient connection, EntitySta
// else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
}
// for client's owned ClientToServer components.
static void OnEntityStateMessageUnreliableBaseline(NetworkConnectionToClient connection, EntityStateMessageUnreliableBaseline message, int channelId)
{
// safety check: baseline should always arrive over Reliable channel.
if (channelId != Channels.Reliable)
{
Debug.LogError($"Server OnEntityStateMessageUnreliableBaseline arrived on channel {channelId} instead of Reliable. This should never happen!");
return;
}
// need to validate permissions carefully.
// an attacker may attempt to modify a not-owned or not-ClientToServer component.
// valid netId?
if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null)
{
// owned by the connection?
if (identity.connectionToClient == connection)
{
// set the last received reliable baseline tick number.
identity.lastUnreliableBaselineReceived = message.baselineTick;
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
{
// DeserializeServer checks permissions internally.
// failure to deserialize disconnects to prevent exploits.
//
// full state updates (initial=true) arrive over reliable.
if (!identity.DeserializeServer(reader, true))
{
if (exceptionsDisconnect)
{
Debug.LogError($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}, Disconnecting.");
connection.Disconnect();
}
else
Debug.LogWarning($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}.");
}
}
}
// An attacker may attempt to modify another connection's entity
// This could also be a race condition of message in flight when
// RemoveClientAuthority is called, so not malicious.
// Don't disconnect, just log the warning.
else
Debug.LogWarning($"EntityStateMessage from {connection} for {identity.name} without authority.");
}
// no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages.
// else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
}
// for client's owned ClientToServer components.
static void OnEntityStateMessageUnreliableDelta(NetworkConnectionToClient connection, EntityStateMessageUnreliableDelta message, int channelId)
{
// safety check: baseline should always arrive over Reliable channel.
if (channelId != Channels.Unreliable)
{
Debug.LogError($"Server OnEntityStateMessageUnreliableDelta arrived on channel {channelId} instead of Unreliable. This should never happen!");
return;
}
// need to validate permissions carefully.
// an attacker may attempt to modify a not-owned or not-ClientToServer component.
// valid netId?
if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null)
{
// owned by the connection?
if (identity.connectionToClient == connection)
{
// unreliable state sync messages may arrive out of order.
// only ever apply state that's newer than the last received state.
// note that we send one EntityStateMessage per Entity,
// so there will be multiple with the same == timestamp.
if (connection.remoteTimeStamp < identity.lastUnreliableStateTime)
{
// debug log to show that it's working.
// can be tested via LatencySimulation scramble easily.
Debug.Log($"Server caught out of order Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}");
return;
}
// UDP messages may accidentally arrive twice.
// or even intentionally, if unreliableRedundancy is turned on.
else if (connection.remoteTimeStamp == identity.lastUnreliableStateTime)
{
// only log this if unreliableRedundancy is disabled.
// otherwise it's expected and will happen a lot.
if (!unreliableRedundancy) Debug.Log($"Server caught duplicate Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}");
return;
}
// make sure this delta is for the correct baseline.
// we don't want to apply an old delta on top of a new baseline.
if (message.baselineTick != identity.lastUnreliableBaselineReceived)
{
Debug.Log($"Server caught Unreliable state message for old baseline for {identity} with baselineTick={identity.lastUnreliableBaselineReceived} messageBaseline={message.baselineTick}. This is fine.");
return;
}
// set the new last received time for unreliable
identity.lastUnreliableStateTime = connection.remoteTimeStamp;
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
{
// DeserializeServer checks permissions internally.
// failure to deserialize disconnects to prevent exploits.
//
// delta state updates (initial=false) arrive over unreliable.
if (!identity.DeserializeServer(reader, false))
{
if (exceptionsDisconnect)
{
Debug.LogError($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}, Disconnecting.");
connection.Disconnect();
}
else
Debug.LogWarning($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}.");
}
}
}
// An attacker may attempt to modify another connection's entity
// This could also be a race condition of message in flight when
// RemoveClientAuthority is called, so not malicious.
// Don't disconnect, just log the warning.
else
Debug.LogWarning($"EntityStateMessage from {connection} for {identity.name} without authority.");
}
// no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages.
// else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
}
// client sends TimeSnapshotMessage every sendInterval.
// batching already includes the remoteTimestamp.
// we simply insert it on-message here.
@ -1876,11 +2017,18 @@ public static void RebuildObservers(NetworkIdentity identity, bool initialize)
// broadcasting ////////////////////////////////////////////////////////
// helper function to get the right serialization for a connection
static NetworkWriter SerializeForConnection(NetworkIdentity identity, NetworkConnectionToClient connection)
// -> unreliableBaselineElapsed: even though we only care about RELIABLE
// components here, GetServerSerializationAtTick still caches all
// the serializations for this frame. and when caching we already
// need to know if the unreliable baseline will be needed or not.
static NetworkWriter SerializeForConnection_ReliableComponents(
NetworkIdentity identity,
NetworkConnectionToClient connection,
bool unreliableBaselineElapsed)
{
// get serialization for this entity (cached)
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks
NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount);
NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount, unreliableBaselineElapsed);
// is this entity owned by this connection?
bool owned = identity.connectionToClient == connection;
@ -1890,23 +2038,65 @@ static NetworkWriter SerializeForConnection(NetworkIdentity identity, NetworkCon
if (owned)
{
// was it dirty / did we actually serialize anything?
if (serialization.ownerWriter.Position > 0)
return serialization.ownerWriter;
if (serialization.ownerWriterReliable.Position > 0)
return serialization.ownerWriterReliable;
}
// observers writer if not owned
else
{
// was it dirty / did we actually serialize anything?
if (serialization.observersWriter.Position > 0)
return serialization.observersWriter;
if (serialization.observersWriterReliable.Position > 0)
return serialization.observersWriterReliable;
}
// nothing was serialized
return null;
}
// helper function to get the right serialization for a connection
static (NetworkWriter, NetworkWriter) SerializeForConnection_UnreliableComponents(
NetworkIdentity identity,
NetworkConnectionToClient connection,
bool unreliableBaselineElapsed)
{
// get serialization for this entity (cached)
// IMPORTANT: int tick avoids floating point inaccuracy over days/weeks
NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount, unreliableBaselineElapsed);
// is this entity owned by this connection?
bool owned = identity.connectionToClient == connection;
NetworkWriter baselineWriter = null;
NetworkWriter deltaWriter = null;
// send serialized data
// owner writer if owned
if (owned)
{
// was it dirty / did we actually serialize anything?
if (serialization.ownerWriterUnreliableBaseline.Position > 0)
baselineWriter = serialization.ownerWriterUnreliableBaseline;
if (serialization.ownerWriterUnreliableDelta.Position > 0)
deltaWriter = serialization.ownerWriterUnreliableDelta;
}
// observers writer if not owned
else
{
// was it dirty / did we actually serialize anything?
if (serialization.observersWriterUnreliableBaseline.Position > 0)
baselineWriter = serialization.observersWriterUnreliableBaseline;
if (serialization.observersWriterUnreliableDelta.Position > 0)
deltaWriter = serialization.observersWriterUnreliableDelta;
}
// nothing was serialized
return (baselineWriter, deltaWriter);
}
// helper function to broadcast the world to a connection
static void BroadcastToConnection(NetworkConnectionToClient connection)
static void BroadcastToConnection(NetworkConnectionToClient connection, bool unreliableBaselineElapsed)
{
// for each entity that this connection is seeing
bool hasNull = false;
@ -1918,9 +2108,24 @@ static void BroadcastToConnection(NetworkConnectionToClient connection)
// NetworkServer.Destroy)
if (identity != null)
{
// 'Reliable' sync: send Reliable components over reliable with initial/delta
// get serialization for this entity viewed by this connection
// (if anything was serialized this time)
NetworkWriter serialization = SerializeForConnection(identity, connection);
NetworkWriter serialization = SerializeForConnection_ReliableComponents(identity, connection,
// IMPORTANT: even for Reliable components we must pass unreliableBaselineElapsed!
//
// consider this (in one frame):
// Serialize Reliable (unreliableBaseline=false)
// GetServerSerializationAtTick (unreliableBaseline=false)
// serializes new, clears dirty bits
// Serialize Unreliable (unreliableBaseline=true)
// GetServerSerializationAtTick (unreliableBaseline=true)
// last.baseline != baseline
// serializes new, which does nothing since dirty bits were already cleared above!
//
// TODO make this less magic in the future. too easy to miss.
unreliableBaselineElapsed);
if (serialization != null)
{
EntityStateMessage message = new EntityStateMessage
@ -1930,6 +2135,51 @@ static void BroadcastToConnection(NetworkConnectionToClient connection)
};
connection.Send(message);
}
// 'Unreliable' sync: send Unreliable components over unreliable
// state is 'initial' for reliable baseline, and 'not initial' for unreliable deltas.
// note that syncInterval is always ignored for unreliable in order to have tick aligned [SyncVars].
// even if we pass SyncMethod.Reliable, it serializes with initialState=true.
(NetworkWriter baselineSerialization, NetworkWriter deltaSerialization) = SerializeForConnection_UnreliableComponents(identity, connection, unreliableBaselineElapsed);
// send unreliable delta first. ideally we want this to arrive before the new baseline.
// reliable baseline also clears dirty bits, so unreliable must be sent first.
if (deltaSerialization != null)
{
EntityStateMessageUnreliableDelta message = new EntityStateMessageUnreliableDelta
{
baselineTick = identity.lastUnreliableBaselineSent,
netId = identity.netId,
payload = deltaSerialization.ToArraySegment()
};
connection.Send(message, Channels.Unreliable);
// quake sends unreliable messages twice to make up for message drops.
// this double bandwidth, but allows for smaller buffer time / faster sync.
// best to turn this off unless the game is extremely fast paced.
if (unreliableRedundancy) connection.Send(message, Channels.Unreliable);
}
// if it's for a baseline sync, then send a reliable baseline message too.
// this will likely arrive slightly after the unreliable delta above.
if (unreliableBaselineElapsed)
{
if (baselineSerialization != null)
{
// remember last sent baseline tick for this entity.
// (byte) to minimize bandwidth. we don't need the full tick,
// just something small to compare against.
identity.lastUnreliableBaselineSent = (byte)Time.frameCount;
EntityStateMessageUnreliableBaseline message = new EntityStateMessageUnreliableBaseline
{
baselineTick = identity.lastUnreliableBaselineSent,
netId = identity.netId,
payload = baselineSerialization.ToArraySegment()
};
connection.Send(message, Channels.Reliable);
}
}
}
// spawned list should have no null entries because we
// always call Remove in OnObjectDestroy everywhere.
@ -1968,7 +2218,8 @@ static bool DisconnectIfInactive(NetworkConnectionToClient connection)
internal static readonly List<NetworkConnectionToClient> connectionsCopy =
new List<NetworkConnectionToClient>();
static void Broadcast()
// unreliableFullSendIntervalElapsed: indicates that unreliable sync components need a reliable baseline sync this time.
static void Broadcast(bool unreliableBaselineElapsed)
{
// copy all connections into a helper collection so that
// OnTransportDisconnected can be called while iterating.
@ -2005,7 +2256,7 @@ static void Broadcast()
connection.Send(new TimeSnapshotMessage(), Channels.Unreliable);
// broadcast world state to this connection
BroadcastToConnection(connection);
BroadcastToConnection(connection, unreliableBaselineElapsed);
}
// update connection to flush out batched messages
@ -2061,7 +2312,7 @@ internal static void NetworkLateUpdate()
bool sendIntervalElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime);
bool unreliableBaselineElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, unreliableBaselineInterval, ref lastUnreliableBaselineTime);
if (!Application.isPlaying || sendIntervalElapsed)
Broadcast();
Broadcast(unreliableBaselineElapsed);
}
// process all outgoing messages after updating the world

View File

@ -101,7 +101,7 @@ protected void DrawDefaultSyncSettings()
// Unreliable sync method: show a warning!
if (syncMethod.enumValueIndex == (int)SyncMethod.Unreliable)
{
EditorGUILayout.HelpBox("Beware!\nUnreliable is experimental and only meant for hardcore competitive games!", MessageType.Warning);
EditorGUILayout.HelpBox("Beware!\nUnreliable is experimental, do not use this yet!", MessageType.Warning);
}
// sync interval

View File

@ -11,7 +11,6 @@ GameObject:
- component: {fileID: 4492442352427800}
- component: {fileID: 114118589361100106}
- component: {fileID: 114250499875391520}
- component: {fileID: 3464953498043699706}
- component: {fileID: 114654712548978148}
- component: {fileID: 6900008319038825817}
m_Layer: 0
@ -31,6 +30,7 @@ Transform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 5803173220413450940}
- {fileID: 2155495746218491392}
@ -50,7 +50,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
sceneId: 0
_assetId: 3454335836
_assetId: 2638947628
serverOnly: 0
visibility: 0
hasSpawned: 0
@ -63,9 +63,10 @@ MonoBehaviour:
m_GameObject: {fileID: 1916082411674582}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, type: 3}
m_Script: {fileID: 11500000, guid: d993bac37a92145448c1ea027b3e9ddc, type: 3}
m_Name:
m_EditorClassIdentifier:
syncMethod: 1
syncDirection: 1
syncMode: 0
syncInterval: 0
@ -73,53 +74,22 @@ MonoBehaviour:
syncPosition: 1
syncRotation: 1
syncScale: 0
onlySyncOnChange: 1
compressRotation: 1
onlySyncOnChange: 0
compressRotation: 0
interpolatePosition: 1
interpolateRotation: 1
interpolateScale: 0
coordinateSpace: 0
timelineOffset: 0
timelineOffset: 1
showGizmos: 0
showOverlay: 0
overlayColor: {r: 0, g: 0, b: 0, a: 0.5}
bufferResetMultiplier: 3
positionSensitivity: 0.01
onlySyncOnChangeCorrectionMultiplier: 2
rotationSensitivity: 0.01
scaleSensitivity: 0.01
--- !u!114 &3464953498043699706
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1916082411674582}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, type: 3}
m_Name:
m_EditorClassIdentifier:
syncDirection: 1
syncMode: 0
syncInterval: 0
target: {fileID: 5803173220413450936}
syncPosition: 0
syncRotation: 1
syncScale: 0
onlySyncOnChange: 1
compressRotation: 1
interpolatePosition: 0
interpolateRotation: 1
interpolateScale: 0
coordinateSpace: 0
timelineOffset: 0
showGizmos: 0
showOverlay: 0
overlayColor: {r: 0, g: 0, b: 0, a: 0.5}
bufferResetMultiplier: 3
positionSensitivity: 0.01
rotationSensitivity: 0.01
scaleSensitivity: 0.01
positionPrecision: 0.01
rotationPrecision: 0.001
scalePrecision: 0.01
debugDraw: 1
--- !u!114 &114654712548978148
MonoBehaviour:
m_ObjectHideFlags: 0
@ -132,6 +102,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 7deadf756194d461e9140e42d651693b, type: 3}
m_Name:
m_EditorClassIdentifier:
syncMethod: 1
syncDirection: 0
syncMode: 0
syncInterval: 0.1
@ -196,6 +167,7 @@ Transform:
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 5, z: 0}
m_LocalScale: {x: 0.1, y: 0.1, z: 0.1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4492442352427800}
m_RootOrder: 1
@ -211,10 +183,12 @@ MeshRenderer:
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
@ -239,6 +213,7 @@ MeshRenderer:
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!102 &955977906578811009
TextMesh:
serializedVersion: 3
@ -337,9 +312,9 @@ PrefabInstance:
objectReference: {fileID: 0}
m_RemovedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: dad07e68d3659e6439279d0d4110cf4c, type: 3}
--- !u!4 &5803173220413450940 stripped
--- !u!4 &606281948174800110 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c,
m_CorrespondingSourceObject: {fileID: 7683056980803567927, guid: dad07e68d3659e6439279d0d4110cf4c,
type: 3}
m_PrefabInstance: {fileID: 7130959241934869977}
m_PrefabAsset: {fileID: 0}
@ -355,9 +330,9 @@ Transform:
type: 3}
m_PrefabInstance: {fileID: 7130959241934869977}
m_PrefabAsset: {fileID: 0}
--- !u!4 &606281948174800110 stripped
--- !u!4 &5803173220413450940 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 7683056980803567927, guid: dad07e68d3659e6439279d0d4110cf4c,
m_CorrespondingSourceObject: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c,
type: 3}
m_PrefabInstance: {fileID: 7130959241934869977}
m_PrefabAsset: {fileID: 0}

View File

@ -13,7 +13,7 @@ OcclusionCullingSettings:
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
serializedVersion: 9
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
@ -44,6 +44,7 @@ RenderSettings:
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 12
m_GIWorkflowMode: 1
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
@ -66,6 +67,9 @@ LightmapSettings:
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_FinalGather: 0
m_FinalGatherFiltering: 1
m_FinalGatherRayCount: 256
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 0
@ -100,7 +104,7 @@ NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
serializedVersion: 2
agentTypeID: 0
agentRadius: 2
agentHeight: 3.5
@ -113,7 +117,7 @@ NavMeshSettings:
cellSize: 0.6666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
accuratePlacement: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
@ -151,17 +155,9 @@ Camera:
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_FocalLength: 50
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
@ -195,13 +191,13 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 88936773}
serializedVersion: 2
m_LocalRotation: {x: 0.3420201, y: 0, z: 0, w: 0.9396927}
m_LocalPosition: {x: 0, y: 20, z: -30}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 2
m_LocalEulerAnglesHint: {x: 40, y: 0, z: 0}
--- !u!114 &88936778
MonoBehaviour:
@ -244,13 +240,13 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 251893064}
serializedVersion: 2
m_LocalRotation: {x: 0, y: -0.92387956, z: 0, w: 0.38268343}
m_LocalPosition: {x: 14, y: 0, z: 14}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 4
m_LocalEulerAnglesHint: {x: 0, y: -135, z: 0}
--- !u!114 &251893066
MonoBehaviour:
@ -288,13 +284,13 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 535739935}
serializedVersion: 2
m_LocalRotation: {x: 0, y: -0.38268343, z: 0, w: 0.92387956}
m_LocalPosition: {x: 14, y: 0, z: -14}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 5
m_LocalEulerAnglesHint: {x: 0, y: -45, z: 0}
--- !u!114 &535739937
MonoBehaviour:
@ -315,7 +311,8 @@ LightingSettings:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Settings.lighting
serializedVersion: 8
serializedVersion: 4
m_GIWorkflowMode: 1
m_EnableBakedLightmaps: 0
m_EnableRealtimeLightmaps: 0
m_RealtimeEnvironmentLighting: 1
@ -325,8 +322,6 @@ LightingSettings:
m_UsingShadowmask: 1
m_BakeBackend: 1
m_LightmapMaxSize: 1024
m_LightmapSizeFixed: 0
m_UseMipmapLimits: 1
m_BakeResolution: 40
m_Padding: 2
m_LightmapCompression: 3
@ -344,6 +339,9 @@ LightingSettings:
m_RealtimeResolution: 2
m_ForceWhiteAlbedo: 0
m_ForceUpdates: 0
m_FinalGather: 0
m_FinalGatherRayCount: 256
m_FinalGatherFiltering: 1
m_PVRCulling: 1
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
@ -353,7 +351,7 @@ LightingSettings:
m_LightProbeSampleCountMultiplier: 4
m_PVRBounces: 2
m_PVRMinBounces: 2
m_PVREnvironmentImportanceSampling: 0
m_PVREnvironmentMIS: 1
m_PVRFilteringMode: 2
m_PVRDenoiserTypeDirect: 0
m_PVRDenoiserTypeIndirect: 0
@ -367,7 +365,7 @@ LightingSettings:
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_RespectSceneVisibilityWhenBakingGI: 0
m_PVRTiledBaking: 0
--- !u!1 &1107091652
GameObject:
m_ObjectHideFlags: 0
@ -404,8 +402,6 @@ MeshRenderer:
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_RenderingLayerMask: 4294967295
m_RendererPriority: 0
m_Materials:
@ -439,17 +435,9 @@ MeshCollider:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1107091652}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 5
serializedVersion: 4
m_Convex: 0
m_CookingOptions: 30
m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0}
@ -468,13 +456,13 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1107091652}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 4, y: 1, z: 4}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1282001517
GameObject:
@ -503,13 +491,13 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1282001517}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 3
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1282001519
MonoBehaviour:
@ -541,18 +529,22 @@ MonoBehaviour:
runInBackground: 1
headlessStartMode: 1
editorAutoStart: 0
sendRate: 30
sendRate: 5
unreliableBaselineRate: 1
unreliableRedundancy: 0
autoStartServerBuild: 0
autoConnectClientBuild: 0
offlineScene:
onlineScene:
offlineSceneLoadDelay: 0
transport: {fileID: 1282001521}
networkAddress: localhost
maxConnections: 100
disconnectInactiveConnections: 0
disconnectInactiveTimeout: 60
authenticator: {fileID: 0}
playerPrefab: {fileID: 1916082411674582, guid: 6f43bf5488a7443d19ab2a83c6b91f35, type: 3}
playerPrefab: {fileID: 1916082411674582, guid: 6f43bf5488a7443d19ab2a83c6b91f35,
type: 3}
autoCreatePlayer: 1
playerSpawnMethod: 1
spawnPrefabs:
@ -641,13 +633,13 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1458789072}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0.92387956, z: 0, w: 0.38268343}
m_LocalPosition: {x: -14, y: 0, z: 14}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 6
m_LocalEulerAnglesHint: {x: 0, y: 135, z: 0}
--- !u!114 &1458789074
MonoBehaviour:
@ -685,13 +677,13 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1501912662}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0.38268343, z: 0, w: 0.92387956}
m_LocalPosition: {x: -14, y: 0, z: -14}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 7
m_LocalEulerAnglesHint: {x: 0, y: 45, z: 0}
--- !u!114 &1501912664
MonoBehaviour:
@ -730,8 +722,9 @@ Light:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2054208274}
m_Enabled: 1
serializedVersion: 11
serializedVersion: 10
m_Type: 1
m_Shape: 0
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_Intensity: 1
m_Range: 10
@ -790,23 +783,11 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2054208274}
serializedVersion: 2
m_LocalRotation: {x: 0.10938167, y: 0.8754261, z: -0.40821788, w: 0.23456976}
m_LocalPosition: {x: 0, y: 10, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 50, y: 150, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 2054208276}
- {fileID: 1107091656}
- {fileID: 88936777}
- {fileID: 1282001518}
- {fileID: 251893065}
- {fileID: 535739936}
- {fileID: 1458789073}
- {fileID: 1501912663}

View File

@ -20,10 +20,18 @@ public class Tank : NetworkBehaviour
public Transform projectileMount;
[Header("Stats")]
[SyncVar] public int health = 5;
public int health = 5;
int lastHealth = 5;
void Update()
{
// manual setdirty test
if (health != lastHealth)
{
SetDirty();
lastHealth = health;
}
// always update health bar.
// (SyncVar hook would only update on clients, not on server)
healthBar.text = new string('-', health);
@ -91,5 +99,17 @@ void RotateTurret()
turret.transform.LookAt(lookRotation);
}
}
public override void OnSerialize(NetworkWriter writer, bool initialState)
{
// Debug.LogWarning($"Tank {name} OnSerialize {(initialState ? "full" : "delta")} health={health}");
writer.WriteInt(health);
}
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
health = reader.ReadInt();
// Debug.LogWarning($"Tank {name} OnDeserialize {(initialState ? "full" : "delta")} health={health}");
}
}
}

View File

@ -219,7 +219,7 @@ public void SerializeServer_NotInitial_NotDirty_WritesNothing()
// serialize server object.
// 'initial' would write everything.
// instead, try 'not initial' with 0 dirty bits
serverIdentity.SerializeServer_Broadcast(ownerWriter, observersWriter);
serverIdentity.SerializeServer_Broadcast(ownerWriter, observersWriter, new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), false);
Assert.That(ownerWriter.Position, Is.EqualTo(0));
Assert.That(observersWriter.Position, Is.EqualTo(0));
}
@ -243,7 +243,7 @@ public void SerializeClient_NotInitial_NotDirty_WritesNothing()
// clientComp.value = "42";
// serialize client object
clientIdentity.SerializeClient(ownerWriter);
clientIdentity.SerializeClient(ownerWriter, new NetworkWriter(), new NetworkWriter(), false);
Assert.That(ownerWriter.Position, Is.EqualTo(0));
}
@ -267,7 +267,7 @@ public void SerializeAndDeserialize_ClientToServer_NOT_OWNED()
comp2.value = "67890";
// serialize all
identity.SerializeClient(ownerWriter);
identity.SerializeClient(ownerWriter, new NetworkWriter(), new NetworkWriter(), false);
// shouldn't sync anything. because even though it's ClientToServer,
// we don't own this one so we shouldn't serialize & sync it.
@ -302,7 +302,7 @@ public void SerializeServer_OwnerMode_ClientToServer()
comp.SetValue(22); // modify with helper function to avoid #3525
ownerWriter.Position = 0;
observersWriter.Position = 0;
identity.SerializeServer_Broadcast(ownerWriter, observersWriter);
identity.SerializeServer_Broadcast(ownerWriter, observersWriter, new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), false);
Debug.Log("delta ownerWriter: " + ownerWriter);
Debug.Log("delta observersWriter: " + observersWriter);
Assert.That(ownerWriter.Position, Is.EqualTo(0));
@ -340,7 +340,7 @@ public void SerializeServer_ObserversMode_ClientToServer()
comp.SetValue(22); // modify with helper function to avoid #3525
ownerWriter.Position = 0;
observersWriter.Position = 0;
identity.SerializeServer_Broadcast(ownerWriter, observersWriter);
identity.SerializeServer_Broadcast(ownerWriter, observersWriter, new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), new NetworkWriter(), false);
Debug.Log("delta ownerWriter: " + ownerWriter);
Debug.Log("delta observersWriter: " + observersWriter);
Assert.That(ownerWriter.Position, Is.EqualTo(0));

View File

@ -66,10 +66,10 @@ public IEnumerator TestSerializationWithLargeTimestamps()
// 14 * 24 hours per day * 60 minutes per hour * 60 seconds per minute = 14 days
// NOTE: change this to 'float' to see the tests fail
int tick = 14 * 24 * 60 * 60;
NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(tick);
NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(tick, false);
// advance tick
++tick;
NetworkIdentitySerialization serializationNew = identity.GetServerSerializationAtTick(tick);
NetworkIdentitySerialization serializationNew = identity.GetServerSerializationAtTick(tick, false);
// if the serialization has been changed the tickTimeStamp should have moved
Assert.That(serialization.tick == serializationNew.tick, Is.False);