Skip to content

Commit 5a2b607

Browse files
feat: Added GenerateSerializationForGenericParameterAttribute and GenerateSerializationForTypeAttribute (#2694)
This enables users to control the generation of serialization code through codegen, making it easier to create their own network variable subtypes, and also making it possible for them to easily generate serialization for specific types they know they need to use. NetworkVariable and NetworkList have been changed to use these attributes so they are no longer special cases in our codegen. This PR also exposes methods in `NetworkVariableSerialization<T>` to further support this type of serialization need. resolves #2686 --------- Co-authored-by: Noel Stephens <[email protected]>
1 parent c84a1dc commit 5a2b607

11 files changed

+406
-55
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ Additional documentation and release notes are available at [Multiplayer Documen
1212

1313
- Added context menu tool that provides users with the ability to quickly update the GlobalObjectIdHash value for all in-scene placed prefab instances that were created prior to adding a NetworkObject component to it. (#2707)
1414
- Added methods NetworkManager.SetPeerMTU and NetworkManager.GetPeerMTU to be able to set MTU sizes per-peer (#2676)
15+
- Added `GenerateSerializationForGenericParameterAttribute`, which can be applied to user-created Network Variable types to ensure the codegen generates serialization for the generic types they wrap. (#2694)
16+
- Added `GenerateSerializationForTypeAttribute`, which can be applied to any class or method to ensure the codegen generates serialization for the specific provided type. (#2694)
17+
- Exposed `NetworkVariableSerialization<T>.Read`, `NetworkVariableSerialization<T>.Write`, `NetworkVariableSerialization<T>.AreEqual`, and `NetworkVariableSerialization<T>.Duplicate` to further support the creation of user-created network variables by allowing users to access the generated serialization methods and serialize generic types efficiently without boxing. (#2694)
18+
- Added `NetworkVariableBase.MarkNetworkBehaviourDirty` so that user-created network variable types can mark their containing `NetworkBehaviour` to be processed by the update loop. (#2694)
1519

1620
### Fixed
1721

com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,37 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
7676
.ToList()
7777
.ForEach(b => ProcessNetworkBehaviour(b, compiledAssembly.Defines));
7878

79+
foreach (var type in mainModule.GetTypes())
80+
{
81+
var resolved = type.Resolve();
82+
foreach (var attribute in resolved.CustomAttributes)
83+
{
84+
if (attribute.AttributeType.Name == nameof(GenerateSerializationForTypeAttribute))
85+
{
86+
var wrappedType = mainModule.ImportReference((TypeReference)attribute.ConstructorArguments[0].Value);
87+
if (!m_WrappedNetworkVariableTypes.Contains(wrappedType))
88+
{
89+
m_WrappedNetworkVariableTypes.Add(wrappedType);
90+
}
91+
}
92+
}
93+
94+
foreach (var method in resolved.Methods)
95+
{
96+
foreach (var attribute in method.CustomAttributes)
97+
{
98+
if (attribute.AttributeType.Name == nameof(GenerateSerializationForTypeAttribute))
99+
{
100+
var wrappedType = mainModule.ImportReference((TypeReference)attribute.ConstructorArguments[0].Value);
101+
if (!m_WrappedNetworkVariableTypes.Contains(wrappedType))
102+
{
103+
m_WrappedNetworkVariableTypes.Add(wrappedType);
104+
}
105+
}
106+
}
107+
}
108+
}
109+
79110
CreateNetworkVariableTypeInitializers(assemblyDefinition);
80111
}
81112
catch (Exception e)
@@ -196,10 +227,7 @@ private void CreateNetworkVariableTypeInitializers(AssemblyDefinition assembly)
196227
equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_UnmanagedValueEqualsArray_MethodRef);
197228
}
198229

199-
if (serializeMethod != null)
200-
{
201-
serializeMethod.GenericArguments.Add(wrappedType);
202-
}
230+
serializeMethod?.GenericArguments.Add(wrappedType);
203231
equalityMethod.GenericArguments.Add(wrappedType);
204232
}
205233
#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT
@@ -259,10 +287,7 @@ private void CreateNetworkVariableTypeInitializers(AssemblyDefinition assembly)
259287
equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_UnmanagedValueEquals_MethodRef);
260288
}
261289

262-
if (serializeMethod != null)
263-
{
264-
serializeMethod.GenericArguments.Add(type);
265-
}
290+
serializeMethod?.GenericArguments.Add(type);
266291
equalityMethod.GenericArguments.Add(type);
267292
}
268293
else
@@ -296,10 +321,7 @@ private void CreateNetworkVariableTypeInitializers(AssemblyDefinition assembly)
296321
equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_ManagedClassEquals_MethodRef);
297322
}
298323

299-
if (serializeMethod != null)
300-
{
301-
serializeMethod.GenericArguments.Add(type);
302-
}
324+
serializeMethod?.GenericArguments.Add(type);
303325
equalityMethod.GenericArguments.Add(type);
304326
}
305327

@@ -1146,13 +1168,22 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass
11461168
//var type = field.FieldType;
11471169
if (type.IsGenericInstance)
11481170
{
1149-
if (type.Resolve().Name == typeof(NetworkVariable<>).Name || type.Resolve().Name == typeof(NetworkList<>).Name)
1171+
foreach (var attribute in type.Resolve().CustomAttributes)
11501172
{
1151-
var genericInstanceType = (GenericInstanceType)type;
1152-
var wrappedType = genericInstanceType.GenericArguments[0];
1153-
if (!m_WrappedNetworkVariableTypes.Contains(wrappedType))
1173+
if (attribute.AttributeType.Name == nameof(GenerateSerializationForGenericParameterAttribute))
11541174
{
1155-
m_WrappedNetworkVariableTypes.Add(wrappedType);
1175+
var idx = (int)attribute.ConstructorArguments[0].Value;
1176+
var genericInstanceType = (GenericInstanceType)type;
1177+
if (idx < 0 || idx >= genericInstanceType.GenericArguments.Count)
1178+
{
1179+
m_Diagnostics.AddError($"{type} has a {nameof(GenerateSerializationForGenericParameterAttribute)} referencing a parameter index outside the valid range (0-{genericInstanceType.GenericArguments.Count - 1}");
1180+
continue;
1181+
}
1182+
var wrappedType = genericInstanceType.GenericArguments[idx];
1183+
if (!m_WrappedNetworkVariableTypes.Contains(wrappedType))
1184+
{
1185+
m_WrappedNetworkVariableTypes.Add(wrappedType);
1186+
}
11561187
}
11571188
}
11581189
}
@@ -1173,13 +1204,22 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass
11731204
GetAllBaseTypesAndResolveGenerics(type.Resolve(), ref baseTypes, genericParams);
11741205
foreach (var baseType in baseTypes)
11751206
{
1176-
if (baseType.Resolve().Name == typeof(NetworkVariable<>).Name || baseType.Resolve().Name == typeof(NetworkList<>).Name)
1207+
foreach (var attribute in baseType.Resolve().CustomAttributes)
11771208
{
1178-
var genericInstanceType = (GenericInstanceType)baseType;
1179-
var wrappedType = genericInstanceType.GenericArguments[0];
1180-
if (!m_WrappedNetworkVariableTypes.Contains(wrappedType))
1209+
if (attribute.AttributeType.Name == nameof(GenerateSerializationForGenericParameterAttribute))
11811210
{
1182-
m_WrappedNetworkVariableTypes.Add(wrappedType);
1211+
var idx = (int)attribute.ConstructorArguments[0].Value;
1212+
var genericInstanceType = (GenericInstanceType)baseType;
1213+
if (idx < 0 || idx >= genericInstanceType.GenericArguments.Count)
1214+
{
1215+
m_Diagnostics.AddError($"{baseType} has a {nameof(GenerateSerializationForGenericParameterAttribute)} referencing a parameter index outside the valid range (0-{genericInstanceType.GenericArguments.Count - 1}");
1216+
continue;
1217+
}
1218+
var wrappedType = genericInstanceType.GenericArguments[idx];
1219+
if (!m_WrappedNetworkVariableTypes.Contains(wrappedType))
1220+
{
1221+
m_WrappedNetworkVariableTypes.Add(wrappedType);
1222+
}
11831223
}
11841224
}
11851225
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System;
2+
3+
namespace Unity.Netcode
4+
{
5+
/// <summary>
6+
/// Marks a generic parameter in this class as a type that should be serialized through
7+
/// <see cref="NetworkVariableSerialization{T}"/>. This enables the use of the following methods to support
8+
/// serialization within a Network Variable type:
9+
/// <br/>
10+
/// <br/>
11+
/// <see cref="NetworkVariableSerialization{T}"/>.<see cref="NetworkVariableSerialization{T}.Read"/>
12+
/// <br/>
13+
/// <see cref="NetworkVariableSerialization{T}"/>.<see cref="NetworkVariableSerialization{T}.Write"/>
14+
/// <br/>
15+
/// <see cref="NetworkVariableSerialization{T}"/>.<see cref="NetworkVariableSerialization{T}.AreEqual"/>
16+
/// <br/>
17+
/// <see cref="NetworkVariableSerialization{T}"/>.<see cref="NetworkVariableSerialization{T}.Duplicate"/>
18+
/// <br/>
19+
/// <br/>
20+
/// The parameter is indicated by index (and is 0-indexed); for example:
21+
/// <br/>
22+
/// <code>
23+
/// [SerializesGenericParameter(1)]
24+
/// public class MyClass&lt;TTypeOne, TTypeTwo&gt;
25+
/// {
26+
/// }
27+
/// </code>
28+
/// <br/>
29+
/// This tells the code generation for <see cref="NetworkVariableSerialization{T}"/> to generate
30+
/// serialized code for <b>TTypeTwo</b> (generic parameter 1).
31+
/// <br/>
32+
/// <br/>
33+
/// Note that this is primarily intended to support subtypes of <see cref="NetworkVariableBase"/>,
34+
/// and as such, the type resolution is done by examining fields of <see cref="NetworkBehaviour"/>
35+
/// subclasses. If your type is not used in a <see cref="NetworkBehaviour"/>, the codegen will
36+
/// not find the types, even with this attribute.
37+
/// <br/>
38+
/// <br/>
39+
/// This attribute is properly inherited by subclasses. For example:
40+
/// <br/>
41+
/// <code>
42+
/// [SerializesGenericParameter(0)]
43+
/// public class MyClass&lt;T&gt;
44+
/// {
45+
/// }
46+
/// <br/>
47+
/// public class MySubclass1 : MyClass&lt;Foo&gt;
48+
/// {
49+
/// }
50+
/// <br/>
51+
/// public class MySubclass2&lt;T&gt; : MyClass&lt;T&gt;
52+
/// {
53+
/// }
54+
/// <br/>
55+
/// [SerializesGenericParameter(1)]
56+
/// public class MySubclass3&lt;TTypeOne, TTypeTwo&gt; : MyClass&lt;TTypeOne&gt;
57+
/// {
58+
/// }
59+
/// <br/>
60+
/// public class MyBehaviour : NetworkBehaviour
61+
/// {
62+
/// public MySubclass1 TheValue;
63+
/// public MySubclass2&lt;Bar&gt; TheValue;
64+
/// public MySubclass3&lt;Baz, Qux&gt; TheValue;
65+
/// }
66+
/// </code>
67+
/// <br/>
68+
/// The above code will trigger generation of serialization code for <b>Foo</b> (passed directly to the
69+
/// base class), <b>Bar</b> (passed indirectly to the base class), <b>Baz</b> (passed indirectly to the base class),
70+
/// and <b>Qux</b> (marked as serializable in the subclass).
71+
/// </summary>
72+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true)]
73+
public class GenerateSerializationForGenericParameterAttribute : Attribute
74+
{
75+
internal int ParameterIndex;
76+
77+
public GenerateSerializationForGenericParameterAttribute(int parameterIndex)
78+
{
79+
ParameterIndex = parameterIndex;
80+
}
81+
}
82+
}

com.unity.netcode.gameobjects/Runtime/Messaging/GenerateSerializationForGenericParameterAttribute.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
3+
namespace Unity.Netcode
4+
{
5+
/// <summary>
6+
/// Specifies a specific type that needs serialization to be generated by codegen.
7+
/// This is only needed in special circumstances where manual serialization is being done.
8+
/// If you are making a generic network variable-style class, use <see cref="GenerateSerializationForGenericParameterAttribute"/>.
9+
/// <br />
10+
/// <br />
11+
/// This attribute can be attached to any class or method anywhere in the codebase and
12+
/// will trigger codegen to generate serialization code for the provided type. It only needs
13+
/// to be included once type per codebase, but including it multiple times for the same type
14+
/// is safe.
15+
/// </summary>
16+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method, AllowMultiple = true)]
17+
public class GenerateSerializationForTypeAttribute : Attribute
18+
{
19+
internal Type Type;
20+
21+
public GenerateSerializationForTypeAttribute(Type type)
22+
{
23+
Type = type;
24+
}
25+
}
26+
}

com.unity.netcode.gameobjects/Runtime/Messaging/GenerateSerializationForTypeAttribute.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
using System;
22
using System.Collections.Generic;
33
using Unity.Collections;
4-
using UnityEngine;
54

65
namespace Unity.Netcode
76
{
87
/// <summary>
98
/// Event based NetworkVariable container for syncing Lists
109
/// </summary>
1110
/// <typeparam name="T">The type for the list</typeparam>
11+
[GenerateSerializationForGenericParameter(0)]
1212
public class NetworkList<T> : NetworkVariableBase where T : unmanaged, IEquatable<T>
1313
{
1414
private NativeList<T> m_List = new NativeList<T>(64, Allocator.Persistent);
@@ -68,14 +68,7 @@ public override bool IsDirty()
6868

6969
internal void MarkNetworkObjectDirty()
7070
{
71-
if (m_NetworkBehaviour == null)
72-
{
73-
Debug.LogWarning($"NetworkList is written to, but doesn't know its NetworkBehaviour yet. " +
74-
"Are you modifying a NetworkList before the NetworkObject is spawned?");
75-
return;
76-
}
77-
78-
m_NetworkBehaviour.NetworkManager.BehaviourUpdater.AddForUpdate(m_NetworkBehaviour.NetworkObject);
71+
MarkNetworkBehaviourDirty();
7972
}
8073

8174
/// <inheritdoc />

com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace Unity.Netcode
88
/// </summary>
99
/// <typeparam name="T">the unmanaged type for <see cref="NetworkVariable{T}"/> </typeparam>
1010
[Serializable]
11+
[GenerateSerializationForGenericParameter(0)]
1112
public class NetworkVariable<T> : NetworkVariableBase
1213
{
1314
/// <summary>
@@ -149,7 +150,7 @@ public override void ResetDirty()
149150
if (!m_HasPreviousValue || !NetworkVariableSerialization<T>.AreEqual(ref m_InternalValue, ref m_PreviousValue))
150151
{
151152
m_HasPreviousValue = true;
152-
NetworkVariableSerialization<T>.Serializer.Duplicate(m_InternalValue, ref m_PreviousValue);
153+
NetworkVariableSerialization<T>.Duplicate(m_InternalValue, ref m_PreviousValue);
153154
}
154155
}
155156

com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,22 @@ public virtual void SetDirty(bool isDirty)
8888

8989
if (m_IsDirty)
9090
{
91-
if (m_NetworkBehaviour == null)
92-
{
93-
Debug.LogWarning($"NetworkVariable is written to, but doesn't know its NetworkBehaviour yet. " +
94-
"Are you modifying a NetworkVariable before the NetworkObject is spawned?");
95-
return;
96-
}
97-
98-
m_NetworkBehaviour.NetworkManager.BehaviourUpdater.AddForUpdate(m_NetworkBehaviour.NetworkObject);
91+
MarkNetworkBehaviourDirty();
9992
}
10093
}
10194

95+
protected void MarkNetworkBehaviourDirty()
96+
{
97+
if (m_NetworkBehaviour == null)
98+
{
99+
Debug.LogWarning($"NetworkVariable is written to, but doesn't know its NetworkBehaviour yet. " +
100+
"Are you modifying a NetworkVariable before the NetworkObject is spawned?");
101+
return;
102+
}
103+
104+
m_NetworkBehaviour.NetworkManager.BehaviourUpdater.AddForUpdate(m_NetworkBehaviour.NetworkObject);
105+
}
106+
102107
/// <summary>
103108
/// Resets the dirty state and marks the variable as synced / clean
104109
/// </summary>

0 commit comments

Comments
 (0)