Skip to content

Commit

Permalink
refactor!: starting to use syncSettings
Browse files Browse the repository at this point in the history
Starting to use syncSettings,
replacing interval,
refactoring some of the helper methods

BREAKING CHANGE: Renaming and Obsolete of custom serialize methods
  • Loading branch information
James-Frowen committed Mar 31, 2023
1 parent 53ceb2a commit f673f9b
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 62 deletions.
86 changes: 62 additions & 24 deletions Assets/Mirage/Runtime/NetworkBehaviour.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Mirage.Collections;
using Mirage.Logging;
using Mirage.RemoteCalls;
Expand All @@ -11,8 +12,10 @@ namespace Mirage
/// <summary>
/// Sync to everyone, or only to owner.
/// </summary>
[System.Obsolete("Use new SyncSettings instead", true)]
public enum SyncMode { Observers, Owner }


/// <summary>
/// Base class which should be inherited by scripts which contain networking functionality.
///
Expand All @@ -28,21 +31,29 @@ public abstract class NetworkBehaviour : MonoBehaviour
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(NetworkBehaviour));

internal float _lastSyncTime;
// protected because it is ok for child classes to set this if they want
protected internal float _nextSyncTime;

/// <summary>
/// Sync settings for this NetworkBehaviour
/// <para>Settings will be hidden in inspector unless Behaviour has SyncVar or SyncObjects</para>
/// </summary>
public SyncSettings SyncSettings = SyncSettings.Default;

// hidden because NetworkBehaviourInspector shows it only if has OnSerialize.
/// <summary>
/// sync mode for OnSerialize
/// </summary>
[System.Obsolete("Use new SyncSettings instead", true)]
[HideInInspector] public SyncMode syncMode = SyncMode.Observers;

// hidden because NetworkBehaviourInspector shows it only if has OnSerialize.
/// <summary>
/// sync interval for OnSerialize (in seconds)
/// </summary>
[Tooltip("Time in seconds until next change is synchronized to the client. '0' means send immediately if changed. '0.5' means only send changes every 500ms.\n(This is for state synchronization like SyncVars, SyncLists, OnSerialize. Not for Cmds, Rpcs, etc.)")]
// [0,2] should be enough. anything >2s is too laggy anyway.
[Range(0, 2)]
[System.Obsolete("Use new SyncSettings instead", true)]
[HideInInspector] public float syncInterval = 0.1f;

/// <summary>
Expand Down Expand Up @@ -127,7 +138,11 @@ public abstract class NetworkBehaviour : MonoBehaviour
/// <returns></returns>
public Id BehaviourId => new Id(this);

protected internal ulong SyncVarDirtyBits { get; private set; }
private ulong _syncVarDirtyBits;
private bool _anySyncObjectDirty;

protected internal ulong SyncVarDirtyBits => _syncVarDirtyBits;
protected internal bool AnySyncObjectDirty => _anySyncObjectDirty;

private ulong _syncVarHookGuard;

Expand Down Expand Up @@ -256,8 +271,9 @@ protected internal void InitSyncObject(ISyncObject syncObject)

private void SyncObject_OnChange()
{
if (IsServer)
if (IsServer) // todo change to syncfrom
{
_anySyncObjectDirty = true;
Server.SyncVarSender.AddDirtyObject(Identity);
}
}
Expand All @@ -275,7 +291,7 @@ protected internal bool SyncVarEqual<T>(T value, T fieldValue)
/// <param name="dirtyBit">Bit mask to set.</param>
public void SetDirtyBit(ulong dirtyBit)
{
SyncVarDirtyBits |= dirtyBit;
_syncVarDirtyBits |= dirtyBit;
if (IsServer)
Server.SyncVarSender.AddDirtyObject(Identity);
}
Expand All @@ -284,27 +300,22 @@ public void SetDirtyBit(ulong dirtyBit)
/// This clears all the dirty bits that were set on this script by SetDirtyBits();
/// <para>This is automatically invoked when an update is sent for this object, but can be called manually as well.</para>
/// </summary>
public void ClearAllDirtyBits()
public void ClearAllDirtyBits(float now)
{
_lastSyncTime = Time.time;
SyncVarDirtyBits = 0L;
SyncSettings.UpdateTime(ref _nextSyncTime, now);
_syncVarDirtyBits = 0L;

// flush all unsynchronized changes in syncobjects
// note: don't use List.ForEach here, this is a hot path
// List.ForEach: 432b/frame
// for: 231b/frame
for (var i = 0; i < syncObjects.Count; ++i)
for (var i = 0; i < syncObjects.Count; i++)
{
syncObjects[i].Flush();
}
_anySyncObjectDirty = false;
}

private bool AnySyncObjectDirty()
private bool CheckSyncObjectDirty() // todo can we remove this?
{
// note: don't use Linq here. 1200 networked objects:
// Linq: 187KB GC/frame;, 2.66ms time
// for: 8KB GC/frame; 1.28ms time
for (var i = 0; i < syncObjects.Count; ++i)
for (var i = 0; i < syncObjects.Count; i++)
{
if (syncObjects[i].IsDirty)
{
Expand All @@ -314,21 +325,48 @@ private bool AnySyncObjectDirty()
return false;
}

public bool IsDirty()
/// <summary>
/// True if this behaviour is dirty and it is time to sync
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldSync(float time)
{
if (Time.time - _lastSyncTime >= syncInterval)
{
return SyncVarDirtyBits != 0L || AnySyncObjectDirty();
}
return false;
return AnyDirtyBits() && TimeToSync(time);
}

/// <summary>
/// If it is time to sync based on last sync and <see cref="SyncSettings"/>
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TimeToSync(float time)
{
return time >= _nextSyncTime;
}

/// <summary>
/// Are any SyncVar or SyncObjects dirty
/// </summary>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool AnyDirtyBits()
{
return SyncVarDirtyBits != 0L || AnySyncObjectDirty;
}

// old version of ShouldSync, name isn't great so use
[System.Obsolete("Use ShouldSync instead", true)]
public bool IsDirty(float time) => ShouldSync(time);

// true if this component has data that has not been
// synchronized. Note that it may not synchronize
// right away because of syncInterval
public bool StillDirty()
{
return SyncVarDirtyBits != 0L || AnySyncObjectDirty();
return SyncVarDirtyBits != 0L || AnySyncObjectDirty;
}

/// <summary>
Expand Down
92 changes: 54 additions & 38 deletions Assets/Mirage/Runtime/NetworkIdentity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -633,55 +633,64 @@ private void OnSerialize(NetworkBehaviour comp, NetworkWriter writer, bool initi
/// <para>We pass dirtyComponentsMask into this function so that we can check if any Components are dirty before creating writers</para>
/// </summary>
/// <param name="initialState"></param>
/// <param name="ownerWriter"></param>
/// <param name="mainWriter"></param>
/// <param name="observersWriter"></param>
internal (int ownerWritten, int observersWritten) OnSerializeAll(bool initialState, NetworkWriter ownerWriter, NetworkWriter observersWriter)
internal (int ownerWritten, int observersWritten) OnSerializeAll(bool initialState, NetworkWriter mainWriter, NetworkWriter observersWriter)
{
var ownerWritten = 0;
var observersWritten = 0;

// check if components are in byte.MaxRange just to be 100% sure
// that we avoid overflows
var components = NetworkBehaviours;
// store time as variable so we dont have to call property for each component
var now = Time.time;

// serialize all components
for (var i = 0; i < components.Length; ++i)
{
// is this component dirty?
// -> always serialize if initialState so all components are included in spawn packet
// -> note: IsDirty() is false if the component isn't dirty or sendInterval isn't elapsed yet
var comp = components[i];
if (initialState || comp.IsDirty())

// check if we should be writing this components
if (!comp.SyncSettings.ShouldSyncFrom(this))
continue;

// always sync for initial
// so check ShouldSync if initial is false
if (!initialState && !comp.ShouldSync(now))
continue;

if (logger.LogEnabled()) logger.Log($"OnSerializeAllSafely: '{name}', component '{comp.GetType()}', initial state: '{initialState}'");

// remember start position in case we need to copy it into
// observers writer too
var startBitPosition = mainWriter.BitPosition;

// write index as byte [0..255]
mainWriter.WriteByte((byte)i);

// serialize into ownerWriter first
// (owner always gets everything!)
OnSerialize(comp, mainWriter, initialState);
ownerWritten++;

// copy into observersWriter too if SyncMode.Observers
// -> we copy instead of calling OnSerialize again because
// we don't know what magic the user does in OnSerialize.
// -> it's not guaranteed that calling it twice gets the
// same result
// -> it's not guaranteed that calling it twice doesn't mess
// with the user's OnSerialize timing code etc.
// => so we just copy the result without touching
// OnSerialize again


// should copy to observer writer
if (comp.SyncSettings.CopyToObservers())
{
if (logger.LogEnabled()) logger.Log($"OnSerializeAllSafely: '{name}', component '{comp.GetType()}', initial state: '{initialState}'");

// remember start position in case we need to copy it into
// observers writer too
var startBitPosition = ownerWriter.BitPosition;

// write index as byte [0..255]
ownerWriter.WriteByte((byte)i);

// serialize into ownerWriter first
// (owner always gets everything!)
OnSerialize(comp, ownerWriter, initialState);
ownerWritten++;

// copy into observersWriter too if SyncMode.Observers
// -> we copy instead of calling OnSerialize again because
// we don't know what magic the user does in OnSerialize.
// -> it's not guaranteed that calling it twice gets the
// same result
// -> it's not guaranteed that calling it twice doesn't mess
// with the user's OnSerialize timing code etc.
// => so we just copy the result without touching
// OnSerialize again
if (comp.syncMode == SyncMode.Observers)
{
var bitLength = ownerWriter.BitPosition - startBitPosition;
observersWriter.CopyFromWriter(ownerWriter, startBitPosition, bitLength);
observersWritten++;
}
var bitLength = mainWriter.BitPosition - startBitPosition;
observersWriter.CopyFromWriter(mainWriter, startBitPosition, bitLength);
observersWritten++;
}
}

Expand Down Expand Up @@ -1119,9 +1128,12 @@ internal void SendToRemoteObservers<T>(T msg, bool includeOwner = true, int chan
/// </summary>
internal void ClearAllComponentsDirtyBits()
{
// store time as variable so we dont have to call property for each component
var now = Time.time;

foreach (var comp in NetworkBehaviours)
{
comp.ClearAllDirtyBits();
comp.ClearAllDirtyBits(now);
}
}

Expand All @@ -1131,11 +1143,15 @@ internal void ClearAllComponentsDirtyBits()
/// </summary>
internal void ClearDirtyComponentsDirtyBits()
{
// store time as variable so we dont have to call property for each component
var now = Time.time;

foreach (var comp in NetworkBehaviours)
{
if (comp.IsDirty())
// todo this seems weird, should we be clearing this somewhere else?
if (comp.ShouldSync(now))
{
comp.ClearAllDirtyBits();
comp.ClearAllDirtyBits(now);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions Assets/Mirage/Runtime/SyncVarSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal void Update()

identity.SendUpdateVarsMessage();

// todo, why didn't it sync? is it from interval? can we return still dirty from SendUpdateVarsMessage, instead of having to recheck everything?
if (identity.StillDirty())
_dirtyObjectsTmp.Add(identity);
}
Expand Down

0 comments on commit f673f9b

Please sign in to comment.