Store a reference to a UnityObject as a struct?

Hi, I’m working on an editor tool using the experimental GraphView, where I create a ObjectField inside a node to reference an asset in my project. Due to my goal being to be able to integrate DOTS as much as possible into my project, I would like to store the reference to the asset as a struct, rather than creating a variable storing the asset directly.

I’ve stumbled upon the official documentation for the UnityObjectRef struct, which seemed to work at first, as I could assign the reference inside a GraphView, then save the containing object, the latter correctly referencing the asset.

However, I’ve noticed the reference changed when restarting the editor, as it seems the UnityObjectRef stores the asset’s instanceID, which is renewed by the editor and unreliable for persistence. Is there a way to make this ID consistent, or to be able to trace the data back to the asset?

I know I can just store the file path, and then call AssetDatabase.LoadAssetAtPath() to get the asset this way, but I would like to know if there is a way to make the UnityObjectRef work in this context.

Here are the scripts for the node, and the data struct itself :

using System;
using Herve.Editor.Models;
using Herve.Runtime.Models;
using Unity.Entities;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;

namespace Herve.Editor.Views
{
    /// <summary>
    /// Node acting as the start of a dialogue
    /// </summary>
    internal sealed class EndNode : BaseNode<EndNodeData>
    {
        #region Private fields


        #endregion

        #region Constructor

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="position">The position of the node on the graph</param>
        public EndNode(Vector2 position) : base(EditorConstants.END_NODE_LABEL, position)
        {
            this.Data = new EndNodeData(Guid.NewGuid().ToString(), position, EndNodeType.End, default);
            USSBuilder.AddUSSClass(this, "endNode");
            this.CreatePorts();
            this.CreateFields();
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="data">The data to assign to the node</param>
        public EndNode(EndNodeData data) : base(EditorConstants.END_NODE_LABEL, data.Position)
        {
            this.Data = data;
            USSBuilder.AddUSSClass(this, "endNode");
            this.CreatePorts();
            this.CreateFields();
        }

        #endregion

        #region Protected methods

        /// <inheritdoc/>
        protected override void CreatePorts()
        {
            GraphBuilder.AddPort(this, "Input", Direction.Input, Port.Capacity.Multi);
        }

        /// <inheritdoc/>
        protected override void CreateFields()
        {
            Box box = UIBuilder.NewBox(this.mainContainer);
            UIBuilder.NewEnumField(box, this.Data.EndNodeType, this.OnEndNodeTypeValueChangedCallback);
            UIBuilder.NewObjectField(box, this.Data.NextDialogue.Value, this.OnNextDialogueValueChangedCallback);
        }

        #endregion

        #region Private methods

        /// <summary>
        /// Called when the value of the enum is changed
        /// </summary>
        /// <param name="type">The type of action to execute once this node is reached</param>
        private void OnEndNodeTypeValueChangedCallback(EndNodeType type)
        {
            this.Data = new EndNodeData(this.Data.NodeGUID, this.Data.Position, type, this.Data.NextDialogue);
        }

        /// <summary>
        /// Called when the value of the object field is changed
        /// </summary>
        /// <param name="dialogue">The new dialogue to register</param>
        private void OnNextDialogueValueChangedCallback(DialogueAssetSO dialogue)
        {
            UnityObjectRef<DialogueAssetSO> reference = new();
            reference.Value = dialogue;
            this.Data = new EndNodeData(this.Data.NodeGUID, this.Data.Position, this.Data.EndNodeType, reference.Value);
        }

        #endregion
    }
}


    /// <summary>
    /// The data of a StartNode instance
    /// </summary>
    [Serializable]
    public struct EndNodeData : INodeData
    {
        #region Properties

        /// <inheritdoc/>
        [field: SerializeField]
        [field: ReadOnly]
        public FixedString64Bytes NodeGUID { get; private set; }

        /// <inheritdoc/>
        [field: SerializeField]
        public float2 Position { get; private set; }

        /// <summary>
        /// The type of action to execute once this node is reached
        /// </summary>
        [field: SerializeField]
        public EndNodeType EndNodeType { get; private set; }

        /// <summary>
        /// The next dialogue (if any) to play once the current one is over
        /// </summary>
        [field: SerializeField]
        public UnityObjectRef<DialogueAssetSO> NextDialogue { get; private set; }

        #endregion
}

This stores objects by InstanceID and as you noticed, it is a transient identifier.

You can however grab the object’s GUID and create a Guid instance. The Guid is a struct (16 bytes).

You would then have to load the actual managed object from its Guid. However this will only work in the editor. If you need that reference at runtime you’d have to make your own “runtime registry” containing those object references to be able to look them up by their Guid at runtime.

Yeah, UnityObjectRef is intended for runtime references and not for persisting references.

On the other hand, GlobalObjectId is intended for serializing references at authoring time and might be what you’re looking for (it’s not available at runtime). You can use it as a struct or also in its string representation.

Thank you both for your replies, they gave me the information I was looking for. Unfortunately, the solution I am looking for needs to be available in both runtime and editor, and the only flexible method I could find was to store the asset path and load the asset with Resources.Load at runtime. That, or referencing the asset directly, which prevents me from using NativeCollections.

No!

That’s a suboptimal solution because any asset under Resources goes in the build, whether used or not. Storing the path is also consuming far more bytes than a simple index. And then you need to use Resources.Load and of course, unload, to manage memory.

You really only need to have a registry of assets that you maintain in the editor. This should be an SO with a List<>. At runtime, you can index assets by, well, index. Or key, or Guid, longitude and latitude, any way you see fit really.

How you build and keep that list updated is up to you. You could for instance scan all assets by type before making a build. You could also manually maintain the list via drag and drop. You can automate adding and removing assets based on usage in your tool.

Hi, thank you. You’re probably right. I wanted to go for the id route to avoid having to build a registry myself, but I guess this would be the optimal choice.

I’ll try to implement an indexing solution to store all assets referenced by my nodes, and I’ll get back to this thread and post my results once I’m done.

Okay. So it was incredibly easy, actually.

In my Dialogue asset ScriptableObject, where I store the data for all my nodes, I now also store a class called NodeAssetReferences containing a serialized dictionary. This dictionary links all required assets by their referencing node using the latter’s GUID.

Storing it directly in the SO makes the dictionary persist across editor and runtime, and also between editor sessions. Assigning and retrieving the asset from this class is as easy as using a regular Dictionary, and I don’t need an extra field in my node’s data struct to make it work; I can just retrieve the GUID it already had.


using System;
using AYellowpaper.SerializedCollections;
using Unity.Collections;
using UnityEngine;

namespace Herve.Runtime.Models
{
    /// <summary>
    /// Registers any asset referenced by a node into a ScriptableObject.
    /// This allows to persist those references across sessions, in both editor and at runtime.
    /// </summary>
    [Serializable]
    public sealed class NodeAssetReferences
    {
        #region Public fields

        /// <summary>
        /// Registers any references to a DialogueAssetSO in any EndNode
        /// </summary>
        [field: SerializeField]
        [field: ReadOnly]
        [field: SerializedDictionary("NodeGUID", "Dialogue")]
        public SerializedDictionary<FixedString64Bytes, DialogueAssetSO> EndNodeDialogueRefs { get; private set; } = new();

        #endregion

        #region Public methods

        /// <summary>
        /// Clears all asset references
        /// </summary>
        public void Clear()
        {
            this.EndNodeDialogueRefs.Clear();
        }

        #endregion
    }
}

Since dictionaries aren’t serializable by default, I had to use ‘Serialized Dicitonary’ from the Asset Store to serialize it and display its content in the Inspector. It’s an incredible asset, free and easy to use, and it got the job done effortlessly.

So it was very easy to do, but since I wasn’t familiar with that kind of setup it took me quite a while to get the classes right. I even thought I had to make a custom Project Settings window, with EditorPrefs and all, to keep the references persistent. But in the end, the solution I ended up with here is much more simple and elegant.

I’m still unable to benefit from Burst as-is though, but I think since the dialogue asset is already a SO, it might not change much anyway.