How to (de) serialize unity references at runtime?

When you save scene with some MonoBehaviour that has public Texture field with texture asset assigned, it serializes in scene file as:

testTextureField: {fileID: 2800000, guid: bcffaa4441161f641a48d989be58c7c2,
      type: 3}

I can imagine how this deserializing on loading scene in editor (asset path by guid). But scenes also can be loaded at runtime! I mean, there is all the “assets” somewhere after build, but AFAIK guid makes sense only in editor? Then how it knows which object is referenced only by this info? What is fileID in this case?

tl;dr: How to get some unique ID for asset (Object) which is constant at runtime (and editor) and between sessions, and then get asset (Object) by this ID?

inb4: “make hashtable database of all the Objects” - is this the only way? Scenes doing it somehow w/o.

You can’t create references to objects from the AssetDatabase on runtime. Then only exception are Resource assets.

The main reason is, that Unity builds exclude all non-referenced assets. So there must be a known web of references to difrerent assets at runtime. The FIleID and the way how unity assets serialize references is nothing you can affect or re-use.

You can, however, serialize ‘references’ to the Resources object, by serializing its path and type. And you can easily create custom serialized struct that holds these values inside and does the Resources.Load for you. You an even create editor drawers to make the inspector for this struct look like an object field.

Here’s my Resource struct

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    namespace Framework
    {
    
        [System.Serializable]
        [Serialization.Serializable]
        public struct Resource : Serialization.ISerializationCallbackReceiver, ISerializationCallbackReceiver
        {
    
            public static Resource empty
            {
                get { return new Resource(); }
            }
    
            [System.Serializable]
            [Serialization.Serializable]
            public struct Address
            {
                public Address(string _uri, System.Type _type)
                {
                    uri = _uri;
                    typeName = TypeToString.GetTypeName(_type);
                }
    
                public static bool operator ==(Address lhs, Address rhs)
                {
                    return Equals(lhs, rhs);
                }
    
                public static bool operator !=(Address lhs, Address rhs)
                {
                    return !Equals(lhs, rhs);
                }
    
                public static bool Equals(Address lhs, Address rhs)
                {
                    return lhs.uri == rhs.uri && lhs.typeName == rhs.typeName;
                }
    
                public override int GetHashCode()
                {
                    return uri.GetHashCode() ^ typeName.GetHashCode();
                }
    
                public override bool Equals(object obj)
                {
                    if (obj is Address)
                    {
                        var other = (Address)obj;
                        return Equals(this, other);
                    }
                    return false;
                }
    
                [Serialization.SaveAsValue, SerializeField] public string uri;
                [Serialization.SaveAsValue, SerializeField] public string typeName;
            }
    
            public static bool Equals(Resource lhs, Resource rhs)
            {
                return Address.Equals(rhs.address, lhs.address);
            }
    
            public static bool operator ==(Resource lhs, Resource rhs)
            {
                return lhs.address == rhs.address;
            }
    
            public static bool operator !=(Resource lhs, Resource rhs)
            {
                return lhs.address != rhs.address;
            }
    
            public override int GetHashCode()
            {
                return address.GetHashCode();
            }
    
            public override bool Equals(object obj)
            {
                if (obj is Resource)
                {
                    var other = (Resource)obj;
                    return Equals(this, other);
                }
                return false;
    
            }
    
            public Resource(Address address) : this()
            {
                m_address = address;
                m_dirty = true;
            }
    
            public void SetDirty()
            {
                m_dirty = true;
            }
    
            private UnityEngine.Object m_obj;
            [SerializeField][Serialization.SaveAsValue] private Address m_address;
            private bool m_dirty;
    
            public Address address
            { get { return m_address; } }
    
            public UnityEngine.Object obj
            {
                get
                {
                    if ( m_dirty )
                    {
                        m_dirty = false;
                        try
                        {
                            var type = TypeToString.ParseType(m_address.typeName);
                            // a hotfix for the new namespace
                            if(type == null)
                                type = TypeToString.ParseType("GrayZone."+m_address.typeName);
    
                            m_obj = Resources.Load(m_address.uri, type);
                        }
                        catch
                        {
                            m_obj = null;
                        }
                    }
    
                    return m_obj;
                }
            }
    
            public void OnBeforeSerialize()
            {
            }
    
            public void OnAfterDeserialize()
            {
                m_dirty = true;
            }
            
    
    #if UNITY_EDITOR
            public static Address Editor_GetObjectAddress(Object obj)
            {
                Address address = new Address();
                if (obj)
                {
                    var assetPath = UnityEditor.AssetDatabase.GetAssetPath(obj);
                    var split = assetPath.Split(System.IO.Path.PathSeparator, System.IO.Path.AltDirectorySeparatorChar);
                    var newPath = "";
                    var resFound = false;
                    for (int i = split.Length - 1; i >= 0; --i)
                    {
                        var part = split*.ToLower();*

if (part == “resources”)
{
resFound = true;
break;
}
newPath = System.IO.Path.Combine(part, newPath);
}
if (resFound)
{
newPath = System.IO.Path.ChangeExtension(newPath, null);
address.typeName = TypeToString.GetTypeName(obj.GetType());
address.uri = newPath;
}
else
{
address.typeName = null;
address.uri = “”;
}
}
else
{
address.uri = “”;
address.typeName = null;
}
return address;
}
#endif
}
}

And the drawer
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using System.Reflection;

namespace Framework.Editor
{
[CustomPropertyDrawer(typeof(Resource))]
[CustomPropertyDrawer(typeof(ResourceAttribute))]
public class ResourceDrawer : PropertyDrawer
{

public System.Type GetResourceType()
{
var attrib = attribute as ResourceAttribute;
var resType = attrib != null ? attrib.resourceType : typeof(Object);
return resType;
}

public override void OnGUI( Rect position, SerializedProperty property, GUIContent label )
{
//var objProperty = property.FindPropertyRelative(“m_obj”);
//EditorGUI.PropertyField(position, objProperty, label, true);
var names = property.propertyPath.Split(‘.’);
var targets = property.serializedObject.targetObjects;
var results = new List();
EditorReflectionUtility.GetVariable(names, targets, results);

Object obj = results[0].obj;
for ( int i = 1; i < results.Count; ++i )
{
var otherObj = results*.obj;*
if ( otherObj != obj )
{
EditorGUI.showMixedValue = true;
break;
}
}

EditorGUI.BeginChangeCheck();
GUI.backgroundColor = new Color(0.9f,0.9f,1f,1f);
var newObject = EditorGUI.ObjectField(position, label, obj, GetResourceType(), false);
GUI.backgroundColor = Color.white;
if ( EditorGUI.EndChangeCheck() )
{
Undo.RecordObjects(targets, “Setting Resource”);
var newRes = new Resource(Resource.Editor_GetObjectAddress(newObject));
newRes.SetDirty();
EditorReflectionUtility.SetVariable(names, targets, newRes);

foreach ( var targ in targets )
{
EditorUtility.SetDirty(targ);
var onValidate = targ.GetType().GetMethod(“OnValidate”, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
if ( onValidate != null)
onValidate.Invoke(targ, null);
}

}
EditorGUI.showMixedValue = false;
return;
}

}
}
Just as an example, not all code dependencies are included

Unity doesn’t provide any runtime function yet to translate an asset GUID to an object. The AssetGUID can be translated in editor code to an asset path with AssetDatabase.GUIDToAssetPath. The fileID usually references the YAML internal instance ID. However there are some special IDs (for those with external references). Some represent hardcoded internal Assets (usually with a fix GUID with all or most digits being 0). Others, like in your case, the fileID essentially represents the asset type (28 in your case which is a Texture2D)…

The Unity Engind does have the assetdatabase of the included asset available internally but doesn’t provide any direct access to it. So yes, you would need to create your own mapping. if you really want to work with the GUIDs manually. Note that when you build your game the scene file usually isn’t in YAML anymore. In a build the scenes are serialized in the binary asset format.

From your question it’s not clear what you actually want to achieve.

So, the main problem was that I can’t retrieve any “path” information from the Object: once it’s loaded, it has no information where it comes from (it was pretty logical, I guess). But it has some information about what it is! I mean hashcode. Currently my solution is ScriptableObject with Hashtable asset map, which I populate in Editor just before build with AssetDatabase.GetAllAssetPaths (not documented?!) as values and corresponding Object hashcodes as keys. Then just Resource.UnloadUnusedAssets. At Runtime, I serialize Object hashcodes, and deserialize by Resources.Load(assetMap[hashcode]). For scene objects (FindObjects(Object)) I use simple List(Object), then reinstantiate and rereference by its indexes.


UPD: Since hashes are not hashes, looks like the only reliable piece of info is Object name, which is not that useful. Solution is in the middle: it’s hybrid of (Resource) AssetMap which is Dictionary/Hashtable(Object, path/address/key) or list (too unstable between versions, so I dropped) and AssetBundles or Addressables package. On deserializarion I just can load direct asset. On serialization I need to load AssetMap and look for path by Object, then unload (still weird). P.S.: I still think it can be much easier, like some direct access through Object for real.

UPD: Solution is, you just create a separate AssetMapping for every Asset and store it in Resources folder, then traverse them one-by-one looking for needed object, unloading if incorrect ofc. It looks slow, sturdy and inconvenient, but it’s the only way to serialize everything. With caching it’s like acceptable. At least you don’t stuff your RAM with everything at once! hehe…