InControl: Rebinding At Runtime

Let's take the example from the previous section and look at how we can rebind controls at runtime. As a refresher, here is the basic outline of our action set.

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

    // ...
}

Rebinding usually involves a certain amount of user interface code which we'll not get into here, but InControl provides all the plumbing you might need.

For example, you might have a list of the bindable player actions in your settings menu. You can easily iterate over all the actions in an action set. The PlayerActionSet.Actions property provides a readonly collection of actions you can iterate over:

foreach (var action in characterActions.Actions)
{
    // ...
}

Additionally, we can iterate over all the current bindings in an action with the PlayerAction.Bindings property:

foreach (var action in characterActions.Actions)
{
    foreach (var binding in action.Bindings)
    {
        // ...
    }
}

While iterating over bindings, non-standard controls that are mapped on the current PlayerActionSet.Device will be skipped, since they are not relevant. They remain present, however, and will reappear if the device is switched to a controller which has that control.

Listening for an additional binding is as simple as calling:

action.ListenForBinding();

This puts the action set in listening mode. The next viable control press will be assigned as an additional binding (once released) and the listening will end. Only one action in an action set can be listening at any given time. If you start another action listening, it will replace the existing listening action in the set as the focus for listening.

You can specify exactly what is considered an acceptable binding by changing the PlayerActionSet.ListenOptions property for the entire set, or override it on an individual action by setting the PlayerAction.ListenOptions property to something other than null.

The BindingListenOptions object contains a number of configurable properties. Let's look at some of them:

public class BindingListenOptions
{
    /// Include controllers when listening for new bindings.
    public bool IncludeControllers = true;

    /// Include unknown controllers when listening for new bindings.
    public bool IncludeUnknownControllers = false;

    /// Include non-standard controls on controllers when listening for new bindings.
    public bool IncludeNonStandardControls = true;

    /// Include mouse buttons when listening for new bindings.
    public bool IncludeMouseButtons = false;

    /// Include keyboard keys when listening for new bindings.
    public bool IncludeKeys = true;

    /// Treat modifiers (Shift, Alt, Control, etc.) as first class keys instead of modifiers.
    public bool IncludeModifiersAsFirstClassKeys = false;

    /// The maximum number of bindings allowed for the action. 
    /// If a new binding is detected and would cause this number to be exceeded, 
    /// enough bindings are removed to make room before adding the new binding.
    /// When zero (default), no limit is applied.
    public uint MaxAllowedBindings = 0;

    /// The maximum number of bindings of a given type allowed for the action. 
    /// If a new binding is detected and would cause this number to be exceeded, 
    /// enough bindings are removed to make room before adding the new binding.
    /// When zero (default), no limit is applied.
    /// When nonzero, this setting overrides MaxAllowedBindings.
    public uint MaxAllowedBindingsPerType = 0;

    /// Allow bindings that are already bound to any other action in the set.
    public bool AllowDuplicateBindingsPerSet = false;

    /// If an existing duplicate binding exists, remove it before adding the new one.
    /// When <code>true</code>, the value of AllowDuplicateBindingsPerSet is irrelevant.
    public bool UnsetDuplicateBindingsOnSet = false;

    /// If not <code>null</code>, and this binding is on the listening action, this binding
    /// will be replace by the newly found binding.
    public BindingSource ReplaceBinding = null;

    /// This function is called when a binding is found but before it is added.
    /// If this function returns false, then the binding is ignored
    /// and listening for new bindings will continue.
    /// If set to null (default), it will not be called.
    public Func<PlayerAction, BindingSource, bool> OnBindingFound = null;

    /// This action is called after a binding is added.
    /// If set to null (default), it will not be called.
    public Action<PlayerAction, BindingSource> OnBindingAdded = null;

    /// This action is called after a binding is found, but rejected along with 
    /// the reason (BindingSourceRejectionType) why it was rejected.
    /// If set to null (default), it will not be called.
    public Action<PlayerAction, BindingSource, BindingSourceRejectionType> OnBindingRejected = null;
}

The two callbacks OnBindingFound and OnBindingAdded can be used to provide further fine tuning, or interrupt the listening process. For example, say you are listening for keyboard bindings, but you want to have the escape key specifically stop listening for bindings instead of get bound. You could hook into the OnBindingFound function:

characterActions.ListenOptions.OnBindingFound = ( action, binding ) =>
{
    // Binding sources are comparable, so we can do this.
    if (binding == new KeyBindingSource( Key.Escape ))
    {
        action.StopListeningForBinding();
        return false;
    }
    return true;
};

There are several more methods in the API to round out the required functionality for modifying bindings at runtime. Please see the following classes in the API reference: