Dev Blog: Wrangling Unity’s Input

It’s Brian again and it’s time for another update post of the development of Wulverblade. I promised more detail after the last post and I’ve decided to focus on an area of the game that I’ve been working on a lot recently: input.

Input systems are the kind that get overlooked quite often. They aren’t glamorous or sexy but a game with poor input will never feel polished. This is one area where putting this game out on PC actually makes things considerably more complicated. Being on PC means supporting a lot of different input devices at the same time and also supporting a much higher expectation of customization than other platforms. When a game is released on PC you have to consider that users will expect to be able to fiddle with game settings and control many different aspects of the experience. You will need to think about the specter of rebindable controls.

The plan with Wulverblade is to support various gamepads as well as mouse & keyboard. So the question becomes: how do we support this flexibility for the user and what does Unity provide to us to make this easier (or harder)?

The input system in Unity is centered around the Input Manager which is a system that can be configured inside the editor. From code you can access the system using the Input class which has a bunch of static functions and properties. This let’s you check if keys or buttons are pressed or if a joystick or gamepad axis is being pushed. When you would like to handle an axis like this (think of pushing a thumbstick to the right) you must define the axis ahead of time by giving it a name, specifying the axis number, the joystick number, and other settings like deadzone in the Input Manager.

Unity's Input Manager

By defining inputs like this we give ourselves a way to access inputs a very programmatic way versus more human readable bindings like “Left” or “Fire”

Let’s say you have set axis 5 for joystick 1 to be defined as “right.” What happens when a player plugs in a different controller which has the thumbstick axis not defined as 5? Suddenly the gamepad will not work correctly for this user. Since Unity’s Input Manager must be set up ahead of time it would appear that this kind of situation could happen easily and that there would be no real good solution to the problem. Even more complicated is that if we truly want configurable controls we need to allow “right” to be defined as a controller button, a controller axis, or a keyboard key code all of which are accessed and checked from the code using different functions (GetButton, GetAxis, GetKey respectively).

The first thing to do to start to create a more flexible system is to change the game from reacting to specific inputs to instead react to abstract “actions.” You define a set of actions that the game looks for which are the basic control concepts in the game. Here is how we define our actions currently:

 public enum ActionType {
         Left = 0,
         Right = 1,
         Up = 2,
         Down = 3,
         Jump = 4,
         Attack1 = 5,
         Attack2 = 6,
         Special = 7,
         Menu = 8,
         LastAction
     }

Explicitly assigning indices to the enumerations isn’t really necessary here as the compiler will do that for us but sometimes it is good to be explicit for our own sake. We can then map these actions to an input binding which can be either a mouse button, keyboard key, gamepad button, or gamepad axis. In our update loop we can then go through each possible action and check if the corresponding input binding is active or not and set our game action’s active state. For organization purposes we separate bindings for keyboard/mouse from gamepad. So a player will be put in gamepad mode or keyboard/mouse mode and the modes can be set by the user in the options menu.

Code for checking input actions

We chose to store input bindings in parallel arrays but it could easily be one array storing a combined InputBinding struct

The variable “currentInput” can be Keyboard, Gamepad1, or Gamepad2. You can see that when we check the gamepad axes we use our own dead zone checking. This is instead of using the Unity Input Manager built in dead zone checking. This is mostly a style choice as I prefer to do this sort of checking in my own code where I can control it and possibly do more sophisticated processing than is shown here.

We need to be able to store bindings for later. You can save out each binding fairly easily by saving the action and then it’s corresponding input binding setting. An important element to smoothly working input system is detecting when the user has set the bindings themselves or when default bindings should be used. The first time the game runs it will detect that no bindings had been saved and it will load defaults for keyboard/mouse and also for the gamepad.

Gamepad bindings on PC can be tricky because different devices will have different mappings. One gamepad can have its left analog stick affect Unity’s axis 0, while another might be bound to axis 1. We plan on supporting as many gamepads as possible with sensible defaults by creating text files which represent input bindings for popular gamepads. The input file has the name that the OS will tell us from the gamepad an then each action-input binding. These text files are loaded and then matched against the currently plugged in gamepad. In this way the most popular gamepads will be able to be used with no configuration by the user.

Temporary Input Binding Settings Screen

This is a very temporary input binding screen in the options menu. It lets the user rebind their controls for player 1 and player 2. This will eventually be replaced by a properly designed and drawn GUI but this menu is already completely functional.

If you have any questions or want more specific and in-depth info on how this system works you can just ask in the comments. Also, let me know if there is another topic you’d like me to cover for the next update. Otherwise I’ll probably just talk about whatever system I spent a good deal of time working on.

  • I’m a little confused on the code sample. I guess I don’t understand Unity and how that method is suppose to work.

    Could you give any more clarification on what the intent is? Why does it return a bool?

    • Brian Johnstone

      So the function basically just returns true if the action is active. So if I were to called “GetAction(ActionType.Jump)” and it returns true that means the user has the “jump” input (which because of this system can be configured to be all sorts of different inputs) active. This is the heart of the system. Outside this class I don’t refer directly to inputs anymore, I only ask the input system if “actions” are active.

  • David

    I was planning on taking the same approach. But is there any programmatic way to set up all those Input Manager settings? It would be awful to have to manually type in the virtual axes for every possible axis and every possible button on every possible joystick that could be plugged in!

    • Marrt

      yes, http://plyoung.appspot.com/blog/manipulating-input-manager-in-script.html

      this is my version, it sets up all 20 controllers + axes
      using UnityEngine;
      using UnityEditor;
      using System.Collections;

      //source
      //http://plyoung.appspot.com/blog/manipulating-input-manager-in-script.html

      //added ui

      public class AxisGeneratorForInputManager : EditorWindow {
      // generates axis within InputManager

      [MenuItem( “Edit/GenerateInputManagerEntries” )]

      static void Init(){
      AxisGeneratorForInputManager window = (AxisGeneratorForInputManager)EditorWindow.GetWindow( typeof( AxisGeneratorForInputManager ) );
      window.maxSize = new Vector2( 200, 125 );
      }

      public void OnGUI(){
      if(GUI.Button(new Rect(0,30, 250, 20),”Clear All Entries”)){
      ClearAllInputs();
      }

      if(GUI.Button(new Rect(0,60, 250, 20),”Generate All Joystick Axis”)){

      string[] axisNames = new string[]{“X”,”Y”,”3″,”4″,”5″,”6″,”7″,”8″,”9″,”10″,”11″,”12″,”13″,”14″,”15″,”16″,”17″,”18″,”19″,”20″,”21″,”22″,”23″,”24″,”25″,”26″,”27″,”28″};
      int joyStickCount = 11; //start with 1

      for(int i=1; i<joyStickCount+1; i++){
      for(int a=0; a<axisNames.Length; a++){

      InputAxis axis = new InputAxis();
      axis.name = "Joystick"+i+"Axis"+axisNames[a];
      // axis.descriptiveName;
      // axis.descriptiveNegativeName;
      // axis.negativeButton;
      // axis.positiveButton;
      // axis.altNegativeButton;
      // axis.altPositiveButton;

      // axis.gravity= 0F;
      // axis.dead= 0F; //don't ever use that http://forum.unity3d.com/threads/axis-input-dead-problematic-for-diagonal-input.399269/
      axis.sensitivity = 1F;

      // axis.snap = false;
      // axis.invert = false;

      axis.type = AxisType.JoystickAxis;

      axis.axis = a+1; //int
      axis.joyNum = i; //int

      AddAxis(axis);
      }
      }
      }
      }

      private void ClearAllInputs(){
      SerializedObject serializedObject = new SerializedObject(AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/InputManager.asset")[0]);
      SerializedProperty axesProperty = serializedObject.FindProperty("m_Axes");
      axesProperty.ClearArray();
      serializedObject.ApplyModifiedProperties();
      }

      private static SerializedProperty GetChildProperty(SerializedProperty parent, string name)
      {
      SerializedProperty child = parent.Copy();
      child.Next(true);
      do
      {
      if (child.name == name) return child;
      }
      while (child.Next(false));
      return null;
      }

      private static bool AxisDefined(string axisName)
      {
      SerializedObject serializedObject = new SerializedObject(AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/InputManager.asset")[0]);
      SerializedProperty axesProperty = serializedObject.FindProperty("m_Axes");

      axesProperty.Next(true);
      axesProperty.Next(true);
      while (axesProperty.Next(false))
      {
      SerializedProperty axis = axesProperty.Copy();
      axis.Next(true);
      if (axis.stringValue == axisName) return true;
      }
      return false;
      }

      public enum AxisType
      {
      KeyOrMouseButton = 0,
      MouseMovement = 1,
      JoystickAxis = 2
      };

      public class InputAxis
      {
      public string name;
      public string descriptiveName;
      public string descriptiveNegativeName;
      public string negativeButton;
      public string positiveButton;
      public string altNegativeButton;
      public string altPositiveButton;

      public float gravity;
      public float dead;
      public float sensitivity;

      public bool snap = false;
      public bool invert = false;

      public AxisType type;

      public int axis;
      public int joyNum;
      }

      private static void AddAxis(InputAxis axis)
      {
      if (AxisDefined(axis.name)) return;

      SerializedObject serializedObject = new SerializedObject(AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/InputManager.asset")[0]);
      SerializedProperty axesProperty = serializedObject.FindProperty("m_Axes");

      axesProperty.arraySize++;
      serializedObject.ApplyModifiedProperties();

      SerializedProperty axisProperty = axesProperty.GetArrayElementAtIndex(axesProperty.arraySize – 1);

      GetChildProperty(axisProperty, "m_Name").stringValue = axis.name;
      GetChildProperty(axisProperty, "descriptiveName").stringValue = axis.descriptiveName;
      GetChildProperty(axisProperty, "descriptiveNegativeName").stringValue = axis.descriptiveNegativeName;
      GetChildProperty(axisProperty, "negativeButton").stringValue = axis.negativeButton;
      GetChildProperty(axisProperty, "positiveButton").stringValue = axis.positiveButton;
      GetChildProperty(axisProperty, "altNegativeButton").stringValue = axis.altNegativeButton;
      GetChildProperty(axisProperty, "altPositiveButton").stringValue = axis.altPositiveButton;
      GetChildProperty(axisProperty, "gravity").floatValue = axis.gravity;
      GetChildProperty(axisProperty, "dead").floatValue = axis.dead;
      GetChildProperty(axisProperty, "sensitivity").floatValue = axis.sensitivity;
      GetChildProperty(axisProperty, "snap").boolValue = axis.snap;
      GetChildProperty(axisProperty, "invert").boolValue = axis.invert;
      GetChildProperty(axisProperty, "type").intValue = (int)axis.type;
      GetChildProperty(axisProperty, "axis").intValue = axis.axis – 1;
      GetChildProperty(axisProperty, "joyNum").intValue = axis.joyNum;

      serializedObject.ApplyModifiedProperties();
      }

      //example
      /*
      public static void SetupInputManager(){
      // Add mouse definitions
      AddAxis(new InputAxis() { name = "myMouseX", sensitivity = 1f, type = AxisType.MouseMovement, axis = 1 });
      AddAxis(new InputAxis() { name = "myMouseY", sensitivity = 1f, type = AxisType.MouseMovement, axis = 2 });
      AddAxis(new InputAxis() { name = "myScrollWheel", sensitivity = 1f, type = AxisType.MouseMovement, axis = 3 });

      // Add gamepad definitions
      int i = 1;
      //for (int i = 1; i <= (int)InputBind.Gamepad.Gamepad4; i++)
      //{
      for (int j = 0; j <= (int)InputBind.GamepadAxis.Axis10; j++)
      {
      AddAxis(new InputAxis()
      {
      name = "myPad" + i + "A" + (j + 1).ToString(),
      dead = 0.2f,
      sensitivity = 1f,
      type = AxisType.JoystickAxis,
      axis = (j + 1),
      joyNum = i,
      });
      }
      //}
      }
      */
      }