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

Can I call a function when a scriptable object is created?

Discussion in 'Scripting' started by mrCharli3, Dec 4, 2021.

  1. mrCharli3

    mrCharli3

    Joined:
    Mar 22, 2017
    Posts:
    956
    I am assigning ID's to my scriptable objects using a counter to make sure they are always unique, currently I do it using a button.

    I was wondering if I can somehow call a function "SetId()" when a new instance of that scriptable object is created. Similar to Awake and Start but run when u create an instance of that asset.
     
  2. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 20, 2015
    Posts:
    9,907
    It is the
    Awake
    . In case of ScriptableObjects Awake runs when the asset is created. Not in play mode.
     
  3. mrCharli3

    mrCharli3

    Joined:
    Mar 22, 2017
    Posts:
    956
    Hmm, that might be, but it calls Awake at other times as well, all my ID's are now F***ed :D Ill go back to just using the button
     
  4. looytroop

    looytroop

    Joined:
    Jun 22, 2017
    Posts:
    63
    This isn't the best solution probably, but I was just trying to implement something similar to what you're talking about and came up with this as my solution.


    Code (CSharp):
    1. [CreateAssetMenu(menuName = "Story Seek/Inventory Item Data")]
    2. public class InventoryItemData : ScriptableObject
    3. {
    4.     [Sirenix.OdinInspector.ReadOnly]
    5.     public int id = 0;
    6.     public string displayName;
    7.     public InventoryItemType itemType;
    8.     public Sprite icon;
    9.  
    10.     private bool hasIdBeenSet = false;
    11.  
    12. #if UNITY_EDITOR
    13.     public void Awake()
    14.     {
    15.         if (!hasIdBeenSet)
    16.         {
    17.             List<int> ids = new List<int>();
    18.             string path = "t:InventoryItemData";
    19.             string[] assets = UnityEditor.AssetDatabase.FindAssets(path);
    20.             foreach (string guid in assets)
    21.             {
    22.                 InventoryItemData data = UnityEditor.AssetDatabase.LoadAssetAtPath<InventoryItemData>(UnityEditor.AssetDatabase.GUIDToAssetPath(guid));
    23.                 if (data != this)
    24.                 {
    25.                     ids.Add(data.id);
    26.                 }
    27.             }
    28.             while (ids.Contains(this.id))
    29.             {
    30.                 this.id += 1;
    31.             }
    32.             hasIdBeenSet = true;
    33.         }
    34.     }
    35. #endif
    36. }
    The [Sirenix.OdinInspector.ReadOnly] bit is from a package that basically let's me see the data in inspector, but not set it in the inspector, but completely not required. It could also be private if you want.
     
    Last edited: Apr 7, 2022
  5. Corva-Nocta

    Corva-Nocta

    Joined:
    Feb 7, 2013
    Posts:
    801
    I just started working on a similar system, I think I found one that works though and prevents the ID'S from changing randomly.

    Code (csharp):
    1. public int itemID;
    2. private static int nextItemID = 1;
    3.  
    4. private void Awake()
    5. {
    6.    if (itemID == 0) // this prevents thenID shifting randomly
    7.    {
    8.       int highestID = 0;
    9.       Object[] _allItems = Resources.LoadAll("Items");
    10.       foreach (Item _item in _allItems)
    11.       {
    12.          if (_item.itemID > highestID)
    13.          {
    14.             highestId = _item.itemID;
    15.          }
    16.       }
    17.       nextItemID = highestID + 1;
    18.       itemID = nextItemID;
    19.       nextEquipID++;
    20.    }
    21. }
    Fairly certain some parts of this code are redundant, like the public static int might not actually be necessary, I used that when I brought the code over from a different part of the project so I think it can be removed.

    Hope it helps!
     
  6. LandePlage

    LandePlage

    Joined:
    May 21, 2016
    Posts:
    32
    I think I found an even better solution, by using a custom attribute that fetches the GUID from the meta-file every time the ScriptableObject inspector is viewed.

    In my ScriptableObject, I use the "Identifier" attribute.
    Code (CSharp):
    1. [Identifier] public string id;
    IdentifierAttribute.cs
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class IdentifierAttribute : PropertyAttribute
    4. {
    5. }
    IdentifierAttributeDrawer.cs (placed in a subfolder named Editor)
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. [CustomPropertyDrawer(typeof(IdentifierAttribute))]
    5. public class IdentifierAttributeDrawer : PropertyDrawer
    6. {
    7.     bool hasCheckedGUID = false;
    8.  
    9.     public override void OnGUI(Rect rect, SerializedProperty prop, GUIContent label) {
    10.         GUI.enabled = false;
    11.         EditorGUI.PropertyField(rect, prop, label);
    12.         GUI.enabled = true;
    13.  
    14.         if (!hasCheckedGUID)
    15.             FetchGUIDFromFile(prop);
    16.     }
    17.  
    18.     void FetchGUIDFromFile(SerializedProperty prop) {
    19.         if (prop.propertyType != SerializedPropertyType.String)
    20.             return;
    21.  
    22.         if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(prop.serializedObject.targetObject.GetInstanceID(), out string guid, out long localID)) {
    23.             if (prop.stringValue != guid)
    24.                 prop.stringValue = guid;
    25.         }
    26.         hasCheckedGUID = true;
    27.     }
    28. }
    Pros:
    - The ID sets itself, even if we are duplicating another scriptable object, which is what I usually do.
    - Quite reusable, very little boilerplate.

    Downsides:
    - If we're creating scriptable objects without viewing the inspector, this method will likely not work.
    - Need to make sure we're using a string value.

    One way to improve this would be to not use a custom attribute and rather use an "Identifier" class with a custom inspector that does the same thing.
     
  7. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,883
    FWIW for future readers,
    Reset()
    is called when an SO is created, and should suffice for the above needs.

    Mind you, if you want a unique ID for an SO, you can literally just do:

    Code (CSharp):
    1. [SerializeField]
    2. private string _guid = System.Guid.NewGuid().ToString();
    No editor code required. Though, editor cools are probably required to make the field read only.
     
    awahlert likes this.
  8. LandePlage

    LandePlage

    Joined:
    May 21, 2016
    Posts:
    32
    This was the method I used for a while - but when you duplicate a scriptable object, it also duplicates the ID. Other than that, this method is simple and works well.
     
  9. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,883
    Yeah it was an example of the most simple implementation, just to show it doesn't require editor scripting to work.

    Naturally a robust, reliable example will need some form of editor scripting and is definitely worthwhile if you need this on a larger scale. My happy solution was to make a wrapper class
    AssetGUID
    to encapsulate everything, and assets that express a unique ID implement an
    IAssetGUID
    interface.

    Then to ensure when duplicating, say, a scriptable object, I use an AssetModificationProcessor to check for any duplicate GUID's when an asset is created, and generating a new one if there is. The aforementioned processor is also be a good way to generate these on creation of an asset too.
     
    enpiech_unity likes this.
  10. drewva32

    drewva32

    Joined:
    Apr 5, 2020
    Posts:
    2
    Oh I never knew about the AssetModificationProcessor. Thanks for bringing that up. Any chance you could share more about how you go about checking for duplicate GUIDs via the AssetModificationProcessor? Do you use OnWillCreateAsset? I'm stuck on that exact problem of having some event fire when a scriptable object is duplicated so I can generate a new GUID.

    Also, just to throw out a related solution that has worked for me if you need a unique serialized GUID per monobehavior component in the scene, I created a wrapper class GUIDComponent. Scripts that were originally monobehaviors can extend this and be placed on prefabs etc. Only when that prefab exists in a scene, is its GUID generated. You can also duplicate objects in a scene and the copies will generate new GUIDs. However, this method of checking for duplication only works on objects in a scene, and does not work for scriptable object assets when they are duplicated in the project window.

    Code (CSharp):
    1. public class GuidComponent : MonoBehaviour
    2.     {
    3.         [NonEditable] public string GlobalUniqueID;
    4.  
    5. #if UNITY_EDITOR
    6.         private void OnValidate()
    7.         {
    8.             // Ensures that prefab assets always have an empty GUID. (So GUIDs are always set when an instance of this object is created
    9.             // This is only called on prefab assets within the project window.
    10.             if (PrefabUtility.IsPartOfPrefabAsset(this))
    11.             {
    12.                 GlobalUniqueID = string.Empty;
    13.                 return;
    14.             }
    15.  
    16.             // This is an instance that has been placed in a scene. Generate GUID if one does not exist.
    17.             if (string.IsNullOrEmpty(GlobalUniqueID))
    18.                 GlobalUniqueID = Guid.NewGuid().ToString();
    19.  
    20.             // Generate new GUID if the object has been duplicated. (This only gets called on the new copy)
    21.             Event e = Event.current;
    22.             if (e != null)
    23.                 if (e.type == EventType.ExecuteCommand && e.commandName == "Duplicate")
    24.                     GlobalUniqueID = Guid.NewGuid().ToString();
    25.         }
    26. #endif
    27.  
    28.         public virtual void Awake()
    29.         {
    30.             // Runtime spawned instances set their GUID here.
    31.             if (string.IsNullOrEmpty(GlobalUniqueID))
    32.                 GlobalUniqueID = Guid.NewGuid().ToString();
    33.         }
    34.     }
     
    Last edited: Aug 7, 2023
  11. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,883
    I believe it was using
    OnWillCreateAsset
    . I'm not presently about my work station to check.

    Though the general logic was to just check all assets that express my IAssetGUID interface for anything with a guid that matches the existing one. If there is a match, just force re-generate the GUID.

    When I'm home I'll post the code. It wasn't all that much code in the end.
     
    drewva32 likes this.
  12. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,883
    Okay I remembered wrong, I did it with an AssetPostProcessor instead, like so:

    Code (CSharp):
    1. #if UNITY_EDITOR && UNITY_2021_1_OR_NEWER
    2. namespace LBG.Editor.Processors
    3. {
    4.     using UnityEngine;
    5.     using UnityEditor;
    6.     using LBG.Utilities.Editor;
    7.  
    8.     internal sealed class AssetGUIDAssetPostProcessor : AssetPostprocessor
    9.     {
    10. #pragma warning disable UNT0033 // Incorrect message case
    11.         private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths, bool didDomainReload)
    12. #pragma warning restore UNT0033 // Incorrect message case
    13.         {
    14.             if (didDomainReload)
    15.             {
    16.                 return;
    17.             }
    18.  
    19.             int importedCount = importedAssets.Length;
    20.             for (int i = 0; i < importedCount; i++)
    21.             {
    22.                 string assetPath = importedAssets[i];
    23.                 UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<Object>(assetPath);
    24.  
    25.                 if (asset != null && asset is IAssetGUID assetGUID)
    26.                 {
    27.                     bool hasGuid = assetGUID.AssetGUID.HasGeneratedGUID;
    28.                     if (!hasGuid)
    29.                     {
    30.                         Debug.LogWarning($"Asset GUID: {asset.name} does not have a generated GUID.", asset);
    31.                     }
    32.  
    33.                     var duplicates = AssetUtilities.FindAssets(asset.GetType(),
    34.                         x => x is IAssetGUID aGuid && aGuid.AssetGUID == assetGUID.AssetGUID);
    35.  
    36.                     if (duplicates != null && duplicates.Count > 1)
    37.                     {
    38.                         Debug.LogWarning("Asset GUID: Created Asset has same GUID as other assets. Recreating GUID", asset);
    39.                         assetGUID.AssetGUID.ForceGenerateGUID();
    40.                     }
    41.                 }
    42.             }
    43.         }
    44.     }
    45. }
    46. #endif
    I might have to code a toggle to turn this off, as I can see this perhaps causing overhead in large projects.

    AssetUtilities.FindAssets
    is my own robust helper function for finding assets, allowing for a predicate and other options.

    I haven't designed this with prefabs in mind as the need hasn't come up yet. But it'd be simple stuff to add.
     
    Last edited: Aug 7, 2023
    drewva32 likes this.
  13. drewva32

    drewva32

    Joined:
    Apr 5, 2020
    Posts:
    2
    Ahhh okay, thank you for the help! Should definitely be able to get something working after seeing this.