Yeah, this is something I have found peculiar as well.
For starters System.Guid is not serializable by Unity. Not only that, Unity has their own internal GUID type which is effectively the same (there might be some underlying differences in how a new guid is created? no idea, not like I have internal access). And it’s part of UnityEditor.dll and not UnityEngine meaning it’s editor time only.
But that’s not Addressables specific, so I didn’t really mention it here.
But I wanted my own guid that was interoperable with System.Guid so I just wrote my own:
using UnityEngine;
namespace com.spacepuppy
{
[System.Serializable]
public struct SerializableGuid
{
#region Fields
[SerializeField]
private int a;
[SerializeField]
private short b;
[SerializeField]
private short c;
[SerializeField]
private byte d;
[SerializeField]
private byte e;
[SerializeField]
private byte f;
[SerializeField]
private byte g;
[SerializeField]
private byte h;
[SerializeField]
private byte i;
[SerializeField]
private byte j;
[SerializeField]
private byte k;
#endregion
#region CONSTRUCTOR
public SerializableGuid(System.Guid guid)
{
var arr = guid.ToByteArray(); //unfortunately unoptimized for gc, but this is how System.Guid grants access to the bytes of the guid
a = System.BitConverter.ToInt32(arr, 0);
b = System.BitConverter.ToInt16(arr, 4);
c = System.BitConverter.ToInt16(arr, 6);
d = arr[8];
e = arr[9];
f = arr[10];
g = arr[11];
h = arr[12];
i = arr[13];
j = arr[14];
k = arr[15];
}
public SerializableGuid(int a, short b, short c, byte d, byte e, byte f, byte g, byte h, byte i, byte j, byte k)
{
this.a = a;
this.b = b;
this.c = c;
this.d = d;
this.e = e;
this.f = f;
this.g = g;
this.h = h;
this.i = i;
this.j = j;
this.k = k;
}
#endregion
#region Methods
public override string ToString()
{
return this.ToGuid().ToString();
}
public string ToString(string format)
{
return this.ToGuid().ToString(format);
}
public System.Guid ToGuid()
{
return new System.Guid(a, b, c, d, e, f, g, h, i, j, k);
}
public override bool Equals(object obj)
{
if (obj is SerializableGuid sg)
{
return this.ToGuid() == sg.ToGuid();
}
else if (obj is System.Guid g)
{
return this.ToGuid() == g;
}
else
{
return false;
}
}
public override int GetHashCode()
{
return this.ToGuid().GetHashCode();
}
#endregion
#region Static Utils
public static SerializableGuid NewGuid()
{
return new SerializableGuid(System.Guid.NewGuid());
}
public static SerializableGuid Parse(string input)
{
return new SerializableGuid(System.Guid.Parse(input));
}
public static bool TryParse(string input, out SerializableGuid result)
{
System.Guid output;
if (System.Guid.TryParse(input, out output))
{
result = new SerializableGuid(output);
return true;
}
else
{
result = default(SerializableGuid);
return false;
}
}
public static bool Coerce(object input, out SerializableGuid result)
{
System.Guid guid;
if (CoerceGuid(input, out guid))
{
result = new SerializableGuid(guid);
return true;
}
else
{
result = default(SerializableGuid);
return false;
}
}
public static bool CoerceGuid(object input, out System.Guid result)
{
switch (input)
{
case System.Guid guid:
result = guid;
return true;
case SerializableGuid sguid:
result = sguid.ToGuid();
return true;
case string s:
return System.Guid.TryParse(s, out result);
case object o:
return System.Guid.TryParse(o.ToString(), out result);
default:
result = default(SerializableGuid);
return false;
}
}
public static implicit operator System.Guid(SerializableGuid guid)
{
return guid.ToGuid();
}
public static bool operator ==(SerializableGuid a, SerializableGuid b)
{
return a.ToGuid() == b.ToGuid();
}
public static bool operator ==(System.Guid a, SerializableGuid b)
{
return a == b.ToGuid();
}
public static bool operator ==(SerializableGuid a, System.Guid b)
{
return a.ToGuid() == b;
}
public static bool operator !=(SerializableGuid a, SerializableGuid b)
{
return a.ToGuid() != b.ToGuid();
}
public static bool operator !=(System.Guid a, SerializableGuid b)
{
return a != b.ToGuid();
}
public static bool operator !=(SerializableGuid a, System.Guid b)
{
return a.ToGuid() != b;
}
#endregion
#region Special Types
public class ConfigAttribute : System.Attribute
{
public bool AllowZero;
/// <summary>
/// Attempts to make the guid match the guid associated with the asset this is on.
/// Note this only works if it's on an asset that exists on disk (ScriptableObject, Prefab).
/// Also it means guids will match across all scripts that are on a prefab with this flagged. (since the prefab only has one guid)
/// Really... this should be mainly done with ScriptableObjects only, Prefab's are just a
/// sort of bonus but with caveats.
///
/// New instances created via 'Instantiate' or 'CreateInstance' will not get anything.
/// This is editor time only for assets on disk!
/// </summary>
public bool LinkToAsset;
}
#endregion
}
}
Editor:
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;
using com.spacepuppy;
using com.spacepuppy.Utils;
using System.Diagnostics.Eventing.Reader;
using com.spacepuppy.Collections;
using System.Reflection;
using System.Linq.Expressions;
namespace com.spacepuppyeditor.Core
{
[CustomPropertyDrawer(typeof(SerializableGuid))]
public class SerializableGuidPropertyDrawer : PropertyDrawer
{
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUIUtility.singleLineHeight;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if(property.serializedObject.isEditingMultipleObjects)
{
EditorGUI.LabelField(position, label, EditorHelper.TempContent("Not Supported in Multi-Edit Mode"));
return;
}
EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, label);
EditorHelper.SuppressIndentLevel();
try
{
var attrib = this.fieldInfo.GetCustomAttribute<SerializableGuid.ConfigAttribute>();
bool resetOnZero = attrib == null || !attrib.AllowZero;
bool linkToAsset = attrib != null && attrib.LinkToAsset;
System.Guid guid = FromSerializedProperty(property);
if (linkToAsset)
{
string gid;
long lid;
System.Guid assetguid;
if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(property.serializedObject.targetObject, out gid, out lid) && System.Guid.TryParse(gid, out assetguid))
{
if(assetguid != guid)
{
guid = assetguid;
ToSerializedProperty(property, guid);
}
EditorGUI.SelectableLabel(position, guid.ToString("D"), EditorStyles.textField);
EditorGUI.EndProperty();
return;
}
}
//if we made it here we want to draw default
float w = Mathf.Min(position.width, 60f);
var r2 = new Rect(position.xMax - w, position.yMin, w, position.height);
var r1 = new Rect(position.xMin, position.yMin, Mathf.Max(position.width - w, 0f), position.height);
EditorGUI.SelectableLabel(r1, guid.ToString("D"), EditorStyles.textField);
if (GUI.Button(r2, "New Id") || (resetOnZero && guid == System.Guid.Empty))
{
guid = System.Guid.NewGuid();
ToSerializedProperty(property, guid);
}
EditorGUI.EndProperty();
}
finally
{
EditorHelper.ResumeIndentLevel();
}
}
public static System.Guid FromSerializedProperty(SerializedProperty prop)
{
return new System.Guid(prop.FindPropertyRelative("a").intValue, (short)prop.FindPropertyRelative("b").intValue, (short)prop.FindPropertyRelative("c").intValue,
(byte)prop.FindPropertyRelative("d").intValue, (byte)prop.FindPropertyRelative("e").intValue, (byte)prop.FindPropertyRelative("f").intValue,
(byte)prop.FindPropertyRelative("g").intValue, (byte)prop.FindPropertyRelative("h").intValue, (byte)prop.FindPropertyRelative("i").intValue,
(byte)prop.FindPropertyRelative("j").intValue, (byte)prop.FindPropertyRelative("k").intValue);
}
public static void ToSerializedProperty(SerializedProperty prop, System.Guid guid)
{
var arr = guid.ToByteArray();
prop.FindPropertyRelative("a").intValue = System.BitConverter.ToInt32(arr, 0);
prop.FindPropertyRelative("b").intValue = System.BitConverter.ToInt16(arr, 4);
prop.FindPropertyRelative("c").intValue = System.BitConverter.ToInt16(arr, 6);
prop.FindPropertyRelative("d").intValue = arr[8];
prop.FindPropertyRelative("e").intValue = arr[9];
prop.FindPropertyRelative("f").intValue = arr[10];
prop.FindPropertyRelative("g").intValue = arr[11];
prop.FindPropertyRelative("h").intValue = arr[12];
prop.FindPropertyRelative("i").intValue = arr[13];
prop.FindPropertyRelative("j").intValue = arr[14];
prop.FindPropertyRelative("k").intValue = arr[15];
}
private class SerializableGuidPostProcessor : AssetPostprocessor
{
private static readonly HashSet<System.Reflection.FieldInfo> _knownSOFields = new HashSet<System.Reflection.FieldInfo>();
private static readonly HashSet<System.Reflection.FieldInfo> _knownGOFields = new HashSet<System.Reflection.FieldInfo>();
static SerializableGuidPostProcessor()
{
var guidtp = typeof(SerializableGuid);
var sotp = typeof(ScriptableObject);
var ctp = typeof(MonoBehaviour);
foreach (var tp in TypeUtil.GetTypes(t => TypeUtil.IsType(t, sotp) || TypeUtil.IsType(t, ctp)))
{
foreach (var field in tp.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance))
{
if (field.FieldType != guidtp) continue;
var attrib = field.GetCustomAttribute<SerializableGuid.ConfigAttribute>();
if(attrib != null && attrib.LinkToAsset)
{
if(TypeUtil.IsType(field.DeclaringType, sotp))
{
_knownSOFields.Add(field);
}
else if(TypeUtil.IsType(field.DeclaringType, ctp))
{
_knownGOFields.Add(field);
}
}
}
}
}
static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
if (_knownGOFields.Count == 0 && _knownSOFields.Count == 0) return;
var sotp = typeof(ScriptableObject);
var gotp = typeof(GameObject);
if (importedAssets != null && importedAssets.Length > 0)
{
foreach (var path in importedAssets)
{
var atp = AssetDatabase.GetMainAssetTypeAtPath(path);
if (_knownSOFields.Count > 0 && TypeUtil.IsType(atp, sotp))
{
HandleScriptableObject(path, atp);
}
else if (_knownGOFields.Count > 0 && TypeUtil.IsType(atp, gotp))
{
HandleGameObject(AssetDatabase.LoadAssetAtPath<GameObject>(path));
}
}
}
}
static void HandleGameObject(GameObject go)
{
if (go == null) return;
bool edited = false;
foreach(var field in _knownGOFields)
{
var c = go.GetComponent(field.DeclaringType);
if(c != null)
{
try
{
var guid = (SerializableGuid)field.GetValue(c);
string gid;
long lid;
System.Guid assetguid;
if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(c, out gid, out lid) && System.Guid.TryParse(gid, out assetguid))
{
if (assetguid != guid)
{
field.SetValue(c, new SerializableGuid(assetguid));
edited = true;
}
}
}
catch(System.Exception ex)
{
Debug.LogException(ex);
}
}
}
if(edited)
{
PrefabUtility.SavePrefabAsset(go);
}
}
static void HandleScriptableObject(string path, System.Type atp)
{
ScriptableObject so = null;
foreach (var field in _knownSOFields)
{
if (TypeUtil.IsType(atp, field.DeclaringType))
{
if (object.ReferenceEquals(so, null)) so = AssetDatabase.LoadAssetAtPath<ScriptableObject>(path);
try
{
var guid = (SerializableGuid)field.GetValue(so);
string gid;
long lid;
System.Guid assetguid;
if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(so, out gid, out lid) && System.Guid.TryParse(gid, out assetguid))
{
if (assetguid != guid)
{
field.SetValue(so, new SerializableGuid(assetguid));
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssetIfDirty(so);
}
}
}
catch (System.Exception ex)
{
Debug.LogException(ex);
}
}
}
}
}
}
}