mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
quake jitter single commit, no compression, always sync, no corrections
This commit is contained in:
parent
5cc8050d47
commit
e29ab56a20
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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:
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,35 +926,97 @@ internal void OnStopLocalPlayer()
|
||||
NetworkBehaviour component = components[i];
|
||||
ulong nthBit = (1u << i);
|
||||
|
||||
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)
|
||||
ownerMask |= 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)
|
||||
// RELIABLE COMPONENTS /////////////////////////////////////////
|
||||
if (component.syncMethod == SyncMethod.Reliable)
|
||||
{
|
||||
// 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;
|
||||
// check if this component is dirty
|
||||
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)
|
||||
ownerMaskReliable |= 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) observerMaskReliable |= nthBit;
|
||||
}
|
||||
}
|
||||
// UNRELIABLE COMPONENTS ///////////////////////////////////////
|
||||
else if (component.syncMethod == SyncMethod.Unreliable)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
////////////////////////////////////////////////////////////
|
||||
}
|
||||
}
|
||||
|
||||
return (ownerMask, observerMask);
|
||||
}
|
||||
|
||||
// build dirty mask for client.
|
||||
// build dirty mask for client components.
|
||||
// server always knows initialState, so we don't need it here.
|
||||
ulong ClientDirtyMask()
|
||||
// -> 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)
|
||||
{
|
||||
ulong mask = 0;
|
||||
dirtyMaskReliable = 0;
|
||||
dirtyMaskUnreliableBaseline = 0;
|
||||
dirtyMaskUnreliableDelta = 0;
|
||||
|
||||
NetworkBehaviour[] components = NetworkBehaviours;
|
||||
for (int i = 0; i < components.Length; ++i)
|
||||
@ -936,13 +1034,29 @@ ulong ClientDirtyMask()
|
||||
|
||||
if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
|
||||
{
|
||||
// set the n-th bit if dirty
|
||||
// shifting from small to large numbers is varint-efficient.
|
||||
if (component.IsDirty()) mask |= nthBit;
|
||||
// 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()) 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.
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -20,16 +20,24 @@ 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);
|
||||
|
||||
|
||||
// take input from focused window only
|
||||
if(!Application.isFocused) return;
|
||||
if(!Application.isFocused) return;
|
||||
|
||||
// movement for local player
|
||||
if (isLocalPlayer)
|
||||
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user