Using/Managing a proper "Selector" type (enum, string, type, ....)

I am running into a design issue when it comes to deciding which type to use when wanting to represent something.

Lets take Inputs as an example.
You make a Input manager that lets you type in a name for your input and what button that name corresponds to.
You are now able to select which key you want to check is being pressed by doing a InputDown(“String Name”) check.
I really dont like using strings as a means to choosing something. Strings can be very flexible, but easy to misuse (typos and what not). So then we get into maybe having a InputName class with static input names representing the input names we typed in the Input Manager. Now we dont have to worry about typos as long as we always use those static fields, but what if we want to change the name of the input? Now we need to remember to go in and change those static fields.
If we go with enums, then we get the neat things that come with enums such as help from intellisense. The bad thing is that the user would need to edit the enum file directly, unless if maybe we get into auto generating code?

More examples

Lets take another example, an Audio Manager. We need to be able to define different audio channels so we can handle their volumes separately. We pretty much have the same issue as our inputs, except there is a higher change that I will not need to reference it in code that much, but more so in the editor on the components via a dropdown or what ever.

Another example, a State Machine data passer thing. This might be poor design, but I have a component that I can call GetState(); It will return to me a State object that has information about what ever state assigned there that was of type T. So the internal dictionary is a Dictionary<Type, State>. The Type doesnt need to have anything to do with the actual state, its just what the state decided to use to define itself in the dictionary so that other objects can select it and get its data (The data of that state is stored in a plain object variable, so to get the actual data there would be a cast done). The reason for not using a enum with all the states is because the state can come from anywhere. For example, my Grounded state might come from my custom Rigidbody component, while my Walking state comes from something completely different, like a totally different asset that might have its own enums representing its states. However, what about strings? Would that have been a better choice?
Heres the code to give a better idea
Click for code

    public class StateWatcher : MonoBehaviour
    {
        Dictionary<Type, State> states = new Dictionary<Type, State>();

        public State GetState<T>()
        {
            if(states.ContainsKey(typeof(T))) return states[typeof(T)];

            Debug.LogWarning("StateWatcher couldnt find state of type " + typeof(T).ToString() + ". Returning Null");
            return null;
        }

        public bool AddState<T>(string stateName, object stateObject)
        {
            if(states.ContainsKey(typeof(T))) return false;
       
            State state = new State(stateName, stateObject);
            states.Add(typeof(T), state);
            return true;
        }

        [Serializable]
        public class State
        {
            public string name;
            public delegate void StateEvent();
            public event StateEvent OnStartState;
            public event StateEvent OnEndState;

            object stateObject;
       
            public State(string stateName, object stateObject)
            {
                name = stateName;
                this.stateObject = stateObject;
            }

            public void StateStarted()
            {
                OnStartState();
            }

            public void StateEnded()
            {
                OnEndState();
            }

            public void Subscribe(StateEvent onStart, StateEvent onEnd)
            {
                OnStartState += onStart;
                OnEndState += onEnd;
            }

            public void Unsubscribe(StateEvent onStart, StateEvent onEnd)
            {
                OnStartState -= onStart;
                OnEndState -= onEnd;
            }

            public void SetStateObject<T>(T stateObject)
            {
                this.stateObject = stateObject;
            }

            public T GetStateObject<T>()
            {
                if(stateObject != null)
                {
                    if(stateObject.GetType() != typeof(T))
                    {
                        Debug.LogWarning("StateWatcher State GetStateObject type does not match. Actual state type is " + stateObject.GetType() + " where the GetStateObject type was " + typeof(T));
                    }
                    return (T)stateObject;
                }
                return default(T);
            }
        }
    }

In terms of managing these things, how do you do it? If I have a public enum so that I can select it in the editor, what happens if I decide I want to remove an enum value? I had enum AudioLayer {Main = 0, PlayerSfx = 1, EnemySfx = 2} and now I decided I didnt want a PlayerSfx and MonsterSfx, but instead wanted to combine them into just Sfx. How do I change all my editor properties? All my code will throw compile errors when I remove the enums, so I can change them fine there, but my editor stuff will be hidden away until its found in runtime or something. This goes for strings too. If I have a custom editor to be able to have get all the AudioLayer Strings as a Dropdown, how do you change them all?

I would like to know how others handle this =).

Good question… this is really an architectural and design decision that can be tough and there’s no real right answer. You can, however, combine approaches. Let’s use input as an example with a not very helpful pseudocode-ish scenario. :slight_smile: So our problems are:

  1. Can we use an enum for our inputs?
  2. How do we allow custom inputs?
  3. How do we handle removing enum members?

Well, the answer to #1 is yes, and we can predefine values for those enums which handles #3 as well. So, let’s say you build an input system with some preset actions: Run, Jump, Shoot. I recommend incrementing values by 10 or 100 so it leaves space in between to add members.

public enum InputActions
{
    None = 0,
    Run = 10,
    Jump = 20,
    Shoot = 30
}

Now, you can use a Dictionary<int, KeyCode> to represent those as a singleton.

public static InputManager
{
   private static readonly Dictionary<int, KeyCode> _inputs;   

   static InputManager()
   {
        _inputs = new Dictionary<int, KeyCode>();
        _inputs.Add((int)InputActions.Run, KeyCode.A);
        _inputs.Add((int)InputActions.Jump, KeyCode.B);
        _inputs.Add((int)InputActions.Shoot, KeyCode.C);
   }
}

So this gives the flexibility of getting or setting the values for the built in defaults (InputActions), and it gives users the ability to add their own custom ones. A fuller example of the InputManager class might look something like this:

public static InputManager
{
   private static readonly Dictionary<int, KeyCode> _inputs;   

   static InputManager()
   {
        _inputs = new Dictionary<int, KeyCode>();
        _inputs.Add((int)InputActions.Run, KeyCode.A);
        _inputs.Add((int)InputActions.Jump, KeyCode.B);
        _inputs.Add((int)InputActions.Shoot, KeyCode.C);
   }

   public static KeyCode GetKeyCode(InputActions action)
   {
         return _inputs.ContainsKey((int)action) ? _inputs[(int)action] : null;
   }

   public static KeyCode GetKeyCode(int action)
   {
         return _inputs.ContainsKey(action) ? _inputs[action] : null;
   }

   public static void SetKeyCode(InputActions action, KeyCode newCode)
   {
         if(_inputs.ContainsKey((int)action))
         {
                _inputs[(int)action] = newCode;
         }
         else
         {
               _inputs.Add((int)action, newCode);
         }
   }

   public static void SetKeyCode(int action, KeyCode newCode)
   {
         if(action < 1000)
             throw new ArgumentException("Custom actions must be greater than or equal to 1000 - reserved for built in actions");    

         if(_inputs.ContainsKey(action))
         {
                _inputs[action] = newCode;
         }
         else
         {
               _inputs.Add(action, newCode);
         }
   }

}

Now the above is silly, but gives an example of how you could make a system flexible enough to allow customization. In fact, if someone wanted type safety, they could define their own enum and a wrapper class:

public enum CustomInputActions
{
      Crawl = 1000,
      Twerk = 1100
}

public static class CustomActions
{
         public static void SetCustomAction(CustomInputAction action, KeyCode code)
         {
                InputManager.SetKeyCode((int)action, code);
         }

        //rinse and repeat for other methods
}

So the above would allow the user to set custom codes in a compile time type safe manner while using the more flexible underlying system.

As for magic strings, I prefer to use global constants whenever possible. You still get type safety for members of a static class.

This really doesn’t answer your question I know… but it’s food for thought, just offering up a scenario.

In your example, you are providing a means of setting and getting input with an int as well as an InputAction. Do you see it as a better design choice to have InputAction locked in by us, the creator, or would it be good to do something like creating a Editor window where the user can assign their own input names and values, and then when they save it, we auto generate the InputAction enum file to contain all those keys, as well as keeping our default ones?

I was assuming this was something like a custom asset that was meant to be shared where you would have sensible defaults but also allow users to provide their own.

Edit: If it’s for internal use, then you should make your design decision based on how you want to handle it from project to project. Maybe the “InputManager” only takes an int… and you create a wrapper in each project that takes an enum… and you define the members of the enum per project and just use them to set the int value in the input manager.

While I am asking mainly for my own use and I will of course design things in a way that I want them to be, the problem is I dont really know how I want them to be. Whether its for internal use or to be created as an asset for others to use, the point is to design it in a way that is encapsulated and reusable (which would (or should) benefit me for internal use as well).
If the result is some major complicated mess, then yea I’ll probably pass, but I would still like to know thats the way it would be, with perhaps some examples.

This is the second time I have moved away from using strings to something more strongly typed like enums or plain Type, but then concerns raised.
In terms of my Inputs, I feel using enums is the way I would prefer, and just edit the enum file for different projects.
I was going to use strings for my audio manager, but then I told myself “Hey, you are already going to edit your enum file for your inputs, why not just do the same for your audio channels?”
Which is when I started to wonder if me constantly doing this could have negative side effects, while there might be an easy alternative.

The state machine data passing example might be where things get tricky. I could do things as I am now by using types and then getting the whole object, but it would be nicer if instead of getting objects filled with data, to maybe do something like GetStateData(“GroundNormal”) which would return the ground normal. This way I dont need to care about anything other than what I actually want, but this is now using strings. Do I just create a enum file that holds all random names for infos? That seems like it might get messy, but handling the strings seems messy too.
Or do people just fk it and create a custom made component with all the info you know you will need properly displayed and call it done? But now things are relying on that instead of a generic component. Your encapsulated skill system now relies on that component that has nothing useful to your skill system other than a few bools or whatever.

Also, a big concern for my audio manager, as well as any enum I select in the inspector, is what happens when I delete an enum? How do I update all my editor stuff? Id assume big projects have a way of handling this, otherwise it would be a nightmare.

It’s really hard to know without truly understanding the complete scenario. There’s no magic bullet, you just do what you feel makes the code most maintainable. Magic strings are bad when they’re scattered throughout the code, however if you define them as global fields on a static class, then it’s easy to know when one is removed. The same goes for enums.

Storage-wise, enums are efficient because they’re just stored as an int internally (unless you’re using flags, in which case it’s a byte). Also, using global constants or enums makes your code testable.

This is only true for when you reference them by code. How do you handle editor stuff? Typing a string in the editor (or better, selecting it via a popup) or selecting a enum.

While you typed that post, I edited my previous post talking about my state machine example. Perhaps that can be a better scenario to work with.

That’s very true, I didn’t even think about that scenario.

State machines are interesting… for a basic state machine I would definitely use enums. For something more complex, you might need to go as far as using a different data structure that represents input / output and state but then you get into have to really think about the design again.