Skip to content

Instantly share code, notes, and snippets.

@James-Frowen
Last active August 31, 2020 11:17
Show Gist options
  • Save James-Frowen/6026ff26ef08768b00a851299fd29614 to your computer and use it in GitHub Desktop.
Save James-Frowen/6026ff26ef08768b00a851299fd29614 to your computer and use it in GitHub Desktop.
Allows syncing of NetworkBehaviours in child objects.
using System;
using System.Linq;
using Mirror;
using UnityEngine;
namespace JamesFrowen.MirrorExamples
{
/// <summary>
/// Sync NetworkBehviours in chuld objects
/// </summary>
public class ChildNetworkBehaviours : NetworkBehaviour
{
static readonly ILogger logger = LogFactory.GetLogger<ChildNetworkBehaviours>();
NetworkBehaviour[] networkBehavioursCache;
NetworkBehaviour[] NetworkBehaviours
{
get
{
if (networkBehavioursCache == null)
{
networkBehavioursCache = FindChildBehaviours();
}
return networkBehavioursCache;
}
}
NetworkBehaviour[] FindChildBehaviours()
{
// get all behaviours including one on this object
return GetComponentsInChildren<NetworkBehaviour>(true)
// only include ones on child objects
.Where(x => x.gameObject != gameObject)
.ToArray();
}
ulong GetDirtyComponentsMask()
{
// loop through all components only once and then write dirty+payload into the writer afterwards
ulong dirtyComponentsMask = 0L;
NetworkBehaviour[] components = NetworkBehaviours;
for (int i = 0; i < components.Length; ++i)
{
NetworkBehaviour comp = components[i];
if (comp.IsDirty())
{
dirtyComponentsMask |= 1UL << i;
}
}
return dirtyComponentsMask;
}
public override bool OnSerialize(NetworkWriter writer, bool initialState)
{
ulong dirtyMask = GetDirtyComponentsMask();
OnSerializeAllSafely(initialState, dirtyMask, writer, out int ownerWritten);
if (ownerWritten > 0)
{
ClearDirtyComponentsDirtyBits();
}
return ownerWritten > 0;
}
void OnSerializeAllSafely(bool initialState, ulong dirtyComponentsMask, NetworkWriter ownerWriter, out int ownerWritten)
{
// clear 'written' variables
ownerWritten = 0;
// dirtyComponentsMask should be changed before tyhis function is called
Debug.Assert(dirtyComponentsMask != 0UL, "OnSerializeAllSafely Should not be given a zero dirtyComponentsMask", this);
// write regular dirty mask for owner,
// writer 'dirty mask & syncMode==Everyone' for everyone else
// (WritePacked64 so we don't write full 8 bytes if we don't have to)
ownerWriter.WritePackedUInt64(dirtyComponentsMask);
foreach (NetworkBehaviour comp in NetworkBehaviours)
{
// 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
if (initialState || comp.IsDirty())
{
if (logger.LogEnabled()) logger.Log("OnSerializeAllSafely: " + name + " -> " + comp.GetType() + " initial=" + initialState);
// serialize into ownerWriter first
// (owner always gets everything!)
int startPosition = ownerWriter.Position;
OnSerializeSafely(comp, ownerWriter, initialState);
++ownerWritten;
}
}
}
bool OnSerializeSafely(NetworkBehaviour comp, NetworkWriter writer, bool initialState)
{
// write placeholder length bytes
// (jumping back later is WAY faster than allocating a temporary
// writer for the payload, then writing payload.size, payload)
int headerPosition = writer.Position;
writer.WriteInt32(0);
int contentPosition = writer.Position;
// write payload
bool result = false;
try
{
result = comp.OnSerialize(writer, initialState);
}
catch (Exception e)
{
// show a detailed error and let the user know what went wrong
logger.LogError("OnSerialize failed for: object=" + name + " component=" + comp.GetType() + " sceneId=" + netIdentity.sceneId.ToString("X") + "\n\n" + e);
}
int endPosition = writer.Position;
// fill in length now
writer.Position = headerPosition;
writer.WriteInt32(endPosition - contentPosition);
writer.Position = endPosition;
if (logger.LogEnabled()) logger.Log("OnSerializeSafely written for object=" + comp.name + " component=" + comp.GetType() + " sceneId=" + netIdentity.sceneId.ToString("X") + "header@" + headerPosition + " content@" + contentPosition + " end@" + endPosition + " contentSize=" + (endPosition - contentPosition));
return result;
}
void ClearDirtyComponentsDirtyBits()
{
foreach (NetworkBehaviour comp in NetworkBehaviours)
{
if (comp.IsDirty())
{
comp.ClearAllDirtyBits();
}
}
}
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
OnDeserializeAllSafely(reader, initialState);
}
void OnDeserializeAllSafely(NetworkReader reader, bool initialState)
{
// read component dirty mask
ulong dirtyComponentsMask = reader.ReadPackedUInt64();
NetworkBehaviour[] components = NetworkBehaviours;
// loop through all components and deserialize the dirty ones
for (int i = 0; i < components.Length; ++i)
{
// is the dirty bit at position 'i' set to 1?
ulong dirtyBit = 1UL << i;
if ((dirtyComponentsMask & dirtyBit) != 0L)
{
OnDeserializeSafely(components[i], reader, initialState);
}
}
}
void OnDeserializeSafely(NetworkBehaviour comp, NetworkReader reader, bool initialState)
{
// read header as 4 bytes and calculate this chunk's start+end
int contentSize = reader.ReadInt32();
int chunkStart = reader.Position;
int chunkEnd = reader.Position + contentSize;
// call OnDeserialize and wrap it in a try-catch block so there's no
// way to mess up another component's deserialization
try
{
if (logger.LogEnabled()) logger.Log("OnDeserializeSafely: " + comp.name + " component=" + comp.GetType() + " sceneId=" + sceneId.ToString("X") + " length=" + contentSize);
comp.OnDeserialize(reader, initialState);
}
catch (Exception e)
{
// show a detailed error and let the user know what went wrong
logger.LogError($"OnDeserialize failed for: object={name} component={comp.GetType()} sceneId={sceneId.ToString("X")} length={contentSize}. Possible Reasons:\n" +
$" * Do {comp.GetType()}'s OnSerialize and OnDeserialize calls write the same amount of data({contentSize} bytes)? \n" +
$" * Was there an exception in {comp.GetType()}'s OnSerialize/OnDeserialize code?\n" +
$" * Are the server and client the exact same project?\n" +
$" * Maybe this OnDeserialize call was meant for another GameObject? The sceneIds can easily get out of sync if the Hierarchy was modified only in the client OR the server. Try rebuilding both.\n\n" +
$"Exeption {e}");
}
// now the reader should be EXACTLY at 'before + size'.
// otherwise the component read too much / too less data.
if (reader.Position != chunkEnd)
{
// warn the user
int bytesRead = reader.Position - chunkStart;
logger.LogWarning("OnDeserialize was expected to read " + contentSize + " instead of " + bytesRead + " bytes for object:" + name + " component=" + comp.GetType() + " sceneId=" + sceneId.ToString("X") + ". Make sure that OnSerialize and OnDeserialize write/read the same amount of data in all cases.");
// fix the position, so the following components don't all fail
reader.Position = chunkEnd;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment