Skip to content

Commit

Permalink
feat: SyncVar support arbitrary NetworkBehavior (#514)
Browse files Browse the repository at this point in the history
Similar to NetworkIdentity and GameObjects, now we can synchronize
NetworkBehaviors in a lazy fashion. Meaning even if the target
object is spawned at a later time, it will still be found
  • Loading branch information
paulpach committed Dec 29, 2020
1 parent 5704e44 commit 67b0c9f
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 21 deletions.
38 changes: 21 additions & 17 deletions Assets/Mirror/Editor/Weaver/Processors/SyncVarProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ namespace Mirror.Weaver
/// </summary>
public class SyncVarProcessor
{

private readonly List<FieldDefinition> syncVars = new List<FieldDefinition>();

// store the unwrapped types for every field
private readonly Dictionary<FieldDefinition, TypeReference> originalTypes = new Dictionary<FieldDefinition, TypeReference>();

// ulong = 64 bytes
const int SyncVarLimit = 64;


static string HookParameterMessage(string hookName, TypeReference ValueType)
=> string.Format("void {0}({1} oldValue, {1} newValue)", hookName, ValueType);

Expand Down Expand Up @@ -214,13 +217,13 @@ private static void LoadField(FieldDefinition fd, ILProcessor worker)
}
}

static void ProcessSyncVar(FieldDefinition fd, long dirtyBit)
void ProcessSyncVar(FieldDefinition fd, long dirtyBit)
{
string originalName = fd.Name;
Weaver.DLog(fd.DeclaringType, "Sync Var " + fd.Name + " " + fd.FieldType);

TypeReference originalType = fd.FieldType;
fd.FieldType = WrapType(fd.Module, fd.FieldType);
fd.FieldType = WrapType(fd);

MethodDefinition get = GenerateSyncVarGetter(fd, originalName, originalType);
MethodDefinition set = GenerateSyncVarSetter(fd, originalName, dirtyBit, originalType);
Expand All @@ -244,34 +247,35 @@ static void ProcessSyncVar(FieldDefinition fd, long dirtyBit)
}
}

private static TypeReference WrapType(ModuleDefinition module, TypeReference typeReference)
private TypeReference WrapType(FieldDefinition syncvar)
{
TypeReference typeReference = syncvar.FieldType;

originalTypes[syncvar] = typeReference;
if (typeReference.Is<NetworkIdentity>())
{
// change the type of the field to a wrapper NetworkIDentitySyncvar
return module.ImportReference<NetworkIdentitySyncvar>();
return syncvar.Module.ImportReference<NetworkIdentitySyncvar>();
}
if (typeReference.Is<GameObject>())
return module.ImportReference<GameObjectSyncvar>();
return syncvar.Module.ImportReference<GameObjectSyncvar>();

if (typeReference.Resolve().IsDerivedFrom<NetworkBehaviour>())
return syncvar.Module.ImportReference<NetworkBehaviorSyncvar>();

return typeReference;
}

private static TypeReference UnwrapType(ModuleDefinition module, TypeReference typeReference)
private TypeReference UnwrapType(FieldDefinition syncvar)
{
if (typeReference.Is<NetworkIdentitySyncvar>())
{
// change the type of the field to a wrapper NetworkIDentitySyncvar
return module.ImportReference<NetworkIdentity>();
}
if (typeReference.Is<GameObjectSyncvar>())
return module.ImportReference<GameObject>();
return typeReference;
return originalTypes[syncvar];
}

private static bool IsWrapped(TypeReference typeReference)
{
return typeReference.Is<NetworkIdentitySyncvar>() ||
typeReference.Is<GameObjectSyncvar>();
typeReference.Is<GameObjectSyncvar>() ||
typeReference.Is<NetworkBehaviorSyncvar>();
}

public void ProcessSyncVars(TypeDefinition td)
Expand Down Expand Up @@ -619,7 +623,7 @@ void GenerateDeSerialization(TypeDefinition netBehaviourSubclass)
/// <param name="hookResult"></param>
void DeserializeField(FieldDefinition syncVar, ILProcessor serWorker, MethodDefinition deserialize)
{
TypeReference originalType = UnwrapType(syncVar.Module, syncVar.FieldType);
TypeReference originalType = UnwrapType(syncVar);
MethodDefinition hookMethod = GetHookMethod(syncVar, originalType);

/*
Expand Down
82 changes: 82 additions & 0 deletions Assets/Mirror/Runtime/NetworkBehaviorSyncvar.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
namespace Mirror
{

/// <summary>
/// backing struct for a NetworkIdentity when used as a syncvar
/// the weaver will replace the syncvar with this struct.
/// </summary>
public struct NetworkBehaviorSyncvar
{
/// <summary>
/// The network client that spawned the parent object
/// used to lookup the identity if it exists
/// </summary>
internal NetworkClient client;
internal uint netId;
internal int componentId;

internal NetworkBehaviour component;

internal uint NetId => component != null ? component.NetId : netId;
internal int ComponentId => component != null ? component.ComponentIndex : componentId;

public NetworkBehaviour Value
{
get
{
if (component != null)
return component;

if (client != null)
{
client.Spawned.TryGetValue(netId, out NetworkIdentity identity);
if (identity != null)
return identity.NetworkBehaviours[componentId];
}

return null;
}

set
{
if (value == null)
{
netId = 0;
componentId = 0;
}
component = value;
}
}
}


public static class NetworkBehaviorSerializers
{
public static void WriteNetworkIdentity(this NetworkWriter writer, NetworkBehaviorSyncvar id)
{
writer.WritePackedUInt32(id.NetId);
writer.WritePackedInt32(id.ComponentId);
}

public static NetworkBehaviorSyncvar ReadNetworkIdentity(this NetworkReader reader)
{
uint netId = reader.ReadPackedUInt32();
int componentId = reader.ReadPackedInt32();

NetworkIdentity identity = null;
if (!(reader.Client is null))
reader.Client.Spawned.TryGetValue(netId, out identity);

if (!(reader.Server is null))
reader.Server.Spawned.TryGetValue(netId, out identity);

return new NetworkBehaviorSyncvar
{
client = reader.Client,
netId = netId,
componentId = componentId,
component = identity != null ? identity.NetworkBehaviours[componentId] : null
};
}
}
}
11 changes: 11 additions & 0 deletions Assets/Mirror/Runtime/NetworkBehaviorSyncvar.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions Assets/Tests/Editor/Weaver/WeaverSyncVarTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ public void SyncVarArraySegment()
[Test]
public void SyncVarsDerivedNetworkBehaviour()
{
HasError("Cannot generate writer for component type MyBehaviour. Use a supported type or provide a custom writer",
"WeaverSyncVarTests.SyncVarsDerivedNetworkBehaviour.MyBehaviour");
HasError("invalidVar has unsupported type. Use a supported MirrorNG type instead",
"WeaverSyncVarTests.SyncVarsDerivedNetworkBehaviour.MyBehaviour WeaverSyncVarTests.SyncVarsDerivedNetworkBehaviour.SyncVarsDerivedNetworkBehaviour::invalidVar");
IsSuccess();
}

[Test]
Expand Down
76 changes: 76 additions & 0 deletions Assets/Tests/Runtime/NetworkBehaviorSyncvarTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Collections;
using Cysharp.Threading.Tasks;
using NUnit.Framework;
using UnityEngine.TestTools;

namespace Mirror.Tests
{
public class SampleBehaviorWithNB : NetworkBehaviour
{
[SyncVar]
public SampleBehaviorWithNB target;
}

public class NetworkBehaviorSyncvarTest : ClientServerSetup<SampleBehaviorWithNB>
{
[Test]
public void IsNullByDefault()
{
// out of the box, target should be null in the client

Assert.That(clientComponent.target, Is.Null);
}

[UnityTest]
public IEnumerator ChangeTarget() => UniTask.ToCoroutine(async () =>
{
serverComponent.target = serverComponent;
await UniTask.WaitUntil(() => clientComponent.target != null);
Assert.That(clientComponent.target, Is.SameAs(clientComponent));
});

[Test]
public void UpdateAfterSpawn()
{
// this situation can happen when the client does nto see an object
// but the object is assigned in a syncvar.
// this can easily happen during spawn if spawning in an unexpected order
// or if there is AOI in play.
// in this case we would have a valid net id, but we would not
// find the object at spawn time

var goSyncvar = new NetworkBehaviorSyncvar
{
client = client,
netId = serverIdentity.NetId,
component = null,
};

Assert.That(goSyncvar.Value, Is.SameAs(clientComponent));
}

[UnityTest]
public IEnumerator SpawnWithTarget() => UniTask.ToCoroutine(async () =>
{
// create an object, set the target and spawn it
UnityEngine.GameObject newObject = UnityEngine.Object.Instantiate(playerPrefab);
SampleBehaviorWithNB newBehavior = newObject.GetComponent<SampleBehaviorWithNB>();
newBehavior.target = serverComponent;
serverObjectManager.Spawn(newObject);
// wait until the client spawns it
uint newObjectId = newBehavior.NetId;
await UniTask.WaitUntil(() => client.Spawned.ContainsKey(newObjectId));
// check if the target was set correctly in the client
NetworkIdentity newClientObject = client.Spawned[newObjectId];
SampleBehaviorWithNB newClientBehavior = newClientObject.GetComponent<SampleBehaviorWithNB>();
Assert.That(newClientBehavior.target, Is.SameAs(clientComponent));
// cleanup
serverObjectManager.Destroy(newObject);
});
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/Runtime/NetworkBehaviorSyncvarTest.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 67b0c9f

Please sign in to comment.