简体   繁体   中英

How to ensure ScriptableObjects are unique when copied in the Unity 3D editor?

In Unity 3D, I have a MonoBehaviour which contains a list of class derivates which all are based on a common ScriptableObject . The list is filled and handled in a custom editor. This works flawlessly, but with one exception: Whenever I copy/paste my MonoBehaviour , or duplicate a game object holding it in the Unity editor, my list contains only instances and not unique clones.

Here's some example code (note that this is only stripped down test code, my actual classes are much more complex, which requires a separate data class):

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public abstract class MyAbstractBaseClass : ScriptableObject
{
    public abstract void foo();
}

public class MyTestScriptableObject : MyAbstractBaseClass
{
    public string stringMember;
    public override void foo()
    {
    }
}

public class MyTestMonoBehaviour : MonoBehaviour
{
    public List<MyAbstractBaseClass> testList;
}

[CustomEditor(typeof(MyTestMonoBehaviour))]
public class MyTestMonoBehaviourEditor : Editor
{
    const int NUM_LISTENTRIES = 5;

    public override void OnInspectorGUI()
    {
        SerializedProperty testListProp = serializedObject.FindProperty("testList");

        for (int i = 0; i < testListProp.arraySize; i++)
        {
            SerializedObject myTestScriptableObjectSO = new SerializedObject(testListProp.GetArrayElementAtIndex(i).objectReferenceValue);
            SerializedProperty stringMemberProp = myTestScriptableObjectSO.FindProperty("stringMember");
            EditorGUILayout.PropertyField(stringMemberProp);
            myTestScriptableObjectSO.ApplyModifiedProperties();
        }

        if( GUILayout.Button("Generate List"))
        {
            testListProp.arraySize = NUM_LISTENTRIES;
            for( int i=0; i<NUM_LISTENTRIES; i++)
                testListProp.GetArrayElementAtIndex(i).objectReferenceValue = ScriptableObject.CreateInstance<MyTestScriptableObject>();
        }
        serializedObject.ApplyModifiedProperties();
    }
}

Like I said, this code works flawlessly with the exception of the reference/clone issue. This means, when I change string 1 on GameObject A, string 1 on the copied GameObject B is also changed. I could of course easily clone the ScriptableObject references with ScriptableObject.Instantiate() - my problem is, that I don't know WHEN to clone.

My questions are:

  • Is there any callback/virtual method or similar which tells me, when my MonoBehaviour is duplicated in the editor (via C&P or duplicated GameObjects?)
  • Alternatively, is there any kind of reference counting in C# or Unity-wise, which can tell me, how often an object is referenced? Since it's only editor code, a method using reflection would be also ok.
  • Is ScriptableObject the best choice for the base of a data class which has to be unique at all, or are there alternatives?

frankhermes already suggested in the comments to use simple serialized data classes. This works fine for a single class, but unfortunately not for class hierarchies with a base class, since afaik they are not supported in the Unity serialization.

Edit:

--- Caution ---

This answer only works partially. The unique instance id CHANGES between sessions, which makes it useless for clone detection and serializing :( https://answers.unity.com/questions/863084/does-getinstanceid-ever-change-on-an-object.html

.

Found the solution at in the Unity forum : Storing and checking the instance id of the GameObject the MonoBehaviour is attached to works very good (see MakeListElementsUnique() ).

The only situation where this will fail is if you copy a MonoBehaviour and paste it to the same GameObject . But this isn't a real problem for my use case and for many others I suppose.

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public abstract class MyAbstractBaseClass : ScriptableObject
{
    public abstract void foo();
}

public class MyTestScriptableObject : MyAbstractBaseClass
{
    public string stringMember;
    public override void foo()
    {
    }
}

public class MyTestMonoBehaviour : MonoBehaviour
{
    public int instanceID = 0;
    public List<MyAbstractBaseClass> testList;
}

[CustomEditor(typeof(MyTestMonoBehaviour))]
public class MyTestMonoBehaviourEditor : Editor
{
    const int NUM_LISTENTRIES = 5;

    public override void OnInspectorGUI()
    {
        MyTestMonoBehaviour myScriptableObject = (MyTestMonoBehaviour)target;

        SerializedProperty testListProp = serializedObject.FindProperty("testList");

        MakeListElementsUnique(myScriptableObject, testListProp);

        for (int i = 0; i < testListProp.arraySize; i++)
        {

            SerializedObject myTestScriptableObjectSO = new SerializedObject(testListProp.GetArrayElementAtIndex(i).objectReferenceValue);
            SerializedProperty stringMemberProp = myTestScriptableObjectSO.FindProperty("stringMember");
            EditorGUILayout.PropertyField(stringMemberProp);
            myTestScriptableObjectSO.ApplyModifiedProperties();
        }

        if (GUILayout.Button("Generate List"))
        {
            testListProp.arraySize = NUM_LISTENTRIES;
            for (int i = 0; i < NUM_LISTENTRIES; i++)
                testListProp.GetArrayElementAtIndex(i).objectReferenceValue = ScriptableObject.CreateInstance<MyTestScriptableObject>();
        }
        serializedObject.ApplyModifiedProperties();
    }

    private void MakeListElementsUnique( MyTestMonoBehaviour scriptableObject, SerializedProperty testListProp )
    {
        SerializedProperty instanceIdProp = serializedObject.FindProperty("instanceID");
        // stored instance id == 0: freshly created, just set the instance id of the game object
        if (instanceIdProp.intValue == 0)
        {
            instanceIdProp.intValue = scriptableObject.gameObject.GetInstanceID();
        }
        // stored instance id != current instance id: copied!
        else if (instanceIdProp.intValue != scriptableObject.gameObject.GetInstanceID())
        {
            // don't forget to change the instance id to the new game object
            instanceIdProp.intValue = scriptableObject.gameObject.GetInstanceID();
            // make clones of all list elements
            for (int i = 0; i < testListProp.arraySize; i++)
            {
                SerializedProperty sp = testListProp.GetArrayElementAtIndex(i);
                sp.objectReferenceValue = Object.Instantiate(sp.objectReferenceValue);
            }
        }
    }

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM