I’m building an editor tool that involves a graph with nodes, and a custom Editor
. I’m utilizing the [SerializeReference]
attribute for the nodes, which has worked really well thus far, until I wanted to duplicate a node. That is, create a new node with all the same values as the previous node. Here are some things I’ve tried:
-
Duplicating the element in the list of nodes. This simply creates another reference to the same node object.
-
Using BinaryFormatter
to create a deep copy of the node, and assigning that to the new node. This doesn’t seem to work well with some built-in Unity classes like Vector2
, and threw some errors.
-
Creating a shallow copy with MemberwiseClone()
. This was the closest I’ve gotten, however reference types, like List
s, were not cloned, and would reference the same List
. So if I changed the list on one node, it would change on the other as well.
One thing to note is that I cannot manually go through each value in the node and assign them on the new node, because there are many derived versions of the nodes, and I do not want to implement a sort of .Clone()
function on each child class.
Any thoughts?
First way:
Use reflection to iterate over all public or private variables that have attribute [SerializeField]
(Remember to ignore the public variable with attribute [NonSerialized]
)
Because when using [SerializeReference]
you can get exactly the derived type of node, instead of the base type.
But be careful, because reflection also causes some performance problems if used in build.
Second way:
Use a ScriptableObject
as a clone machine
//NodeCloner is custom your class
NodeCloner nodeCloner = ScriptableObject.CreateInstance<NodeCloner>();//You can cache it
nodeCloner.nodeData = yourNodeData;
NodeCloner newNodeCloner = ScriptableObject.Instantiate(nodeCloner);
var newNodeData = newNodeCloner.nodeData;
However, as for performance in the build vs. reflection, I haven’t tested it yet.
A couple of years later and still no direct solution to this, so I made this script, invoke SerializeReferenceDuplicationAnchor.Validate
with your Unity object that contains serialized references.
Usage:
#if UNITY_EDITOR
void OnValidate()
{
SerializeReferenceDuplicationAnchor.Validate(this);
}
#endif
Script:
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using CObject = System.Object;
using UObject = UnityEngine.Object;
/// <summary>
/// A tool that will properly duplicate all serialized references inside a unity object
/// </summary>
static class SerializeReferenceDuplicationAnchor
{
static HashSet<CObject> References = new(200, ReferenceEqualityComparer.Default);
class ReferenceEqualityComparer : IEqualityComparer<CObject>
{
//Ensure we always do a reference check
public new bool Equals(CObject x, CObject y) => ReferenceEquals(x, y);
//Default hashcode implementation of the type is good enough
//I wanted to use the CLRs' internal hashcode mechanism, but I couldn't find a public API for it
public int GetHashCode(CObject obj) => obj.GetHashCode();
public static ReferenceEqualityComparer Default { get; } = new ReferenceEqualityComparer();
}
public static void Validate(UObject target)
{
References.Clear();
var managedObject = new SerializedObject(target);
var iterator = managedObject.GetIterator();
while (iterator.NextVisible(true))
{
if (iterator.propertyType is not SerializedPropertyType.ManagedReference)
continue;
if (iterator.managedReferenceValue == null)
continue;
if (References.Add(iterator.managedReferenceValue))
continue;
iterator.managedReferenceValue = DuplicateReference(iterator.managedReferenceValue);
}
managedObject.ApplyModifiedProperties();
}
static CObject DuplicateReference(CObject original)
{
//Yeah, not the most optimal solution, but not many options that Unity allows us
var type = original.GetType();
//Json serialization uses the same serialization engine that the inspector uses
//Ie, we will get all the values we are expecting
var json = JsonUtility.ToJson(original);
var clone = JsonUtility.FromJson(json, type);
return clone;
}
}
#endif