Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

How to handle a serializable class with a Guid

Discussion in 'Scripting' started by paul-masri-stone, Nov 8, 2021.

  1. paul-masri-stone

    paul-masri-stone

    Joined:
    Mar 23, 2020
    Posts:
    49
    I have a serializable class, called MetaCard.
    This is held within a list in a ScriptableObject, called CardDeck.
    I create assets of this ScriptableObject that I manually populate in the inspector.

    Code (CSharp):
    1. [CreateAssetMenu]
    2. public class CardDeck : ScriptableObject
    3. {
    4.     public List<MetaCard> CardList;
    5. }
    So far, so normal.

    However I need each MetaCard to have a unique _id, which I plan to handle with Guid.NewGuid().
    The issue I have is that I don't know when/where in code I should initialize the GUID.

    Obviously a MetaCard's _id should only be set once, on creation. But if I'm creating a new MetaCard within a list by adding an element through the inspector, where in code should I initialize _id and how do I ensure I only do that once per MetaCard? Linked to that, how do I ensure that when I click + to add an extra element to the list it doesn't just duplicate the _id from the last MetaCard?

    Context:
    I'm creating an educational game that uses flashcards. I have a library of flashcards that the game can pick from and present to the user. Once a card has been seen, it needs to be tracked, which I'll do in a saveable 'player learning model'. This means I need to be able to grab the right flashcard at any moment. I plan on using GUIDs to do this.


    As MetaCard includes a Guid and some DateTime, which aren't automatically serialized, I am implementing ISerializationCallbackReceiver. (Which I may not be doing correctly.) Here is my implementation of that:

    Code (CSharp):
    1. [Serializable]
    2. public class MetaCard : ISerializationCallbackReceiver
    3. {
    4.     [SerializeField, HideInInspector]
    5.     private string _serializableGuid;
    6.     private Guid _id = Guid.Empty;
    7.  
    8.     [SerializeField, HideInInspector]
    9.     private string _serializableLearningStepUpdatedAt;
    10.     private DateTime _learningStepUpdatedAt;
    11.  
    12.     [SerializeField, HideInInspector]
    13.     private string _serializableLastSeenAt;
    14.     private DateTime _lastSeenAt;
    15.  
    16.     //... (other fields/properties/methods)
    17.  
    18.     public void OnBeforeSerialize()
    19.     {
    20.         _serializableGuid = _id.ToString();
    21.         _serializableLearningStepUpdatedAt = _learningStepUpdatedAt.ToString("s");
    22.         _serializableLastSeenAt = _lastSeenAt.ToString("s");
    23.     }
    24.  
    25.     public void OnAfterDeserialize()
    26.     {
    27.         if (_serializableGuid == null)
    28.             _id = Guid.Empty;
    29.         else
    30.             _id = new Guid(_serializableGuid);
    31.  
    32.         _learningStepUpdatedAt = System.Convert.ToDateTime(_serializableLearningStepUpdatedAt);
    33.         _lastSeenAt = System.Convert.ToDateTime(_serializableLastSeenAt);
    34.     }
    35. }
     
  2. RadRedPanda

    RadRedPanda

    Joined:
    May 9, 2018
    Posts:
    1,593
    One could create a static MetaCard method, which increments and returns a unique ID (a static int) for each MetaCard to use. This would also keep track of how many MetaCards there are if that were useful.
     
  3. paul-masri-stone

    paul-masri-stone

    Joined:
    Mar 23, 2020
    Posts:
    49
    Maybe my question isn't clear enough. What I require is that when I add an item to the list in the inspector, the new item gets its own GUID.

    My understanding is that when I click +, the parameterless constructor of MetaCard gets called. If I set _id = Guid.NewGuid() in that constructor, it does indeed create a MetaCard with a GUID for the first one in the list. But each time I subsequently click +, the new items are duplicates of the last one, so it looks like it must deserialize the last one over the newly constructed item, overwriting _id.

    Of course, I want the IDs to remain the same if I close and reopen Unity, so correct deserialization is important... only not at the point of creating a new item.

    I don't see how a static method solves this issue. (It would not be of value to me to keep track of total number.)
     
  4. PizzaPie

    PizzaPie

    Joined:
    Oct 11, 2015
    Posts:
    103
    A not so clean way to do it is by adding an Add function on your CardDeck custom inspector and initializing the MetaCards there while adding them in the list.
     
    paul-masri-stone likes this.
  5. RadRedPanda

    RadRedPanda

    Joined:
    May 9, 2018
    Posts:
    1,593
    Ah, ok, I may not have read through your problem thoroughly.

    I ended up looking into this topic, and it may also not be the best solution, but you could look into using a ReorderableList, which by default has callbacks for things you do to the list, including adding values. This also includes a fancy inspector thing specifically for reordering them (hence the name), but it's not officially in the documentation for some reason. My assumption is that the callback for adding values will be run when not in Play mode, however I haven't tested it myself.
     
  6. jasonboukheir

    jasonboukheir

    Joined:
    May 3, 2017
    Posts:
    80
    paul-masri-stone likes this.
  7. paul-masri-stone

    paul-masri-stone

    Joined:
    Mar 23, 2020
    Posts:
    49
    Thank you all for your suggestions.

    My central takeaway from this is: there isn't an easy way to do it that slots in transparently.

    Making MetaCard a ScriptableObject and then getting its InstanceID is an interesting approach. I was trying to avoid having a ton of ScriptableObject files, but I can see this working, and it is simple.

    Alternatively, the custom inspector on CardDeck could work. Not ideal as I could see myself accidentally clicking the + but I can see this working.

    I think I'll avoid undocumented features – I've been burned on other software frameworks where internal implementation details can change and undocumented/unspecified behaviours break.
     
  8. paul-masri-stone

    paul-masri-stone

    Joined:
    Mar 23, 2020
    Posts:
    49
    I had another suggestion from someone else, that pointed me to
    OnValidate()
    . I wasn't familiar with this. It's a method that gets called by the Unity editor on compiling classes and on changes to the item in the inspector.

    My solution is to use this method to check for any empty or non-unique
    _id
    s and generate a new ID at that time. (
    _id
    would be empty on creation of the first element in a list and it would be non-unique on creation of subsequent elements, since they are duplicates of the last element.)

    To make these changes, I added the following to
    MetaCard
    :
    Code (CSharp):
    1.     public Guid Id => _id; //readonly property exposing the ID
    2.  
    3.     public void GenerateNewId()
    4.     {
    5.         _id = Guid.NewGuid();
    6.     }

    Here's the
    OnValidate()
    within
    CardDeck
    :
    Code (CSharp):
    1. #if UNITY_EDITOR
    2.     private void OnValidate()
    3.     {
    4.         // Ensure each card has a valid & unique ID
    5.         var ids = new List<Guid>();
    6.  
    7.         foreach (MetaCard card in CardList)
    8.         {
    9.             if (card.Id == Guid.Empty || ids.Contains(card.Id))
    10.             {
    11.                 card.GenerateNewId();
    12.                 UnityEditor.EditorUtility.SetDirty(this);
    13.             }
    14.  
    15.             ids.Add(card.Id);
    16.         }
    17.     }
    18. #endif
    Although I'm grateful for all the suggestions made, what I like about this approach is:
    • Although this code gets run relatively often, it only gets done in the editor so it won't impact a build version.
    • It's one compact piece of code that hasn't required me to change the architecture of my classes (e.g.
      MetaCard
      doesn't have to become a
      ScriptableObject
      ).
    • It hasn't required altering standard behaviour using a custom editor.