Search code examples
c#unity-game-engineunity-editorunity3d-editor

Trying to create nested ScriptableObject: "AddAssetToSameFile failed because the other asset is not persistent"


Goal: Create nested scriptable objects from the project view.

Expected: When an instance of the container scriptable object is created from the project view, an instance of the child scriptable object is created and attached to the container asset. The container should also keep a reference of the child.

Actual: When I try to attach the child to the container asset, it fails. I use the AssetDatabase.AddObjectToAsset but gives me the following error messages:

  • UnityException: Adding asset to object failed.
  • AddAssetToSameFile failed because the other asset is not persistent

Observations: The container is created successfully. No child asset is created. The inspector shows a child reference as soon as the asset is created, but says Type mismatch when the name of the container is entered.

The child object is not persistent. I do not know what persistent means in this context. I think this might be the reason I don't understand this problem.

Following is the code of a simplified version of what I am trying to implement. The same error is reproduced.

Container class

[CreateAssetMenu]
public class Container : ScriptableObject
{
    [SerializeField] private Child child;
        
    private void Reset()
    {
        // Create new child
        child = ScriptableObject.CreateInstance<Child>();

        // Attach child to the container
        AssetDatabase.AddObjectToAsset(child, this); // This line throws exception!

        // Save changes
        AssetDatabase.SaveAssets();
    }
}

Child class

public class Child : ScriptableObject
{
    [SerializeField] public string myString;
}

Solution

  • The issue is that until you entered the name the new created scriptableObject is not persistent yet. If you hit Escape then it is never created ;)

    What you can do is delay the child creation until the asset was actually created. In order to check this you can use AssetDatabase.Contains

    Note though: I would suggest to not only rely on Reset but additionally use OnValidate and Awake in order to also force the child to be set when someone changes it via the Inspector. In that case I would simply check if a child already exists within this asset in order to not recreate it.

    Also note: UnityEditor is completely stripped of in a build!

    => If this is meant to be used for runtime applications outside of the Unity Editor itself, make sure to wrap anything related to UnityEditor in pre-processor tags

    #if UNITY_EDITOR
    any code related to UnityEditor namespace
    #endif
    

    so I would do something like e.g.

    using System;
    using System.Linq;
    using UnityEngine;
    
    #if UNITY_EDITOR
    using UnityEditor;
    #endif
    
    [CreateAssetMenu]
    public class Container : ScriptableObject
    {
        [SerializeField]
        private Child child;
    
        #if UNITY_EDITOR
        private void Awake()
        {
            Init();
        }
    
        private void OnValidate()
        {
            Init();
        }
    
        private void Reset()
        {
            Init();
        }
    
        private void OnDestroy()
        {
            EditorApplication.update -= DelayedInit;
        }
    
        private void Init()
        {
            // If child is already set -> nothing to do
            if (child)
            {
                return;
            }
    
            // If this asset already exists initialize immediately
            if (AssetDatabase.Contains(this))
            {
                DelayedInit();
            }
            // otherwise attach a callback to the editor update to re-check repeatedly until it exists
            // this means it is currently being created an the name has not been confirmed yet
            else
            {
                EditorApplication.update -= DelayedInit;
                EditorApplication.update += DelayedInit;
            }
        }
    
        private void DelayedInit()
        {
            // if this asset dos still not exist do nothing
            // this means it is currently being created and the name not confirmed yet
            if (!AssetDatabase.Contains(this))
            {
                return;
            }
    
            // as soon as the asset exists remove the callback as we don't need it anymore
            EditorApplication.update -= DelayedInit;
    
            // first try to find existing child within all assets contained in this asset
            var assets = AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(this));
            // you could as well use a loop but this Linq query is a shortcut for finding the first sub asset
            // of type "Child" or "null" if there was none
            child = assets.FirstOrDefault(a => a.GetType() == typeof(Child)) as Child;
    
            // did we find a child ?
            if (!child)
            {
                // If not create a new child
                child = CreateInstance<Child>();
                // just for convenience I'd always give assets a meaningful name
                child.name = name + "_Child";
                
                // Attach child to the container
                AssetDatabase.AddObjectToAsset(child, this);
            }
            
            // Mark this asset as dirty so it is correctly saved in case we just changed the "child" field
            // without using the "AddObjectToAsset" (which afaik does this automatically)
            EditorUtility.SetDirty(this);
    
            // Save all changes
            AssetDatabase.SaveAssets();
        }
        #endif
    }