Looking for Best Practice: Inspector Manager Classes for assigning Assets

Dear Community,

In my current game, I’d like to keep flexibility during development. Hence, I have a couple manager classes where I can assign assets and variables so I can always quickly change them.

E.G. I have a couple GameObject in my Hirarchy which are called

ParticleManager - AccessToParticleEffect
DelayManager - Time Delays in Game
AudioManager - Audio Assets

Those GameObjects have respective scripts assigned which allow playing these effects or accessing these delays via a function. These arrays can then be maintained in the inspector

2897304--213183--ParticleManager.png

For accessing, I created an enum inside the script assigning the proper array position.

2897304--213185--ParticleEnum.png

So I can easily access e.g. the via playParticle(particleEffects.destroyStick).

What makes this a bit inefficient is that I cannot see the enum values in the Inspector, I always have to chekc in the coding. So I would really like to see already in the inspector what the array positions stand for. Best would be to allow also a tooltip.

How are you guys dealing with this - is there another, more efficient way?

Thanks!

For the audio in Hidden Folks, I basically have three components for a similar resource structure as you have:

TriggerSound is the trigger that will play a sound. It has a ‘Sound’ variable (that is serialized in the inspector) with a string for the name and a boolean to play any sound named the same way but with a different number behind it (eg. a TriggerSound with name = ‘Wind’ and playAny=true will play Wind01 and Wind02 and alike).

SoundResource is a component unique in every scene that scans all the TriggerSounds in the scene with the press of a button (in editor, not in runtime), connects all the names of the sounds with actual audio clips, and adds those sounds to a audioclip array variable on that SoundResource.

SoundManager is a singleton that scans newly loaded scenes for SoundResources and adds the audioclips on those resources to itself, to be the link between the sound names and the audioclips at runtime. When a TriggerSound is triggered in runtime, it requests the SoundManager to instantiate an AudioSource at the trigger with the right audioclip.

In this system, I just enter the names of the sounds that I know are in my Audio folder, and I never have to connect anything with anything else - the system does that all for me :slight_smile:

@deraggi , you’ll want a custom editor that uses the enum members to index the array. That’ll allow you to see the names. As an added bonus, adding new values to the enum can auto-enlarge the array:

CustomEditor(typeof(VC_ParticleManager))
public class VC_ParticleManagerEditor : Editor {

    private VC_ParticleManager script;
    private string[] enumNames;

    private void OnEnable() {
        script = (VC_ParticleManager) script;
        enumNames = System.Enum.GetNames(typeof(TestEnum));

        //This happens on the first GUI frame when you add the component to a GO
        if(script.particleObjects == null) {
            script.particleObjects = new GameObject[enumNames.Length];
            SetDirty();
        }
        //Handle adding new enums.
        else if(script.particleObjects.Length != enumNames.Length) {
             var newParticles = new GameObject[enumNames.Length];
             Array.Copy(script.partcileObjects, newParticles, Mathf.Min(script.particleObjects.Length, enumNames.Length));
             script.partcileObjects = newParticles;
             SetDirty();
         }
    }

    private void OnInspectorGUI() {
        base.OnInspectorGUI();

         //particleObjects should be HideInInspector. Undo also automatically sets dirty.
         Undo.RecordObject(script);

         EditorGUILayout.LabelField("Particle prefabs:");
         EditorGUI.indendLevel++;

         for(int i = 0; i < enumNames.Length; i++) {
               //You can use typeof(ParticleSystem) here if you only want to be able to assign prefabs containing a particle system on the root object
              script.particleObjects[i] = EdiorGUILayout.ObjectField(enumNames[i], script.particleObjects[i], typeof(GameObject), false);
         }

         EditorGUI.indendLevel--;
    }

    //EditorUtility.SetDirty has been broken by design since multiscene editing was introduced, see here.
    private void SetDirty() {
        EditorUtility.SetDirty(script);
        EditorSceneManager.MarkSceneDirty(script.gameObject.scene);
    }
}

If you start removing enum values, this won’t work, as the number of enums will be less than the max enum. Handling that problem is an excercise left to the reader.

I also wrote this without testing it, so there’s probably spelling misstakes and compilation errors and all that jazz. The design is solid, though, I’ve written quite a few of these.

Is this manager used in all scenes, by the way? If that’s the case, it should probably be a ScriptableObject asset that you load once and then re-use. It’ll make it easier to create new scenes, less manager prefabs to drag in.

1 Like

Dude, this is freaking fantastic.

Not only did you save me hours of work, but you made my project alot more maintainable with your ScriptableObject advice. Also I learned a lot about Editor Scripting

Thank you a million times over.

Here’s the coding which worked for me:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.SceneManagement;

[CustomEditor(typeof(VC_ParticleManager))]
public class VC_ParticleManagerEditor : Editor
{
    private VC_ParticleManager script;
    private string[] enumNames;

    private void OnEnable()
    {
        script = (VC_ParticleManager)target;
        enumNames = System.Enum.GetNames(typeof(VC_ParticleManager.particleEffects));

        //This happens on the first GUI frame when you add the component to a GO
        if (script.particleObjects == null)
        {
            script.particleObjects = new GameObject[enumNames.Length];
            SetDirty();
        }
        //Handle adding new enums.
        else if (script.particleObjects.Length != enumNames.Length)
        {
            var newParticles = new GameObject[enumNames.Length];
            System.Array.Copy(script.particleObjects, newParticles, Mathf.Min(script.particleObjects.Length, enumNames.Length));
            script.particleObjects = newParticles;
            SetDirty();
        }
    }

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        //particleObjects should be HideInInspector. Undo also automatically sets dirty.
        Undo.RecordObject(script, "script");

        EditorGUILayout.LabelField("Particle prefabs:");
        EditorGUI.indentLevel++;

        for (int i = 0; i < enumNames.Length; i++)
        {
            //You can use typeof(ParticleSystem) here if you only want to be able to assign prefabs containing a particle system on the root object
            script.particleObjects[i] = EditorGUILayout.ObjectField(label: enumNames[i], obj: (GameObject)script.particleObjects[i], objType: typeof(VC_ParticleManager), allowSceneObjects: false) as GameObject;
        }  

        EditorGUI.indentLevel--;
    }

    //EditorUtility.SetDirty has been broken by design since multiscene editing was introduced, see here.
    private void SetDirty()
    {
        EditorUtility.SetDirty(script);
        EditorSceneManager.MarkSceneDirty(script.gameObject.scene);
    }
}
1 Like