-
Couldn't load subscription status.
- Fork 328
Description
Background and Motivation
This API proposal adds a simplification layer around Input Actions and introduces the concept of Global Actions, an input actions asset that comes pre-built with the package and sits in the InputManager asset in Project Settings (bit of an implementation detail but that seems the best place to put it). Global actions are enabled by default and remove the need for the user to a) remember to call Enable and b) create a new Input Action Asset before they can even get started with input (along with the requirement to understand all the concepts around Input Actions that go with that).
The API favours the polling approach to input by default and funnels users towards that type of usage. While the original Input Actions API and all their events are always accessible, they are another layer down, and none of the types in this proposal directly expose any events.
It also tries to make a distinction between the concept of Input Actions as an abstraction over a set of controls, and the interaction model of started -> performed -> cancelled, because this often seems to be a source of confusion.
Finally, this API removes the need for users to know what action type an action has. In the existing API, users must call the ReadValue method to retrieve the value of the action, and if the wrong type is provided, a runtime exception is thrown. This is an easy way for uncaught errors to sneak into a product, as the type of an action can be changed in the asset at any time. Using source generators, this API proposes building strongly-typed wrappers around actions.
Proposed API
namespace UnityEngine.InputSystem
{
public static partial class Input
{
public static bool IsPressed(string actionName, string actionMapName = "");
public static bool WasPressedThisFrame(string actionName, string actionMapName = "");
public static bool WasReleasedThisFrame(string actionName, string actionMapName = "");
}
public class Input<TActionType> where TActionType : struct
{
public InputAction action { get; }
public bool isPressed { get; }
public bool wasPressedThisFrame { get; }
public bool wasReleasedThisFrame { get; }
public TActionType value { get; }
public Input(InputAction action);
public bool WasPerformedThisFrame<TInteraction>() where TInteraction:IInputInteraction;
public bool WasStartedThisFrame<TInteraction>() where TInteraction : IInputInteraction;
public bool WasEndedThisFrame<TInteraction>() where TInteraction : IInputInteraction;
public bool SetInteractionParameter<TInteraction, TParameter>(Expression<Func<TInteraction, TParameter>> expr, TParameter value);
public bool GetInteractionParameter<TInteraction, TParameter>(Expression<Func<TInteraction, TParameter>> expr, out TParameter value);
public bool AddInteraction(IInputInteraction interaction);
public TInteraction AddInteraction<TInteraction>() where TInteraction : IInputInteraction, new();
public bool RemoveInteraction<TInteraction>() where TInteraction : IInputInteraction;
public bool RemoveInteraction(IInputInteraction interaction);
// alternative API
public void AddInteraction(TInteractionConfig config) where T:IInputInteractionConfiguration;
public static implicit operator bool(Input<TActionType> input);
public static implicit operator InputAction(Input<TActionType> input);
}
public struct Interaction<TInteraction, TActionType> : IDisposable
where TInteraction:IInputInteraction
where TActionType:struct
{
public bool wasPerformedThisFrame { get; }
public bool wasStartedThisFrame { get; }
public bool wasCancelledThisFrame { get; }
public Interaction(Input<TActionType> input);
}
public partial class InputSettings : ScriptableObject
{
public InputActionAsset actions{ get; set; }
public bool disableHighLevelAPI { get; set; }
}
}API Usage
Increase a value while an action is pressed
public class ChargedFire : MonoBehaviour
{
private float m_StartTime = 0;
public void Update()
{
if (Input.IsActionPressed("Fire"))
{
m_StartTime = Time.time;
}
if (Input.IsActionUp("Fire"))
{
Fire(Time.time - m_StartTime);
m_StartTime = 0;
}
}
private void Fire(float chargeTime)
{
}
}Perform a gameplay action when a strongly-typed input is pressed
public class Jump : MonoBehaviour
{
public void Update()
{
if (InputActions.jump)
{
GetComponent<Rigidbody>().AddForce(Vector3.up, ForceMode.Impulse);
}
}
}Use source generated interactions
public class FireWeapon : MonoBehaviour
{
public void Update()
{
if (InputActions.fire.holdInteraction.wasPerformedThisFrame)
{
InputActions.fire.TryGetInteractionParameter((HoldInteraction x) => x.duration, out var duration);
Fire(duration);
}
if (InputActions.fire.pressInteraction.wasPerformedThisFrame)
Fire(0);
}
public void Fire(float chargeTime)
{
}
}Change the value of an existing interaction parameter
public class ChangeInteractionParameter : MonoBehaviour
{
public void Update()
{
InputActions.chargedFire.TryGetInteractionParameter((HoldInteraction x) => x.duration, out var duration);
InputActions.chargedFire.SetInteractionParameter((HoldInteraction x) => x.duration,
duration + 0.1f);
}
}Accessing lower level Input Action functionality(rebinding, callbacks etc)
// 'action' property acts as an escape hatch for accessing functionality that exists below the higher level API surface
InputActions.fps.fire.action.performed += ctx => {}
InputActions.fps.fire.action.Rebind(...);
// this would be the equivalent version from the old system
var exampleAsset = new ExampleAsset();
exampleAsset.fps.fire.performed += ctx => {}Source generated strongly-typed input action access example
public static class InputActions
{
public static Input<Vector2> move => new Input<Vector2>(globalAsset.FindAction("Gameplay/Move"));
public static FireInput fire => new FireInput(globalAsset.FindAction("Gameplay/Fire"));
public static Input<float> join => new Input<float>(globalAsset.FindAction("Player/Join"));
public static Input<Vector2> navigate => new Input<Vector2>(globalAsset.FindAction("UI/Navigate"));
public class FireInput : Input<Vector2>
{
public InputInteraction<Vector2, HoldInteraction> holdInteraction;
public InputInteraction<Vector2, PressInteraction> pressInteraction;
internal FireInput(InputAction action) : base(action)
{
holdInteraction = new InputInteraction<Vector2, HoldInteraction>(this);
pressInteraction = new InputInteraction<Vector2, PressInteraction>(this);
}
}
}Source generated XMLDOC showing bindings and interactions on an action
Notes
- Source generated actions can have the action map name prepended if there is a collision in naming.
- As part of this work, the binding logic should be improved so that errors are thrown during binding resoltuion if an incompatible control type gets bound to an action? For example, a button binding in an action of type Vector2. Right now, the errors only get thrown when ReadValue is called, so it would be easy for that error to sneak into a game.
- The global action asset setting in InputSettings will require some custom build pipeline work because all the source generated code has to work against it.
- InputInteraction implements IDisposable so that we can remove performed/started/cancelled event handlers. This should be called by the main Input class when the game shuts down or on exiting play mode.
Risks
- The AddInteraction implementation could be very difficult. Currently the system creates interaction instances dynamically via Activator.CreateInstance from configuration data stored in string format e.g. "Hold(duration=0.5);Press(behaviour=1)" etc. This API though would allow injection of a specific instance of an interaction into the binding state to allow interaction parameters to be changed by direct modification of the instance.
- The constructors for Input and InputInteraction classes need to be public because they will be instantiated from source generated code in a user assembly. It would therefore be possible to wrap an input action in a non-type-safe way. We can add debug asserts at runtime, but that's not ideal.