InControl: Binding Actions To Controls

As of version 1.5.0, InControl has a new bindings API which supports binding controls to actions, allows rebinding controls at runtime and includes the ability to listen for new bindings on demand. Bindings can include mouse input, keyboard input and any of the standard or non-standard controls for supported devices.

Unknown devices cannot be bound since their input mappings are unknown and uncalibrated. A solution for this problem is coming soon.

For an example, see the “Bindings” example at InControl/Examples/Bindings.

To understand this part of the API, it helps to mentally separate actions in your game (such as “Move Left” or “Jump”) from specific controls (such as “DPad Left” or the “A” face button).

A set of actions are represented by a subclass of the PlayerActionSet class. An action set could be the actions for a single player, or something less specific like your user interface controls. Similarly, each player in a multiplayer game would likely have its own action set. You may have any number of action sets. They merely serve as a logical grouping of actions. Action sets can be easily saved and loaded along with their actions and bindings.

Actions are represented by instances of the PlayerAction class. Actions can be bound to one or more controls represented by instances of one of the subclasses of BindingSource, although typically you would rarely, if ever, deal directly with these subclasses.

Let's work through a simple example as an illustration. Let's say you have a simple platformer-style game. The basic actions your character has is “Move Left”, “Move Right” and “Jump”.

First we need to create a subclass of PlayerActionSet called MyCharacterActions that defines these actions:

public class MyCharacterActions : PlayerActionSet
{
    public PlayerAction Left;
    public PlayerAction Right;
    public PlayerAction Jump;
}

Next, in the constructor for our new class, we need to create the instances for these actions. Note, that it is necessary due to internal initialization that this be done in the constructor rather than in the member definitions themselves.

public class MyCharacterActions : PlayerActionSet
{
    public PlayerAction Left;
    public PlayerAction Right;
    public PlayerAction Jump;

    public MyCharacterActions()
    {
        Left = CreatePlayerAction( "Move Left" );
        Right = CreatePlayerAction( "Move Right" );
        Jump = CreatePlayerAction( "Jump" );
    }
}

Finally, because in our game it might be more convenient to add a single value to our sprite's horizontal position, so in addition to the Left and Right actions, we'll create a special filter action that combines the two into a single axis we can query easily:

public class MyCharacterActions : PlayerActionSet
{
    public PlayerAction Left;
    public PlayerAction Right;
    public PlayerAction Jump;
    public PlayerOneAxisAction Move;

    public MyCharacterActions()
    {
        Left = CreatePlayerAction( "Move Left" );
        Right = CreatePlayerAction( "Move Right" );
        Jump = CreatePlayerAction( "Jump" );
        Move = CreateOneAxisPlayerAction( Left, Right );
    }
}

This special PlayerOneAxisAction will not itself be bound to controls, but its two child actions Left and Right will be. There is also a PlayerTwoAxisAction which is useful for 2D vector directional controls.

Next, we need to create an instance of our custom action set. We could have more than one instance, for example, in a multiplayer game each player could have an instance of this class. For the purposes of our example, we only need one. We'll add it to our character controller class.

public class CharacterController : MonoBehaviour
{
    MyCharacterActions characterActions;

    void Start()
    {
        characterActions = new MyCharacterActions();
    }
}

At this point, we have the object representing our actions, but they have no controls bound. Let's bind a few default controls. We'll add gamepad controls and keyboard controls for each action:

public class CharacterController : MonoBehaviour
{
    MyCharacterActions characterActions;

    void Start()
    {
        characterActions = new MyCharacterActions();

        characterActions.Left.AddDefaultBinding( Key.LeftArrow );
        characterActions.Left.AddDefaultBinding( InputControlType.DPadLeft );

        characterActions.Right.AddDefaultBinding( Key.RightArrow );
        characterActions.Right.AddDefaultBinding( InputControlType.DPadRight );

        characterActions.Jump.AddDefaultBinding( Key.Space );
        characterActions.Jump.AddDefaultBinding( InputControlType.Action1 );
    }
}

Great! We'll see how we can rebind those controls at runtime later, but for now let's just put them to use. Actions behave almost exactly like instances of InputControl, so we can query them the same way.

public class CharacterController : MonoBehaviour
{
    MyCharacterActions characterActions;

    void Start()
    {
        // ...
    }

    void Update()
    {
        if (characterActions.Jump.WasPressed)
        {
            PerformJump();
        }

        // We use the aggregate filter action here.
        // It combines Left and Right into a single axis value
        // in the range -1 to +1.
        PerformMove( characterActions.Move.Value );
    }

    void PerformJump()
    {
        // ...
    }

    void PerformMove( float x )
    {
        // ...
    }
}

And that's it! We've defined actions for our player character and bound each action to multiple controls.

Note: It is important to call Destroy() on your action set if you no longer need it, otherwise it will add additional updating overhead. This can become a serious slowdown problem if you create a lot of them over the lifecycle of your application and never properly dispose of them.

By default, the device binding source will read from InputManager.ActiveDevice, the device which last provided input, which is perfect for single player games. This can be overriden by explicitly setting the Device property on the action set. The default value of null means use the current active device. Keyboard and mouse binding sources will naturally ignore this device for the action set. The device is merely an optional context, however, an action set cannot be assigned more than one gamepad device.

Next, we'll look at more advanced topics like rebinding at runtime, iterating through all the actions and bindings for user interface purposes, as well as saving and loading the complete state of an action set.