Leaderboard Manager (2.1.0)

Leaderboard Manager (2.1.0)

While it is a little delayed, the new rather long awaited Leaderboard Manager update is here. So whatโ€™s new?

Summary of changes

Mechanically the asset is pretty much the same as the earlier 2.0.x versions of the asset but with some updated backend logic for future updates. A full rundown of the behind the scenes changes:

  • New saving system similar to the Save Managerโ€™s setup with a JSON save setup instead of the legacy binary save file setup.
  • Updated the structure of leaderboards to allow each entry to have its own unique identifier so you can reference an entry easier.

There is a porting tool to update old saves to the new structure. More on this in a moment.

  • New settings structure to match the rest of the assets released recently with per user settings and a settings provider flow.
  • Offline documentation is now complete instead of a simpler version. The online docs are also updated to match the rest of the assets using Notion as a host (easier to maintain in a pinch and has search built in).

There are also a new editor to allow to viewing of existing leaderboards as well as a setup to port older saves to this new version, both in the editor and at runtime.

Editor

With the editor tab you can view all leaderboards currently in the save file. You canโ€™t make new ones here as the API isnโ€™t designed to have boards made in the editor, instead having them made at runtime. You can however delete board should you need to test the setup and view the entries to make sure they appear as expected which should help with debugging.

Porting data to 2.1.x from 2.0.x

As mentioned the new structure means any old data from the previous version will need updating. A full rundown is in the documentation for the asset. But basically if you used the old setup to save the score as time by saving the score in seconds etc. As you can now save score as into a time class instead. There isnโ€™t any major work you need to do to achieve this, just define the board ids and the type to convert them to and the asset should take care of the rest.

Note: you will need to convert boards even if they were just a score field.

An example below of a board called โ€œArcadeโ€ being converted to a score board in the new version.

Whatโ€™s next?

With this update done, Iโ€™m mainly focusing on support and getting back into my main side project for the year as Iโ€™ve had to take some time off of it to get this out. I do have a few game ideaโ€™s that I may prototype at some point or at-least write out some ideas for next year to work on proper. More on these when I have something to share.

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

Unity Editor: Menu Items

Unity Editor: Menu Items

When making a tool, you may want the user to be able to press a button in the navigation menu in Unity. Whether that be to open a editor window, perform a function or clear for player prefs. Making menu items can be very useful for your tools. This post will go through how to set them up and any quirks I found when making my own tools.

Basic setup up

To setup a menu item you need to apply the attribute [MenuItem] to any static editor method. The method doesn’t need to be public for it to work with this attribute. The method does need to be a method either in the editor space such as an editor folder or in an #if define with UNITY_EDITOR, otherwise you won’t be able to make a build of your project. An example below:

This produces the following once compiled:

using UnityEditor;            // Menu item is in the editor namespace.

public static class MyEditorClass 
{
    [MenuItem("My Custom Menu/My Menu Item")]
    private static void OnMenuItemPressed()
    {
        
    }
}

Attribute settings

The attribute requires you to give it a name/path to use. It can go under existing tabs in the navigation menu if you want it to. Just use that tab name as the root and it should go under that tab when comiled. You can also set the order for the item to draw at as an optional override, which can be finiky to get working. Some general tips for setting it up:

  • The menu path cannot be a single item, it much have a root path to be under:
// Valid - Has a parent path.
[MenuItem("My Custom Menu/My Menu Item")]
private static void OnMenuItemPressed()
{
    
}

// Invalid - Will throw an error in the editor.
[MenuItem("My Menu Item")]
private static void OnMenuItemPressed()
{
    
}

Setting the priority can be a pain, sometimes you may need to reload the project for it to take effect while other times it’ll update just fine when compiling. You can get the dividers to show if your priority is greater than 10 from the last in the menu path.

This example produced the following:

using UnityEditor;            // Menu item is in the editor namespace.

public static class MyEditorClass 
{
    [MenuItem("My Custom Menu/My Menu Item", priority = 1)]
    private static void OnMenuItemPressed()
    {
        
    }
    
    
    [MenuItem("My Custom Menu/My Menu Item 2", priority = 13)]
    private static void OnMenuItemPressedAsWell()
    {
        
    }
    
    
    [MenuItem("My Custom Menu/My Menu Item 3", priority = 2)]
    private static void OnMenuItemPressedAlso()
    {
        
    }
    
    
    [MenuItem("My Custom Menu/My Menu Item 4", priority = 100)]
    private static void OnMenuItemPressedAgain()
    {
        
    }
}

If you want, you can also add custom shortcuts to your frequently used menu items. Add them after your I’d be careful with these and only use them when you know its going to be used often as you’ll like hit a conflict with another tool in larger projects.

  • % or ^ = CTRL
  • # = SHIFT
  • & = ALT
  • _ = No prefix buttons

You can combine these together to to make sequences like CTRL – ATL etc. If you just want say “g” to run it without a pre-req button using the _ beforehand to do that.

using UnityEditor;            // Menu item is in the editor namespace.

public static class MyEditorClass 
{
    [MenuItem("My Custom Menu/My Menu Item", priority = 1)]
    private static void OnMenuItemPressed()
    {
    }
    
    
    // Will also run on the shortcut: CTRL-G
    [MenuItem("My Custom Menu/My Menu Item/My Sub-Menu Item %g")]
    private static void OnMenuItemPressedAsWell()
    {
        // Will appear in a sub menu...
    }
}

You can also sub-menu items by making the path longer using a dash to seperate the menu items. This can go on for a long as you like, but I wouldn’t go too mad if I were you. The example here produces:

using UnityEditor;            // Menu item is in the editor namespace.

public static class MyEditorClass 
{
    [MenuItem("My Custom Menu/My Menu Item", priority = 1)]
    private static void OnMenuItemPressed()
    {
    }
    
    
    [MenuItem("My Custom Menu/My Menu Item/My Sub-Menu Item")]
    private static void OnMenuItemPressedAsWell()
    {
        // Will appear in a sub menu...
    }
}
  • There is also an additional override field for a validate function. Though I have yet to find a use-case for this personally in my tools so far. But if you define a menu item method as a validation function, you can do checks before running a menu item like so:
using UnityEditor;            // Menu item is in the editor namespace.

public static class MyEditorClass 
{
    [MenuItem("My Custom Menu/My Menu Item", priority = 1)]
    private static void OnMenuItemPressed()
    {
        // Logic to run only if the check was true...
    }
    
    
    // Note the menu item path is the same as the actual logic method!
    [MenuItem("My Custom Menu/My Menu Item", true)]
    private static bool ValidateOnMenuItemPressed()
    {
        // Do a check here or something...
        return true;
    }
}

Further reading

Unity docs for menu items: https://docs.unity3d.com/ScriptReference/MenuItem.html

Audio Manager 3.x Release!

Audio Manager 3.x Release!

, , , ,

A post annoucing the release of Audio Manager 3.x & an update to the Save Manager as well.


Audio Manager 3.x

Its been a long time coming, but today the new Audio Manager version is released. Unlike previous releases I’m going to stagger this one to release on the Unity Asset Store next year. This is mostly due to the asset’s stability not being great and the high likelyness of bugs from the total re-write. The new version has a lot of new features that have been teased before, but a quick rundown of some of the features:

  • Automatic scanning of audio clips in the project without any user input needed.
  • Dynamic start time for each clip to start where it starts playing audable audio, cutting out deadspace on clips and saves editing out of Unity.
  • Flexiable API for playing audio clips or groups of clips. The setup is more modular than before for easy debugging and fixing.
  • Editor to manage the library, assign groups of clips together and music track lists. Saves workarounds that were needed in the 2.x version of the asset.
  • No setup needed, just import and go. Before you needed to setup the inspector the use it.
  • Entirely static API, no scene references needed. As the static instance was the more common setup, this change makes it way easier to access the API without needing lots of scene references.
  • Music playing setup. Still a simple setup, but plans to make it better with further updates.
  • Inspector players for quick prototyping without needing to write any code. Like the old audio player class, but allowing for edits to be applied in the editor at will instead of needing a messy inspector with all options avalible.

So as of this post the asset is avalible from its Github repository and the Itch page. With the asset store coming early next year, partly for stabilty fixes, partly due to the review process which will likely take longer around this time of year as is. Links before:

Github: https://github.com/CarterGames/AudioManager

Itch: https://carter-games.itch.io/audio-manager

Save Manager 2.0.15

Along with the Audio Manager release, there is a small update to the Save Manager also coming out at the same time, more or less. This update will go live to all stores and mostly fixes some editor stuff that I worked out in Audio Manager 3.x this week as well as some issues with the save editor not refreshing to the latest data.

Links can be found here: https://carter.games/savemanager/

Thatโ€™s all for now xD

~ J,

Audio Manager Progress Update

Audio Manager Progress Update

,

Hi all. Been a little while so I thought Iโ€™d just update yโ€™all on where Audio Manager 3 is at and when to expect it at this point.


Is it nearly done? yes. The code is like 90-95% done. As normal the last 5% takes most of the time xD. I found a few bugs which took me a week or so to work out in the spare time I had recently. I think Iโ€™ve got them at this point. I also had to add in a new system for an inspector player for audio. Something I couldnโ€™t really leave out of the initial release. A little sneak peak above (right). Its more flexible than the 2.x solution with the option to use mode edit modules from the inspector and choose as few or as many as youโ€™d like to use.

Iโ€™ve got one more script to write for a music player inspector, but after that is done it is just a matter of sorting a few small demo projects, testing & finishing the documentation. The docs I have already started writing. Iโ€™m hoping itโ€™ll be ready before 2024, but Iโ€™m annoyingly busy at the moment on the weekends where I can get the most done. So weโ€™ll see if that happensโ€ฆ.

Also new logo! All the assets have a new logo design as of this weekend. I’m slowly updating them everywhere as I get the time to do so and I’m not in a coding mood…. Audio Manager’s logo below:

Thatโ€™s all for now xD

~ J,