Actions

Players & designers care about actions (Mario jumps, Samus shoots, Captain Falcon turns, brakes, and accelerates), whereas computers care about inputs (The W key is PRESSED, the left mouse button was JUST_RELEASED, gamepad #2's analog stick is MOVED with values x=0.4, y=-0.5).

The FlxAction API provides several benefits over handling every input case directly:

  • Declutter logic by focusing on actions, not inputs
  • Remove tight coupling between specific input bindings and actions
  • Makes it easy to refactor your input code
  • Makes it easy to support player-defined input bindings
  • Makes it easy to handle multiple input devices simultaneously
  • Allows use of the Steam Input API
  • Makes it easier to implement:
    • AI-controlled players/bots
    • Replay systems (record actions, not inputs, then play them back)

FlxAction

FlxActions come in two varieties: digital and analog. Digital is for on/off actions like "JUMP" and "SHOOT". Analog is for things that need a range of values, like jumping with more or less force, or moving a player or camera around. (FlxActions are triggered by attached FlxActionInputs, described in the next section).

Digital actions let you do things like: if(SHOOT.check()) doShoot(); in your update loop rather than have a big ugly block that accounts for every input device you support.

Analog actions let you do things like moveCharacter(MOVE.x, MOVE.y) or fireLaserBeam(LASER_INTENSITY.x), nicely hiding whatever devices are actually driving the input, and just providing you with the resulting floating point values.

To create a FlxAction:

var jump = new FlxActionDigital();
var move = new FlxActionAnalog();
HAXE

You can optionally pass in a name and a callback in the constructor. If you provide a callback, it will fire whenever the action is triggered.

If you are managing FlxActions yourself (as opposed to using the action manager), you will need to update the actions every frame, either by doing something like this:

function updateLoop()
{
    if (jump.check()) doJump();
    if (move.check()) doMove(move.x, move.y);
}
HAXE

...or this:

function updateLoop()
{
    jump.update();
    move.update();
    if (jump.triggered) doJump();
    if (move.triggered) doMove(move.x, move.y);
}
HAXE

FlxAction.update() will update the triggered property, fire the callback if present, and update the x and y values (if an analog action).

FlxAction.check() will update the triggered property, fire the callback if present, update the x and y values (if an analog action), and return the value of triggered.

Either is sufficient to keep the action updated. You must update each action at least once per global update tick to ensure accurate input, but if you accidentally update an action more than once per tick, it's okay -- an internal safety check ensures that nothing bad happens.

If you don't want to manually update FlxActions, use the FlxActionManager, which will keep them updated for you.

FlxActionInput

A FlxActionInput represents a specific input event on a specific input device, like "when the A button on gamepad #1 is JUST_PRESSED", which you can use to trigger a FlxAction.

These come in digital and analog forms for every device that Flixel supports. The following are provided by default, but you can create your own by extending FlxActionInputDigital and FlxActionInputAnalog:

  • FlxActionInputDigitalKeyboard
  • FlxActionInputDigitalMouse
  • FlxActionInputDigitalMouseWheel
  • FlxActionInputDigitalGamepad
  • FlxActionInputDigitalIFlxInput
  • FlxActionInputAnalogMouse
  • FlxActionInputAnalogMouseMotion
  • FlxActionInputAnalogMousePosition
  • FlxActionInputAnalogClickAndDragMouseMotion
  • FlxActionInputAnalogGamepad

You can attach inputs like this:

jump.addKey(SPACE, JUST_PRESSED);
jump.addMouse(LEFT, JUST_PRESSED);
jump.addGamepad(A, JUST_PRESSED, FIRST_ACTIVE);
HAXE

These helper functions are a shorthand for this equivalent:

jump.add(new FlxActionInputDigitalKeyboard(SPACE, JUST_PRESSED));
jump.add(new FlxActionInputDigitalMouse(LEFT, JUST_PRESSED));
jump.add(new FlxActionInputDigitalGamepad(A, JUST_PRESSED, FIRST_ACTIVE);
HAXE

This will cause the "jump" action to trigger on the frame where any of the following conditions is met: space bar was just pressed, left mouse button was just clicked, or the bottom face button on any gamepad was just pressed.

Each FlxActionInput class has its own parameters that let you define exactly when the action should fire. For instance, here is the constructor for FlxActionInputDigitalKeyboard:

public function new(Key:FlxKey, Trigger:FlxInputState)
HAXE

This requires you to specify a specific key, as well as an input state (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED).

Now here's the constructor for FlxActionInputDigitalGamepad, note that in addition to specifying the button and the trigger state, we also have to specify which gamepad we're listening for, since there could be more than one:

public function new(InputID:FlxGamepadInputID, Trigger:FlxInputState, GamepadID:Int = FlxInputDeviceID.FIRST_ACTIVE)
HAXE

Now let's take a quick look at some analog inputs.

Here's FlxActionInputAnalogGamepad:

public function new(InputID:FlxGamepadInputID, Trigger:FlxAnalogState, Axis:FlxAnalogAxis = EITHER, GamepadID:Int = FlxInputDeviceID.FIRST_ACTIVE)
HAXE

In addition to having to specify the input (left/right stick or left/right trigger), the trigger state, and the gamepad ID, we also have to specify which analog axis we care about (X, Y, EITHER, BOTH). (Note that for single-axis analog inputs, such as analog triggers, only the x value will change and the y value will always be zero).

Another example is FlxActionInputAnalogMousePosition:

public function new(Trigger:FlxAnalogState, Axis:FlxAnalogAxis = EITHER)
HAXE

Since there's only ever one mouse, we don't need to specify the device, and we don't need to specify a button since we just want the position. Whenever this action updates the x and y values will match the mouse position.

FlxActionSet

A FlxActionSet is little more than a glorified array of FlxActions. There's little reason to use them directly unless you are using the FlxActionManager, but they can still be a convenient way to call update() on all your actions at once.

FlxActionManager

FlxActionManager lets you manage multiple actions without having to update them manually, and also lets you control action sets. Action sets are groups of actions that can be selectively activated for specific input devices at specific times, which is great for local multiplayer games, games with complex input, and games using the Steam Input API.

FlxActionManager is not initialized in Flixel by default, you have to add it yourself in your initialization code:

var actionManager = new FlxActionManager();
FlxG.inputs.add(actionManager);
HAXE

Once the action manager has been set up, you can simply add actions to it, and it will ensure that all your actions are kept up to date:

//add actions one by one:
actionManager.addAction(jump);
actionManager.addAction(shoot);

//add several actions at once:
actionManager.addActions([action1, action2, action3, action4, action5]);
HAXE

Then in your update loop you can simply check the triggered property, or just wait for callbacks to fire, if you've set any.

function updateLoop()
{
    if (jump.triggered) doJump();
    if (shoot.triggered) doShoot();
}
HAXE

Default action set

What's actually happening when you call addAction or addActions is that the manager is asking for both actions and the action set you want to add them to:

public function addAction(Action:FlxAction, ActionSet:Int = 0):Bool
HAXE

If you don't provide an action set, it assumes you want to add them to the first one (index 0). And if you haven't defined any action sets, it will create one for you at index 0, name it "default", and immediately activate it for all devices.

NOTE: By default, when you change FlxStates, the default action set, including all referenced actions and inputs, will be cleared out and destroyed. You can change this behavior by modifying the FlxActionManager.resetOnStateSwitch policy.

If you're not juggling multiple action sets, you will probably never need to worry about any of this -- just add new actions at the start of every state. We like to explain what's going on under the hood just in case you encounter unexpected behavior.

Working with action sets

Only ONE action set is considered active at a given time for any given device, but multiple devices can be subscribed to the same action set.

For instance, in an asymetrical co-op game where one person drives a tank and the other mans the turret, gamepad #1 could use action set "drive" and gamepad #2 could use action set "gunner". The same could go for the mouse, the keyboard, etc. And in a single-player game you might want to just change the action set of ALL input devices every time you switch to a different screen, such as a menu.

FlxActionManager lets you:

  • ADD action sets
  • REMOVE action sets
  • ACTIVATE an action set for a specific device
  • UPDATE all your action sets at once
  • ENFORCE the "only one action set active per device at a time" rule

To create and add an action set, do something like:

//where up, down, left, right, select are digital actions
var set = new FlxActionSet("menu", [up, down, left, right, select]);
var menuSetIndex = actionManager.addSet(set);
HAXE

Note that addSet returns the action set's index. All operations on action sets require the action set's index (an Int), not its name (a String). This is for performance reasons. If you forget to store the action set, or otherwise lose track of an action set's index, you can query it at any time by passing the set's name to getSetIndex(). Just be sure not to do this repeatedly in frequently called loops.

Steam Input

If you are using the steamwrap library, FlxActionManager gains the ability to automatically create action sets from a steamwrap object derived from the master vdf game actions file that Steam makes you set up. You must then ACTIVATE one of those action sets for any connected steam controllers, which will automatically attach the proper steam action inputs to the actions in the set. You can also add as many regular FlxActionInputs as you like to any actions in the sets.

var config = steamwrap.data.ControllerConfig.fromVDF(myVDF);
actionManager.initSteam(config, digitalCallback, analogCallback);
HAXE

NOTE: If you are using the Steam Input API and/or a Steam Controller, you MUST use FlxActionManager in order to properly process Steam's API via FlxActions. The only other alternative is to call the steamwrap functions directly.

JSON parsing

FlxActionManager can generate a JSON string representation of your action sets via exportToJson() You can also initialize your action sets by calling initFromJson(), feeding in the parsed object representation of the same format.

The format is:

typedef ActionSetJsonArray = 
{
    @:optional var actionSets:Array<ActionSetJSON>;
}

typedef ActionSetJson =
{
    @:optional var name:String;
    @:optional var analogActions:Array<String>;
    @:optional var digitalActions:Array<String>;
}
HAXE

Which would look something like this in practice:

{
    "actionSets" : [
        {
            "name" : "SomeSet",
            "analogActions" : [
                "some_analog_action_1",
                "some_analog_action_2"
            ],
            "digitalActions" : [
                "some_digital_action_1",
                "some_digital_action_2"
            ]
        },
        {
            "name" : "AnotherSet",
            "analogActions" : [
                "another_analog_action_1",
                "another_analog_action_2"
            ],
            "digitalActions" : [
                "another_digital_action_1",
                "another_digital_action_2"
            ]
        }
    ]
}
JSON