HaxeFlixel/flixel

View on GitHub
flixel/input/actions/FlxAction.hx

Summary

Maintainability
Test Coverage
package flixel.input.actions;

import flixel.input.FlxInput.FlxInputState;
import flixel.input.IFlxInput;
import flixel.input.actions.FlxActionInput.FlxInputDeviceID;
import flixel.input.actions.FlxActionInput.FlxInputType;
import flixel.input.actions.FlxActionInputAnalog.FlxAnalogAxis;
import flixel.input.actions.FlxActionInputAnalog.FlxAnalogState;
import flixel.input.actions.FlxActionInputAnalog.FlxActionInputAnalogClickAndDragMouseMotion;
import flixel.input.actions.FlxActionInputAnalog.FlxActionInputAnalogGamepad;
import flixel.input.actions.FlxActionInputAnalog.FlxActionInputAnalogMouseMotion;
import flixel.input.actions.FlxActionInputAnalog.FlxActionInputAnalogMousePosition;
import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalIFlxInput;
import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalGamepad;
import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalKeyboard;
import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalMouse;
import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalMouseWheel;
#if android
import flixel.input.actions.FlxActionInputDigital.FlxActionInputDigitalAndroid;
#end
import flixel.input.keyboard.FlxKey;
import flixel.input.mouse.FlxMouseButton.FlxMouseButtonID;
import flixel.input.android.FlxAndroidKey;
import flixel.input.gamepad.FlxGamepadInputID;
import flixel.util.FlxDestroyUtil;
import flixel.util.FlxDestroyUtil.IFlxDestroyable;
#if FLX_STEAMWRAP
import steamwrap.api.Controller.EControllerActionOrigin;
#end

using flixel.util.FlxArrayUtil;

/**
 * A digital action is a binary on/off event like "jump" or "fire".
 * FlxActions let you attach multiple inputs to a single in-game action,
 * so "jump" could be performed by a keyboard press, a mouse click,
 * or a gamepad button press.
 *
 * @since 4.6.0
 */
class FlxActionDigital extends FlxAction
{
    /**
     * Function to call when this action occurs
     */
    public var callback:FlxActionDigital->Void;

    /**
     * Create a new digital action
     * @param    Name    name of the action
     * @param    Callback    function to call when this action occurs
     */
    public function new(?Name:String = "", ?Callback:FlxActionDigital->Void)
    {
        super(FlxInputType.DIGITAL, Name);
        callback = Callback;
    }

    /**
     * Add a digital input (any kind) that will trigger this action
     * @param    input
     * @return    This action
     */
    public function add(input:FlxActionInputDigital):FlxActionDigital
    {
        addGenericInput(input);
        return this;
    }

    /**
     * Add a generic IFlxInput action input
     *
     * WARNING: IFlxInput objects are often member variables of some other
     * object that is often destructed at the end of a state. If you don't
     * destroy() this input (or the action you assign it to), the IFlxInput
     * reference will persist forever even after its parent object has been
     * destroyed!
     *
     * @param    Input    A generic IFlxInput object (ex: FlxButton.input)
     * @param    Trigger    Trigger What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED)
     * @return    This action
     */
    public function addInput(Input:IFlxInput, Trigger:FlxInputState):FlxActionDigital
    {
        return add(new FlxActionInputDigitalIFlxInput(Input, Trigger));
    }

    /**
     * Add a gamepad action input for digital (button-like) events
     * @param    InputID "universal" gamepad input ID (A, X, DPAD_LEFT, etc)
     * @param    Trigger What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED)
     * @param    GamepadID specific gamepad ID, or FlxInputDeviceID.ALL / FIRST_ACTIVE
     * @return    This action
     */
    public function addGamepad(InputID:FlxGamepadInputID, Trigger:FlxInputState, GamepadID:Int = FlxInputDeviceID.FIRST_ACTIVE):FlxActionDigital
    {
        return add(new FlxActionInputDigitalGamepad(InputID, Trigger, GamepadID));
    }

    /**
     * Add a keyboard action input
     * @param    Key Key identifier (FlxKey.SPACE, FlxKey.Z, etc)
     * @param    Trigger What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED)
     * @return    This action
     */
    public function addKey(Key:FlxKey, Trigger:FlxInputState):FlxActionDigital
    {
        return add(new FlxActionInputDigitalKeyboard(Key, Trigger));
    }

    /**
     * Mouse button action input
     * @param    ButtonID Button identifier (FlxMouseButtonID.LEFT / MIDDLE / RIGHT)
     * @param    Trigger What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED)
     * @return    This action
     */
    public function addMouse(ButtonID:FlxMouseButtonID, Trigger:FlxInputState):FlxActionDigital
    {
        return add(new FlxActionInputDigitalMouse(ButtonID, Trigger));
    }

    /**
     * Action for mouse wheel events
     * @param    Positive    True: respond to mouse wheel values > 0; False: respond to mouse wheel values < 0
     * @param    Trigger        What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED)
     * @return    This action
     */
    public function addMouseWheel(Positive:Bool, Trigger:FlxInputState):FlxActionDigital
    {
        return add(new FlxActionInputDigitalMouseWheel(Positive, Trigger));
    }

    #if android
    /**
     * Android buttons action inputs
     * @param    Key    Android button key, BACK, or MENU probably (might need to set FlxG.android.preventDefaultKeys to disable the default behaviour and allow proper use!)
     * @param    Trigger        What state triggers this action (PRESSED, JUST_PRESSED, RELEASED, JUST_RELEASED)
     * @return    This action
     * 
     * @since 4.10.0
     */
    public function addAndroidKey(Key:FlxAndroidKey, Trigger:FlxInputState):FlxActionDigital
    {
        return add(new FlxActionInputDigitalAndroid(Key, Trigger));
    }
    #end

    override public function destroy():Void
    {
        callback = null;
        super.destroy();
    }

    override public function check():Bool
    {
        var val = super.check();
        if (val && callback != null)
        {
            callback(this);
        }
        return val;
    }
}

/**
 * Analog actions are events with continuous (floating-point) values, and up
 * to two axes (x,y). This is for events like "move" and "accelerate" where the
 * event is not simply on or off.
 *
 * FlxActions let you attach multiple inputs to a single in-game action,
 * so "move" could be performed by a gamepad joystick, a mouse movement, etc.
 *
 * @since 4.6.0
 */
class FlxActionAnalog extends FlxAction
{
    /**
     * Function to call when this action occurs
     */
    public var callback:FlxActionAnalog->Void;

    /**
     * X axis value, or the value of a single-axis analog input.
     */
    public var x(get, never):Float;

    /**
     * Y axis value. (If action only has single-axis input this is always == 0)
     */
    public var y(get, never):Float;

    /**
     * Create a new analog action
     * @param    Name    name of the action
     * @param    Callback    function to call when this action occurs
     */
    public function new(?Name:String = "", ?Callback:FlxActionAnalog->Void)
    {
        super(FlxInputType.ANALOG, Name);
        callback = Callback;
    }

    /**
     * Add an analog input that will trigger this action
     */
    public function add(input:FlxActionInputAnalog):FlxActionAnalog
    {
        addGenericInput(input);
        return this;
    }

    /**
     * Add mouse input -- same as mouse motion, but requires a particular mouse button to be PRESSED
     * Very useful for e.g. panning a map or canvas around
     * @param    ButtonID    Button identifier (FlxMouseButtonID.LEFT / MIDDLE / RIGHT)
     * @param    Trigger    What state triggers this action (MOVED, JUST_MOVED, STOPPED, JUST_STOPPED)
     * @param    Axis    which axes to monitor for triggering: X, Y, EITHER, or BOTH
     * @param    PixelsPerUnit    How many pixels of movement = 1.0 in analog motion (lower: more sensitive, higher: less sensitive)
     * @param    DeadZone    Minimum analog value before motion will be reported
     * @param    InvertY    Invert the Y axis
     * @param    InvertX    Invert the X axis
     * @return    This action
     */
    public function addMouseClickAndDragMotion(ButtonID:FlxMouseButtonID, Trigger:FlxAnalogState, Axis:FlxAnalogAxis = FlxAnalogAxis.EITHER,
            PixelsPerUnit:Int = 10, DeadZone:Float = 0.1, InvertY:Bool = false, InvertX:Bool = false):FlxActionAnalog
    {
        return add(new FlxActionInputAnalogClickAndDragMouseMotion(ButtonID, Trigger, Axis, PixelsPerUnit, DeadZone, InvertY, InvertX));
    }

    /**
     * Add mouse input -- X/Y is the RELATIVE motion of the mouse since the last frame
     * @param    Trigger    What state triggers this action (MOVED, JUST_MOVED, STOPPED, JUST_STOPPED)
     * @param    Axis    which axes to monitor for triggering: X, Y, EITHER, or BOTH
     * @param    PixelsPerUnit    How many pixels of movement = 1.0 in analog motion (lower: more sensitive, higher: less sensitive)
     * @param    DeadZone    Minimum analog value before motion will be reported
     * @param    InvertY    Invert the Y axis
     * @param    InvertX    Invert the X axis
     * @return    This action
     */
    public function addMouseMotion(Trigger:FlxAnalogState, Axis:FlxAnalogAxis = EITHER, PixelsPerUnit:Int = 10, DeadZone:Float = 0.1, InvertY:Bool = false,
            InvertX:Bool = false):FlxActionAnalog
    {
        return add(new FlxActionInputAnalogMouseMotion(Trigger, Axis, PixelsPerUnit, DeadZone, InvertY, InvertX));
    }

    /**
     * Add mouse input -- X/Y is the mouse's absolute screen position
     * @param    Trigger What state triggers this action (MOVED, JUST_MOVED, STOPPED, JUST_STOPPED)
     * @param    Axis which axes to monitor for triggering: X, Y, EITHER, or BOTH
     * @return    This action
     */
    public function addMousePosition(Trigger:FlxAnalogState, Axis:FlxAnalogAxis = EITHER):FlxActionAnalog
    {
        return add(new FlxActionInputAnalogMousePosition(Trigger, Axis));
    }

    /**
     * Add gamepad action input for analog (trigger, joystick, touchpad, etc) events
     * @param    InputID "universal" gamepad input ID (LEFT_TRIGGER, RIGHT_ANALOG_STICK, TILT_PITCH, etc)
     * @param    Trigger What state triggers this action (MOVED, JUST_MOVED, STOPPED, JUST_STOPPED)
     * @param    Axis which axes to monitor for triggering: X, Y, EITHER, or BOTH
     * @param    GamepadID specific gamepad ID, or FlxInputDeviceID.FIRST_ACTIVE / ALL
     * @return    This action
     */
    public function addGamepad(InputID:FlxGamepadInputID, Trigger:FlxAnalogState, Axis:FlxAnalogAxis = EITHER,
            GamepadID:Int = FlxInputDeviceID.FIRST_ACTIVE):FlxActionAnalog
    {
        return add(new FlxActionInputAnalogGamepad(InputID, Trigger, Axis, GamepadID));
    }

    override public function update():Void
    {
        _x = null;
        _y = null;
        super.update();
    }

    override public function destroy():Void
    {
        callback = null;
        super.destroy();
    }

    override public function toString():String
    {
        return "FlxAction(" + type + ") name:" + name + " x/y:" + _x + "," + _y;
    }

    override public function check():Bool
    {
        var val = super.check();
        if (val && callback != null)
        {
            callback(this);
        }
        return val;
    }

    function get_x():Float
    {
        return (_x != null) ? _x : 0;
    }

    function get_y():Float
    {
        return (_y != null) ? _y : 0;
    }
}

/**
 * @since 4.6.0
 */
@:allow(flixel.input.actions.FlxActionDigital, flixel.input.actions.FlxActionAnalog, flixel.input.actions.FlxActionSet)
class FlxAction implements IFlxDestroyable
{
    /**
     * Digital or Analog
     */
    public var type(default, null):FlxInputType;

    /**
     * The name of the action, "jump", "fire", "move", etc.
     */
    public var name(default, null):String;

    /**
     * This action's numeric handle for the Steam API (ignored if not using Steam)
     */
    var steamHandle(default, null):Int = -1;

    /**
     * If true, this action has just been triggered
     */
    public var triggered(default, null):Bool = false;

    /**
     * The inputs attached to this action
     */
    public var inputs:Array<FlxActionInput>;

    var _x:Null<Float> = null;
    var _y:Null<Float> = null;

    var _timestamp:Int = 0;
    var _checked:Bool = false;

    /**
     * Whether the steam controller inputs for this action have changed since the last time origins were polled. Always false if steam isn't active
     */
    public var steamOriginsChanged(default, null):Bool = false;

    #if FLX_STEAMWRAP
    var _steamOriginsChecksum:Int = 0;
    var _steamOrigins:Array<EControllerActionOrigin>;
    #end

    function new(InputType:FlxInputType, Name:String)
    {
        type = InputType;
        name = Name;
        inputs = [];
        #if FLX_STEAMWRAP
        _steamOrigins = [];
        for (i in 0...FlxSteamController.MAX_ORIGINS)
        {
            _steamOrigins.push(cast 0);
        }
        #end
    }

    public function getFirstSteamOrigin():Int
    {
        #if FLX_STEAMWRAP
        if (_steamOrigins == null)
            return 0;
        for (i in 0..._steamOrigins.length)
        {
            if (_steamOrigins[i] != EControllerActionOrigin.NONE)
            {
                return cast _steamOrigins[i];
            }
        }
        #end
        return 0;
    }

    public function getSteamOrigins(?origins:Array<Int>):Array<Int>
    {
        #if FLX_STEAMWRAP
        if (origins == null)
        {
            origins = [];
        }
        if (_steamOrigins != null)
        {
            for (i in 0..._steamOrigins.length)
            {
                origins[i] = cast _steamOrigins[i];
            }
        }
        #end
        return origins;
    }

    public function removeAll(Destroy:Bool = true):Void
    {
        var len = inputs.length;
        for (i in 0...len)
        {
            var j = len - i - 1;
            var input = inputs[j];
            remove(input, Destroy);
            inputs.splice(j, 1);
        }
    }

    public function remove(Input:FlxActionInput, Destroy:Bool = false):Void
    {
        if (Input == null)
            return;
        inputs.remove(Input);
        if (Destroy)
        {
            Input.destroy();
        }
    }

    public function toString():String
    {
        return "FlxAction(" + type + ") name:" + name;
    }

    /**
     * See if this action has just been triggered
     */
    public function check():Bool
    {
        _x = null;
        _y = null;

        if (_timestamp == FlxG.game.ticks)
        {
            triggered = _checked;
            return _checked; // run no more than once per frame
        }

        _timestamp = FlxG.game.ticks;
        _checked = false;

        var len = inputs != null ? inputs.length : 0;
        for (i in 0...len)
        {
            var j = len - i - 1;
            var input = inputs[j];

            if (input.destroyed)
            {
                inputs.splice(j, 1);
                continue;
            }

            input.update();

            if (input.check(this))
            {
                _checked = true;
            }
        }

        triggered = _checked;
        return _checked;
    }

    /**
     * Check input states & fire callbacks if anything is triggered
     */
    public function update():Void
    {
        check();
    }

    public function destroy():Void
    {
        FlxDestroyUtil.destroyArray(inputs);
        inputs = null;
        #if FLX_STEAMWRAP
        FlxArrayUtil.clearArray(_steamOrigins);
        _steamOrigins = null;
        #end
    }

    public function match(other:FlxAction):Bool
    {
        return name == other.name && steamHandle == other.steamHandle;
    }

    function addGenericInput(input:FlxActionInput):FlxAction
    {
        if (inputs == null)
        {
            inputs = [];
        }
        if (!checkExists(input))
            inputs.push(input);

        return this;
    }

    function checkExists(input:FlxActionInput):Bool
    {
        if (inputs == null)
            return false;
        return inputs.contains(input);
    }
}