Unity: Scriptable Object Indexing System

Unity: Scriptable Object Indexing System

The asset indexing system is a little editor system I developed to cache references to scriptable objects into a serializable dictionary, for fast and easy access both in the editor and at runtime. I’ve used this system in all the asset projects I’ve released since I developed it and it has proven its worth. Its also the same system in all bar name that is used in the Common library’s data asset setup.

What it looks like

A few examples of what the indexes look like when populated. It uses a small serializable dictionary class that is pretty much a list of a serializeable keypair class which inherits from the normal dictionary class.

The key for each asset entry is its full name including the namespace. The value is then a list of the base class for a data asset, which is just a normal scriptable object inheriting class. I set it up as a list purely as there could be multiple of the same type.

The asset index object is stored in the resources folder to allow runtime access through static methods. As its only the single asset instead of all of them the performance impact in negligible.

The updating class

The class that updates the index runs when pre-builds and when you are about the enter play-mode to ensure the data is the most up-to date when you run the game project. With the additional option of using a menu item to update it manually if needed. A copy of the updater class below. NOTE: This class uses extension methods to make the editor API a bit shorter which is also shown below:

using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;

namespace CarterGames.Common.Editor
{
    /// <summary>
    /// Handles the setup of the asset index for runtime references to scriptable objects used for the asset.
    /// </summary>
    public sealed class AssetIndexHandler : IPreprocessBuildWithReport
    {
        /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        |   Fields
        โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */

        private const string AssetFilter = "t:commonlibraryasset";        // Filters by the data asset base class.
        
        /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        |   IPreprocessBuildWithReport Implementation
        โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
        
        /// <summary>
        /// The order this script is processed in, in this case its the default.
        /// </summary>
        public int callbackOrder => 0;
        
        
        /// <summary>
        /// Runs before a build is executed.
        /// </summary>
        /// <param name="report">The report about the build (I don't need it, but its a param for the method).</param>
        public void OnPreprocessBuild(BuildReport report)
        {
            UpdateIndex();
        }
        
        /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        |   Methods
        โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
        
        /// <summary>
        /// Initializes the event subscription needed for this to work in editor.
        /// </summary>
        [InitializeOnLoadMethod]
        private static void Initialize()
        {
            EditorApplication.update -= OnEditorUpdate;
            EditorApplication.update += OnEditorUpdate;
        }


        /// <summary>
        /// Runs when the editor has updated.
        /// </summary>
        private static void OnEditorUpdate()
        {
            // If the user is about to enter play-mode, update the index, otherwise leave it be. 
            if (!EditorApplication.isPlayingOrWillChangePlaymode || EditorApplication.isPlaying) return;
            UpdateIndex();
        }


        /// <summary>
        /// Updates the index with all the save manager asset scriptable objects in the project.
        /// </summary>
        [MenuItem("Tools/Carter Games/Common/Update Asset Index")]
        public static void UpdateIndex()
        {
            var foundAssets = new List<CommonLibraryAsset>();
            var asset = AssetDatabase.FindAssets(AssetFilter, null);
            
            if (asset == null || asset.Length <= 0) return;

            foreach (var assetInstance in asset)
            {
                var assetPath = AssetDatabase.GUIDToAssetPath(assetInstance);
                var assetObj = (CommonLibraryAsset) AssetDatabase.LoadAssetAtPath(assetPath, typeof(CommonLibraryAsset));
                
                // Doesn't include editor only or the index itself.
                if (assetObj == null) continue;
                if (assetObj.GetType() == typeof(CommonLibraryAssetIndex)) continue;
                foundAssets.Add((CommonLibraryAsset) AssetDatabase.LoadAssetAtPath(assetPath, typeof(CommonLibraryAsset)));
            }

            var indexProp = new SerializedObject(UtilEditor.AssetIndex);
            
            RemoveNullReferences(indexProp);
            UpdateIndexReferences(foundAssets, indexProp);
            
            indexProp.ApplyModifiedProperties();
            indexProp.Update();
        }


        private static void RemoveNullReferences(SerializedObject indexProp)
        {
            for (var i = 0; i < indexProp.Fp("assets").Fpr("list").arraySize; i++)
            {
                var entry = indexProp.Fp("assets").Fpr("list").GetIndex(i);
                var jIndexAdjustment = 0;

                for (var j = 0; j < entry.Fpr("value").arraySize; j++)
                {
                    if (entry.Fpr("value").GetIndex(j - jIndexAdjustment).objectReferenceValue != null) continue;
                    entry.Fpr("value").DeleteIndex(j);
                    jIndexAdjustment++;
                }
            }
        }


        private static void UpdateIndexReferences(IReadOnlyList<CommonLibraryAsset> foundAssets, SerializedObject indexProp)
        {
            for (var i = 0; i < foundAssets.Count; i++)
            {
                for (var j = 0; j < indexProp.Fp("assets").Fpr("list").arraySize; j++)
                {
                    var entry = indexProp.Fp("assets").Fpr("list").GetIndex(j);
                    
                    if (entry.Fpr("key").stringValue.Equals(foundAssets[i].GetType().ToString()))
                    {
                        for (var k = 0; k < entry.Fpr("value").arraySize; k++)
                        {
                            if (entry.Fpr("value").GetIndex(k).objectReferenceValue == foundAssets[i]) goto AlreadyExists;
                        }
                        
                        entry.Fpr("value").InsertIndex(entry.Fpr("value").arraySize);
                        entry.Fpr("value").GetIndex(entry.Fpr("value").arraySize - 1).objectReferenceValue = foundAssets[i];
                        goto AlreadyExists;
                    }
                }
                
                indexProp.Fp("assets").Fpr("list").InsertIndex(indexProp.Fp("assets").Fpr("list").arraySize);
                indexProp.Fp("assets").Fpr("list").GetIndex(indexProp.Fp("assets").Fpr("list").arraySize - 1).Fpr("key").stringValue = foundAssets[i].GetType().ToString();
                
                if (indexProp.Fp("assets").Fpr("list").GetIndex(indexProp.Fp("assets").Fpr("list").arraySize - 1).Fpr("value").arraySize > 0)
                {
                    indexProp.Fp("assets").Fpr("list").GetIndex(indexProp.Fp("assets").Fpr("list").arraySize - 1)
                        .Fpr("value").ClearArray();
                }
                
                indexProp.Fp("assets").Fpr("list").GetIndex(indexProp.Fp("assets").Fpr("list").arraySize - 1).Fpr("value").InsertIndex(0);
                indexProp.Fp("assets").Fpr("list").GetIndex(indexProp.Fp("assets").Fpr("list").arraySize - 1)
                    .Fpr("value").GetIndex(0).objectReferenceValue = foundAssets[i];

                AlreadyExists: ;
            } 
        }
    }
}

The extension class that I use to make the editor API for serialized properties a little nicer:

using UnityEditor;

namespace CarterGames.Common.Editor
{
    /// <summary>
    /// A helper class to aid with editor scripting where the API is really wordy...
    /// </summary>
    public static class SerializedPropertyHelper
    {
        /// <summary>
        /// Calls InsertArrayElementAtIndex()
        /// </summary>
        /// <param name="property">The property.</param>
        /// <param name="index">The index.</param>
        public static void InsertIndex(this SerializedProperty property, int index)
        {
            property.InsertArrayElementAtIndex(index);
        }
        
        
        /// <summary>
        /// Calls DeleteArrayElementAtIndex()
        /// </summary>
        /// <param name="property">The property.</param>
        /// <param name="index">The index.</param>
        public static void DeleteIndex(this SerializedProperty property, int index)
        {
            property.DeleteArrayElementAtIndex(index);
        }
        
        
        /// <summary>
        /// Calls GetArrayElementAtIndex()
        /// </summary>
        /// <param name="property">The property.</param>
        /// <param name="index">The index.</param>
        /// <returns>The property at the index entered.</returns>
        public static SerializedProperty GetIndex(this SerializedProperty property, int index)
        {
            return property.GetArrayElementAtIndex(index);
        }
        
        
        /// <summary>
        /// Calls FindProperty()
        /// </summary>
        /// <param name="serializedObject">The target object.</param>
        /// <param name="propName">The name of the property.</param>
        /// <returns>The found property.</returns>
        public static SerializedProperty Fp(this SerializedObject serializedObject, string propName)
        {
            return serializedObject.FindProperty(propName);
        }
        
        
        /// <summary>
        /// Calls FindPropertyRelative()
        /// </summary>
        /// <param name="property">The target property.</param>
        /// <param name="propName">The name of the property.</param>
        /// <returns>The found property.</returns>
        public static SerializedProperty Fpr(this SerializedProperty property, string propName)
        {
            return property.FindPropertyRelative(propName);
        }
    }
}

The asset index

This is the class that handles storing the assets for use. The SerializableDictionary is a class that I use can be found here: https://github.com/CarterGames/Common/tree/main/Code/Runtime/Serialization/Dictionary.

using System.Collections.Generic;
using CarterGames.Common.General;
using UnityEngine;

namespace CarterGames.Common
{
    public sealed class CommonLibraryAssetIndex : CommonLibraryAsset
    {
        /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        |   Fields
        โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
        
        [SerializeField] private SerializableDictionary<string, List<CommonLibraryAsset>> assets;

        /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        |   Properties
        โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */

        /// <summary>
        /// A lookup of all the assets in the project that can be used at runtime.
        /// </summary>
        public SerializableDictionary<string, List<CommonLibraryAsset>> Lookup => assets;

Accessing data

To access the data I use a accessor class. The class caches a reference to the index scriptable object from the resources folder for use. From there I just look through the dictionary for the class type as a key and return the data stored. An example of the class here:

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace CarterGames.Common
{
    public static class CommonAssetAccessor
    {
        /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        |   Fields
        โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
     
        private const string IndexPath = "Asset Index";
        
        
        // A cache of all the assets found...
        private static CommonLibraryAssetIndex indexCache;

        /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        |   Properties
        โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */

        /// <summary>
        /// Gets all the assets from the build versions asset...
        /// </summary>
        public static CommonLibraryAssetIndex Index
        {
            get
            {
                if (indexCache != null) return indexCache;
                indexCache = (CommonLibraryAssetIndex) Resources.Load(IndexPath, typeof(CommonLibraryAssetIndex));
                return indexCache;
            }
        }

        /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        |   Methods
        โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */

        /// <summary>
        /// Gets the Save Manager Asset requested.
        /// </summary>
        /// <typeparam name="T">The save manager asset to get.</typeparam>
        /// <returns>The asset if it exists.</returns>
        public static T GetAsset<T>() where T : CommonLibraryAsset
        {
            if (Index.Lookup.ContainsKey(typeof(T).ToString()))
            {
                return (T)Index.Lookup[typeof(T).ToString()][0];
            }

            return null;
        }
        
        
        /// <summary>
        /// Gets the Save Manager Asset requested.
        /// </summary>
        /// <typeparam name="T">The save manager asset to get.</typeparam>
        /// <returns>The asset if it exists.</returns>
        public static List<T> GetAssets<T>() where T : CommonLibraryAsset
        {
            if (Index.Lookup.ContainsKey(typeof(T).ToString()))
            {
                return Index.Lookup[typeof(T).ToString()].Cast<T>().ToList();
            }

            return null;
        }
    }
}

Can I use this?

Of-course you can. The system is built-in to the common library which is the best way to use this system. Each of my assets also use it but their implementations are focused on the asset itself and is not intended for the dev using it as well. See more on the library here: https://carter.games/common

An Update

An Update

, ,

An update on the whole Unity situation for Carter Games.


Just a quick post to update y’all on the whole Unity thingy. So as you’ll have likely seen by now they have announced they were reverting pretty much every change with the runtime fee thingy. The news is pretty good actually. Had they announced that to start with everyone wouldโ€™ve liked it for the most part. Sadly they didnโ€™t and the damage in trust is done. The fact that the original plan was what they though was good, despite their insiders from the community & staff supposedly warning them it wasnโ€™t, just shows what matters to Unity as a company. Money & greed.

Iโ€™ve never been a fan of public companies & shareholders etc. Being a shareholder should mean taking the risk of paying out when a company is doing bad as well as getting some extra money when its doing well. Not just free money which it seems to be treated as today. I could rant about that for ages, but you get the idea.

As for Carter Games and its future developments. Iโ€™ll still be working in Unity for now with the assets Iโ€™ve made. Sadly other engines donโ€™t quite have the things I like about Unity in them in quite as nicer setup. Iโ€™ve given both Unreal & Godot a go over the last few weeks. Godot certainly has promise, but its missing some key things for me currently. Those being a more in-depth inspector for exposing data classes, world space UI & some 3D bits that are early days. I do like the whole node setup though, that is nice. As for Unreal, looks good, but like 0 docs for C++ code which is a bit annoying. Blueprint is good and all, but Iโ€™d prefer to do the C++ a little if I went that route.

So future plans. I do plan to keep with Unity development stuff for now. So Audio Manager 3.x is still on the books to be released this year with an update to Leaderboard Manager sometime early next year. Next year is still in the air for what Iโ€™m doing, I do have some active side projects I want to finish as well as a game idea or two to flesh out and maybe prototype. But more specifics will be posted in the year in review post for this year. As for porting asset and stuff to other engines, some of them may be portable in a slightly different setup to Godot. But Iโ€™d need to do a lot more research. Iโ€™m also not sure Iโ€™d be able to make the same quality of asset automation in other engines, but weโ€™ll see, itโ€™s not totally off the table in the future. Audio Manager 3.x will likely be a bit delayed againโ€ฆ..I know, its a pain, but I want to get it right. That and it just takes time to develop, just like how games take a long time to make. Iโ€™ll keep yโ€™all up to date on how thatโ€™s going.

Thatโ€™s all for now xD

~ J,

May 2023 Update

May 2023 Update

Its been about a month since my last update post. So its about time I updated you all on developments over the last few weeks. Starting on from the last post withโ€ฆ

Save Manager 2.x

As you will have seen by now, the new Save Manager has been released! So far so good. There hasnโ€™t been any major problems from what I can tell so far as Iโ€™ve been using it in my personal projects to handle save data there. I have made a few minor updates to the asset since launch to fix some minor issues, most of which didnโ€™t actually break anything. But they would throw some console messages which could confuse users.

As of writing the asset is on version 2.0.4. Some improvements were also made such as an option to hide the save keys for induvidual save values, reducing the clutter on the save editor. As well as a fix to allow the demo logic to be totally removed from the asset without any errors occurring. Before there was a direct type reference in the save editor window logic to determine if it would show the demo save object data. This has been changed to a type string comparison to fix the issue.

There are plans for some further QOL & feature updates in the future, such as save slots & a better API to allow for custom encryption options. These will be worked on once I get some time to look into it.

You can get the new Save Manager here:

Whatโ€™s Next?

It wonโ€™t be a surprise to those who read the year in review for 2022 or been following on the socials, that the next main project is the Audio Manager update. Like the Save Manager, it is getting a rewrite in the backend. This is mostly due to the 2.xโ€™s setup not being easy to update for the new features & setup I want to implement next.

The current plan is to have this update out by the end of August. Similar to the deadline of April I gave myself for the Save Manager. So 4 months to fully develop & release the asset update. Now this isnโ€™t a flexible deadline and may change if its just not ready in time. Likewise, if it is ready earlier than planned then itโ€™ll release early. Iโ€™ll post updates with progress and a release date nearer the time as well.

Functionally the Audio Manager wonโ€™t change much for the end user, but behind the scenes a lot of work is going to be done to make it just a lot easier to use. Some of these include:

  • Automatic Library Management
    • Youโ€™ll no longer need to manually scan for clips via and inspector or anything. When you import audio, the manager will find a process it automatically for you. There will be an option to manually scan still if needed. This also makes the setup simpler which is a bonus!
  • Static Audio Manager
    • While having the option of an instanced manager can be handy, 99% of the time it wonโ€™t be needed. So in 3.x the whole system is static. So no need to use โ€œinstanceโ€ when calling for audio to play.
  • New Library Window
    • While 2.x had a basic library editor for each audio manager file. 3.x will have a proper editor window with all the settings you need. You can even change the key of the clip! It also has a search option to search through the entire library and select any clip with ease.
  • Helper Structs
    • A new system for 3.x that will make typoโ€™s a thing of the past. Instead of calling โ€œAudioManager.Play(โ€MyClipโ€)โ€ youโ€™ll be able to call a helper class called โ€œClipโ€ to let you select the clip with intellisense. So that same call becomes โ€œAudioManager.Play(Clip.MyClip)โ€. There are other helper structs for groups and mixers as well.
  • Grouping
    • Speaking of groups, youโ€™ll be able to group clips together in the audio library and call for a clip from that group to play. Handle if you have a lot of variations or want to play a clip from a range of options.
  • Dynamic Start Time
    • An experimental feature that tries to guess when the clip actually starts playing audio at a loud enough volume. Then sets the clip to start just before then instead of at 0. You can toggle the feature on/off globally, per clip in the library or via the method call itself. Youโ€™ll also be able to adjust the settings to get a better guess time in the audio library.

Currently it is still early days and some of these features may change a tad. One thing I mentioned in my last post was a method chaining API for calling audio to play. Sadly Iโ€™ve had to scrap this, but Iโ€™m still working to make the API better as a whole. The amount of methods in 2.x was a bit much, so Iโ€™ll be consolidating and using a better base method to avoid issues with call parameters.

Overall the idea is to build upon 2.xโ€™s success with new QOL features & automation where possible. The music side of things is in the air at the moment, as Iโ€™ve always struggled with a good generic solution for it. But if all goes well Iโ€™ll have a little time to research something for that. Iโ€™ve already had a few suggestions for it which I have noted down.

Other Projects

As mentioned in my last post. Iโ€™m also working on a personal project as well to kind of replicate P5โ€™s turn based combat into a small demo game of just that mechanic. This is entirely for portfolioโ€™s sake as itโ€™ll make for a strong project that I can talk about in detail with a lot of systems. It also makes a good testbed for these asset updates. Save Manager 2.x was tested in the project to make sure it was all working before release and Audio Manager 3.x will get the same testing as and when its at that stage. That project is going well at the moment and I juggle my time between both it and the active Carter Games project to keep it fresh.

Organisation

This week I spent an evening or two organising the mess that was the Carter Games Notion. I legit didnโ€™t use it as it was such a mess to find anything. With the new setup I will be using it a lot more. While its not something you see much, it does help to have a goal in mind and to write down tasks so you know where youโ€™re at and what is left to do. Below is the Audio Manager task board for example:

The new structure has goals as โ€œEpicsโ€ which then have โ€œTasksโ€ to complete. Epics & Tasks are linked to โ€œProjectsโ€ which hold branding, links & anything else specific to that project on them. Even this blog post is been written in Notion under the new Marketing section xD

Website & Socials

A little heads up that I plan to change up the discord a tad at sometime. Mostly just removing bloat channels and making super simple. Its not used that much anyways and I want to have a place for update notifications for assets/games as well as the general news/announcements section.

Iโ€™ve also slowly been updating the documentation for all the assets to the new docs setup on the website. Iโ€™m mostly there at this point with Leaderboard Manager being the only asset to fully update. Iโ€™m hoping to have that all done at some point xD

So yea, thatโ€™s about it for this post. Long story short; a lot going on behind the scenes xD Iโ€™ll post another update around the end of June with progress if there is enough to share.

C# Actions

So, you’ve probably done this at-least once as a developer, you’ve got a value or element of your game that needs regularly checking and updating, like; player health, score or if a level has ended. Now you can just check for these in update which in small games is fine, but you’ll find you are calling this code way more than you really need to, if only there was a way to eliminate that xD.

Introducing Actions

This is where actions come into play. They work like events such as the OnClick on a Unity UI button. When you invoke them they run, otherwise to stay as disabled. What makes them handy, like how you can have multiple methods run at once of a button is that you can add and remove methods or delegations on the fly as well as listen for actions being invoked from other scripts. Here’s the four main steps to using actions in you code:

  • Initialise the action, you can pass varied parameters into the action to pass through information you may need to use in the actions subscribers.
// Its recommended to make the action null be default, 
// as you can check for this when you call the action. 
public Action<float, float> OnHealthChange = null;
  • Subscribe our method or delegate our code to run at this point. Methods are easier to manage and are generally recommended, but if you just need to do a quick value change a delegate will work too. Its normally best to do this when the action has no parameters though. It is important to note that your method or delegation must have the same parameters defined in the initialisation.
// Subscribing to the action 
OnHealthChange += UpdateHealthBar;

// The method setup, note the parameters are the same as the action init
private void UpdateHealthBar(float health, float maxHealth)
  • Invoke the action, doing this will call all the methods subscribed to the action.
// Invoking the action... (NOTE: that we call with a null check ("?.") 
// So it doesn't invoke if nothing is subscribed to it. 
OnHealthChange?.Invoke();
  • Un-Subscribe from the action to avoid null exception errors. Recommended to run this on OnDisable or similar to avoid errors.
// Un-subscribes from the action 
OnHealthChange -= UpdateHealthBar;

Usage Example

Here’s an example which is what I used to learn how to use actions.

Health.cs (on a player)

using System;
using UnityEngine;

public class Health : MonoBehaviuor
{
	[SerialzeField] private float maxHealth = 10f;
	private float currentHealth = 10f;

	// init actions
	public Action <float, float> OnHealthChange = null;
	public Action OnNoHealth = null;

	
	public void TakeDamage(float dmg)
	{
		currentHealth -= dmg;
		// Invoking action....
		OnHealthChange?.Invoke(currentHealth, maxHealth);

		if (currentHealth <= 0)
			OnNoHealth?.Invoke();
	}
}

Healthbar.cs (on the health bar UI for the player)

using System;
using UnityEngine;

public class HealthBar : Monobehaviour
{
	[SerialzeField] private Image hpImage;
	
	private Health health;

	private void OnDisable()
	{
		// Un-subscribing from action...
		health.OnHealthChange -= UpdateHPBar;
	}

	private void Start()
	{
		// subscribing to action
		health.OnHealthChange += UpdateHPBar;
		hpSlider = GetComponent<Slider>();
	}

	// Method that has subscribed to the OnHealthChange method
       // Note: It has the same parameters!
	private void UpdateHPBar(float currentHealth, float maxHealth)
	{
		hpSlider.fillAmount = currentHealth / maxHealth;
	}
}

End of 2021 Plans

End of 2021 Plans

With the end of the year fast approaching, we feel it is a good time to announce our plans for the rest of the year. We achieved most of our plans from the summer roadmap, though it did take longer than we initially planned. Because of this we are behind on making a new game project which we will be postponing until the new year.

Spooky Logo

You may have also noticed a logo change recently, this is our yearly bit of fun where we make logo variations based on the time of year. With Halloween fast approaching, we felt we needed to make the logo a little spooky for the fun of it.

The Carter Games 2021 Halloween Banner

The Plan

Those of you who follow our social media will know that we have had a few new assets in the works for a while now. Well we plan to finish these up and release them before the year is up. Those who have looked at our Unity Asset Store page recently will note that the Leaderboard Manager is still out of date. So this is also on the plate for the end of year work. We held off on the Leaderboard Manager for a bit as the asset needed a total re-write, so the new version will be a 2.0.0 release as and when we release it.

So a breakdown of what each asset does and when to expect the update roughlty:

Leaderboard Manager | End of year

Leaderboard Manager is a scripting system that allows users to implement leaderboard into their games with reletive ease. The current asset works, but is rather basic and has some limitations that we would like to remove. Given the amount of work needed to make the Leaderboard Manager work as we want. We expect this asset to be the last one we release this year.

Drop Tables | November

This asset is one of the new ones. The idea is to provided users with a way of creating multiple loot tables with ease with a 1 in X style which are user friendly. So a change of 1 in 4 would be common while 1 in 5000 would be super rare for example. The asset also allows the user to define drops that always get rolled if needed. The asset is currently at the testing stage and we are working out the kinks with the backend to make it usable in projects of any kind of the gate. We expect this asset to be ready sometime in November.

Build Versions | End of October

This is a lightweight asset that is designed to just help the developer automate updating their game build numbers. Which will happen automatically each time the developer makes a build. We plan to have options to change when the build is incremented, options to edit the player settings x.x.x number and options to edit the โ€œtypeโ€ of build. All of data is available in a scriptable object which can be easily implemented into the developers project. This asset is the one we are currently focusing our efforts and we expect this asset to be out by the end of October at the latest.

Closing Notes

So that is the plan ๐Ÿ˜€ Whether or not we achieve it is another question entirely. But we hope that getting these assets out this year will clear our schedules a little so we can start some new projects going into the new year.