Trouble with serializing generated string

Maybe I’m not understanding how Serialization works, but I’ve been trying to serialize a Guid.
I KNOW that built-in guids are not serializable, so I first tried [SerializeField] string m_guid = System.Guid.NewGuid().ToString();

I was under the assumption that once a serialized field was set to the initial value once, it was stored by Unity, and not filled again unless done so manually.
But it’s generating a new guid every time the project is closed and reopened, and whenever the scripts recompile.

So I tried using code for a serializable Guid I found when searching, but it seems to do the same thing whenever the project is reloaded/recompiled
(The code I tried, for reference, from this post)

using System;
using UnityEngine;
/// <summary>
/// Serializable wrapper for System.Guid.
/// Can be implicitly converted to/from System.Guid.
///
/// Author: Searous
/// </summary>
[Serializable]
public struct SerializableGuid : ISerializationCallbackReceiver {
    private Guid guid;
    [SerializeField] private string serializedGuid;
    public SerializableGuid(Guid guid) {
        this.guid = guid;
        serializedGuid = null;
    }
    public override bool Equals(object obj) {
        return obj is SerializableGuid guid &&
                this.guid.Equals(guid.guid);
    }
    public override int GetHashCode() {
        return -1324198676 + guid.GetHashCode();
    }
    public void OnAfterDeserialize() {
        try {
            guid = Guid.Parse(serializedGuid);
        } catch {
            guid = Guid.Empty;
            Debug.LogWarning($"Attempted to parse invalid GUID string '{serializedGuid}'. GUID will set to System.Guid.Empty");
        }
    }
    public void OnBeforeSerialize() {
        serializedGuid = guid.ToString();
    }
    public override string ToString() => guid.ToString();
    public static bool operator ==(SerializableGuid a, SerializableGuid b) => a.guid == b.guid;
    public static bool operator !=(SerializableGuid a, SerializableGuid b) => a.guid != b.guid;
    public static implicit operator SerializableGuid(Guid guid) => new SerializableGuid(guid);
    public static implicit operator Guid(SerializableGuid serializable) => serializable.guid;
    public static implicit operator SerializableGuid(string serializedGuid) => new SerializableGuid(Guid.Parse(serializedGuid));
    public static implicit operator string(SerializableGuid serializedGuid) => serializedGuid.ToString();
}

I tried looking around some more, but still couldn’t understand what seems to be causing me so much headache. I’d be fine just saving the Guid as a string, since I’m not worried about string comparation performance for this use case. (I also heard that maybe ISerializationCallbackReceiver doesn’t work for structs, so I tried making it a class instead, but still no bueno.)

Is there something I’m not understanding about serialization?

Also, it’s important that it be automatically generated when the object is added to the scene, which is why I didn’t just write a property drawer with a button to generate the ID. That would probably work, since the only time Guid.NewGuid would be call is from that button, but it’s REALLY not how I want to do this.

Edit:
OH, I forgot to mention, I did write a bit of code that seemed to actually work, but it felt very hack-y and have no idea if/what errors it would cause for me in the future…

    public string ID {
        get {
            if (string.IsNullOrEmpty(m_ID)) {
                m_ID = System.Guid.NewGuid().ToString();
#if UNITY_EDITOR
                EditorUtility.SetDirty(this);
#endif
            }
            return m_ID;
        }
    }

How are you instancing/using these? Because I feel like something like:

SerializableGuid guid = new (Guid.NewGuid());

is going to create a new guid every domain reload (or every time the asset is reloaded?).

Though to be honest in my own serialisable guid that used a string, I just used a nullable Guid? and would just check if the guid was null, and if so, parse the string once, and then return it. No need for the ISerializationCallbackReciever.

This is the case and the Guid persists for me when trying this. However, note that the field needs to be saved first. Unity doesn’t expect fields to be initialized with changing data and doesn’t mark the object/scene as dirty. This means that Guid can be regenerated for some time until it is finally saved, and only from then on the saved fixed Guid will be used.

Creating reliable Guids for Unity objects is really hard, since you’re not really in control of the data and don’t have proper events when things gets e.g. duplicated or merged. I’d recommend using GlobalObjectId instead. Its ID is based on Unity’s internal IDs (asset GUID and file ID), which Unity guarantees to be unique. But it also comes with its caveats and cannot be used in all situations, make sure you read the manual page.

1 Like

The main one being that it’s in the UnityEditor namespace…

One solution to this problem is to have a ScriptableObject registry of objects/assets that you want to uniquely identify at runtime. Registering and unregistering objects is something you need to take control over in a suitable way, for instance you can add an object when its Reset method runs but you also want to have a editor-only HashSet so that you‘re not duplicating any entries.

You can then even use simple indexes to access each unique object which means you‘d have a 2 or 4 byte index vs a 32+ byte guid string.

Alright, so was I on the right track with what I tried with “EditorUtility.SetDirty(this);”? Is that actually what I should use, or is there a preferred way to automatically save the field after initialization? I wasn’t particularly keen on using internally generated identifiers like “GlobalObjectID” because I’m not sure how or if they’re ever regenerated, and I need a solution that works at runtime. (Guids will still work for me there since I won’t NEED to serialize it in the field once generated; I would be saving it into a string in a JSON file)

Funnily enough, I was planning that exact kind of thing (but still storing GUIDS) in a ScriptableObject’s hashset to prevent duplicate guids before I found out the “Global” in GUID meant the chance for duplicates was so infinitesimally small (As long as I handled object duplication initialization properly…) But that comes with a lot of extra management, especially considering the data needs to be unique and managed across scenes… I’d really rather have each instance generate its own guid. Though it’s still in consideration if really necessary.

Ideally do not have these identifiers at all. Whenever I have a gaggle of ScriptableObjects of a particular type, I just use their names to differentiate instances.

The name is guaranteed to be unique as long as you place all of a given type in a single directory. Thanks Mister Filesystem, you’re the greatest!!!

And if you don’t want them all in the same directory, it’s pretty trivial to write a little editor tooling to throw errors if you ever create two with the same name anywhere in your project.

This has the added side benefit that if you contemplate some savegame thing that needs to save/restore these things, it can even load them too, if you throw them all under a Resources/ folder.

AND… final cherry on the top, it’s a lot easier to reason about a name like DaggerOfWesternesse than 8529bc3f6532a453189983157f0acc1d when you are debugging.

This won’t work for me unfortunately. The data these IDs will be used for is generated procedurally, so the instances could be spawned in any order, under different parents, and some might not be spawned at all, all of which messes with the autonaming system. And I absolutely do not want to lose connections in data if I ever rename an object.

2 Likes

It should honestly be as simple as this:

[System.Serializable]
public struct SerializableGuid : IEquatable<UUID>
{
    [SerializeField]
    private string _guidString;
 
    private Guid _guid;
 
    public Guid Guid
    {
        if (_guid == Guid.Zero)
        {
            _guid = Guid.Parse(_guidString);
        }
        return _guid;
    }
 
    public static SerializableGuid NewGuid()
    {
        return new SerializableGuid()
        {
            _guidString = Guid.NewGuid().ToString();
        };
    }
 
    public bool Equals(SerializableGuid other)
    {
        return string.CompareOrdinal(_guidString, other._guidString) == 0;
    }
}

You probably don’t even need to expose the Guid property either. It can just be the string with comparisons done internally.

1 Like

I’ll try it out tonight, but I’m sure I tried something like that at first. Wouldn’t that have the same issue as what’s plaguing me with “[SerializeField] string m_guid = System.Guid.NewGuid().ToString();”, considering it looks like I’d be using yours like "[SerializeField] SerializableGuid m_guid = SerializableGuid.NewGuid();

Which only seems to assign it using the same “System.Guid.NewGuid().ToString()” method?

I mean that would be required by this absolute minimum implementation.

And there’s nothing wrong with:

[SerializeField]
private SerializableGuid _guid = SerializableGuid.NewGuid();

It’s simple, straightforward, and works. And importantly, encapsulated and reusable.

Though I would have an interface that allows a variety of objects to express that they have a unique ID:

public interface IHasGuid
{
    SerialziableGuid Guid { get; }
}

This is where the encapsulation starts to make more sense. Then you can start building up your supporting architecture around this simple base.

Again, I’ll try it when I get home tonight, but if that works, I’m curious why the line “[SerializeField] string m_guid = System.Guid.NewGuid().ToString();” DOESN’T work? Because they seemingly do a very similar thing, aside from one storing a string directly, and the other storing a string in a struct.

I mean it should work.

This code:

public class SomeMonoBehaviour : MonoBehaviour
{
    [SerializeField]
    private string _guid = System.Guid.NewGuid().ToString();
}

Should generate a guid when the component is added to a game object.

It of course won’t work for components already added to game objects, however. That’s important to note.

Here is what I use for a SerializableGuid:

I have a very complex propertydrawer that goes with it that does a lot of bonus features (like linking the guid to the asset it’s attached to and other stuff):

But for a simplified propertydrawer it just looks like:

[CustomPropertyDrawer(typeof(SerializableGuid))]
    public class SerializableGuidPropertyDrawer_Simple : PropertyDrawer
    {

        public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
        {
            float h;
            if (EditorHelper.AssertMultiObjectEditingNotSupportedHeight(property, label, out h)) return h;

            return EditorGUIUtility.singleLineHeight;
        }

        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            if (EditorHelper.AssertMultiObjectEditingNotSupported(position, property, label)) return;

            EditorGUI.BeginProperty(position, label, property);

            position = EditorGUI.PrefixLabel(position, label);

            {
                System.Guid guid = FromSerializedProperty(property);
                //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);

                DrawGuidField(r1, guid);

                if (GUI.Button(r2, "New Id"))
                {
                    guid = System.Guid.NewGuid();
                    ToSerializedProperty(property, guid);
                }
                EditorGUI.EndProperty();
            }
        }

        private System.Guid DrawGuidField(Rect position, System.Guid guid)
        {
            EditorGUI.SelectableLabel(position, guid.ToString("D"), EditorStyles.textField);
            return guid;
        }

        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];
        }

    }

The convenience of this is that it’s just a struct that is directly mapped to the same field layout as System.Guid. Meaning it’s castable using some pointer magic:

        public unsafe static implicit operator SerializableGuid(System.Guid guid)
        {
            return *(SerializableGuid*)&guid;
        }

As seen in editor (including the extended features of the more complicated propertydrawer):
9907089--1431366--upload_2024-6-24_20-10-59.png

9907089--1431369--upload_2024-6-24_20-11-13.png

I use it in the LinkToAsset mode all the time for prefabs and scriptableobjects:
9907089--1431372--upload_2024-6-24_20-12-54.png

Is there really any requirement that you have to use a Guid or something that mimicks one? If you just need 128-bit identifiers, uint4 from the collections package is serializable / equatable and would be a pretty decent alternative to making a new type for this. There’s also UnityEngine.Hash128 which is serializable / equatable / comparable, composed of two internal ulongs.

Setting up random data for any unmanaged struct type would be something like this:

using System;
using System.Security.Cryptography;

static class RandomKeys
{
  public static unsafe void FillRandom<T>(ref T value, bool requireNonDefault = false) where T : unmanaged
  {
    fixed (T* p = &value)
    {
      Span<byte> span = new(p, sizeof(T));
      RandomNumberGenerator.Fill(span);
      if (requireNonDefault)
      {
        T blank = default;
        if (!span.SequenceEqual(new Span<byte>(&blank, sizeof(T))))
        {
          *(byte*)p = 0x42;
        }
      }
    }
  }

  public static T Create<T>(bool requireNonDefault = false) where T : unmanaged
  {
    T result = default;
    FillRandom(ref result, requireNonDefault);
    return result;
  }
}
public uint4 key1 = RandomKeys.Create<uint4>(true);
public Hash128 key2 = RandomKeys.Create<Hash128>(true);
1 Like

N

No, GUID was just the thing I found that seemed to fit my needs, and seemed to do just what I was looking for. I just need an ID system that can generate unique, non-duplicate values. What format it’s in doesn’t particularly matter. And it seems to be doing its purpose fine, until the next time Unity recompiles things, or you close and open the project again, etc. and the serialized string containing the ID generated a new one instead of keeping the old one. And I don’t know why.

So I’ll mention the ISerializableCallback works just fine on structs. I also just threw your code from your OP into a project and it all worked fine.

The only code I can’t test is where you use this (specifically where this “ID” example code came from). What does that code look like?

I imagine this behaviour would happen to existing components after you’ve added this serialised field. As the field hasn’t yet been serialised into the asset (or scene file), there’s no data to pull from when deserialising, thus it will appear to change on every domain reload.

If you were to dirty the asset/scene in some way, and actually cause the guid to write to disk, then it should stop being regenerated each domain reload.

1 Like

Spiney discussed this in another thread . I just tried a few things (Undo.RecordObject/EditorUtility.MarkDirty in OnValidate) that didn’t seem to work. Seems like forcing reserialization works, though that affects a whole scene or disk asset.

#if UNITY_EDITOR
#nullable enable
using System.Collections.Generic;
using UnityEditor;

public static class ForceReserializeSelection
{
    [MenuItem("Edit/Force Reserialize Selection")]
    private static void RunForceReserializeSelection()
    {
        List<string>? paths = null;
        foreach (var obj in Selection.objects)
        {
            string path = AssetDatabase.GetAssetPath(obj);
            if (!string.IsNullOrEmpty(path))
            {
                (paths ??= new List<string>()).Add(path);
            }
        }
        if (paths != null)
        {
            AssetDatabase.ForceReserializeAssets(paths, ForceReserializeAssetsOptions.ReserializeAssetsAndMetadata);
        }
    }
}
#endif

Also, I would probably wrap the initialization of IDs in editor-only blocks so they definitely don’t run in player builds unnecessarily:

public IdentifierType fieldName
#if UNITY_EDITOR
    = InitializeIdentifier()
#endif
    ;