HaxeFlixel/flixel

View on GitHub
flixel/ui/FlxAnalog.hx

Summary

Maintainability
Test Coverage
package flixel.ui;

import flixel.FlxG;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
import flixel.input.touch.FlxTouch;
import flixel.math.FlxAngle;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.system.FlxAssets;
import flixel.util.FlxDestroyUtil;

/**
 * A virtual thumbstick - useful for input on mobile devices.
 *
 * @author Ka Wing Chin
 */
class FlxAnalog extends FlxSpriteGroup
{
    /**
     * Shows the current state of the button.
     */
    public var status:Int = NORMAL;

    public var thumb:FlxSprite;

    /**
     * The background of the joystick, also known as the base.
     */
    public var base:FlxSprite;

    /**
     * This function is called when the button is released.
     */
    public var onUp:Void->Void;

    /**
     * This function is called when the button is pressed down.
     */
    public var onDown:Void->Void;

    /**
     * This function is called when the mouse goes over the button.
     */
    public var onOver:Void->Void;

    /**
     * This function is called when the button is hold down.
     */
    public var onPressed:Void->Void;

    /**
     * Used with public variable status, means not highlighted or pressed.
     */
    static inline var NORMAL:Int = 0;

    /**
     * Used with public variable status, means highlighted (usually from mouse over).
     */
    static inline var HIGHLIGHT:Int = 1;

    /**
     * Used with public variable status, means pressed (usually from mouse click).
     */
    static inline var PRESSED:Int = 2;

    /**
     * A list of analogs that are currently active.
     */
    static var _analogs:Array<FlxAnalog> = [];

    #if FLX_TOUCH
    /**
     * The current pointer that's active on the analog.
     */
    var _currentTouch:FlxTouch;

    /**
     * Helper array for checking touches
     */
    var _tempTouches:Array<FlxTouch> = [];
    #end

    /**
     * The area which the joystick will react.
     */
    var _zone:FlxRect = FlxRect.get();

    /**
     * The radius in which the stick can move.
     */
    var _radius:Float = 0;

    var _direction:Float = 0;
    var _amount:Float = 0;

    /**
     * The speed of easing when the thumb is released.
     */
    var _ease:Float;

    /**
     * Create a virtual thumbstick - useful for input on mobile devices.
     *
     * @param   X            The X-coordinate of the point in space.
     * @param   Y            The Y-coordinate of the point in space.
     * @param   Radius       The radius where the thumb can move. If 0, half the base's width will be used.
     * @param   Ease         Used to smoothly back thumb to center. Must be between 0 and (FlxG.updateFrameRate / 60).
     * @param   BaseGraphic  The graphic you want to display as base of the joystick.
     * @param   ThumbGraphic The graphic you want to display as thumb of the joystick.
     */
    public function new(X:Float = 0, Y:Float = 0, Radius:Float = 0, Ease:Float = 0.25, ?BaseGraphic:FlxGraphicAsset, ?ThumbGraphic:FlxGraphicAsset)
    {
        super();

        _radius = Radius;
        _ease = FlxMath.bound(Ease, 0, 60 / FlxG.updateFramerate);

        _analogs.push(this);

        _point = FlxPoint.get();

        createBase(BaseGraphic);
        createThumb(ThumbGraphic);

        x = X;
        y = Y;

        scrollFactor.set();
        moves = false;
    }

    /**
     * Creates the background of the analog stick.
     */
    function createBase(?Graphic:FlxGraphicAsset):Void
    {
        base = new FlxSprite(0, 0, Graphic);
        if (Graphic == null)
        {
            base.frames = FlxAssets.getVirtualInputFrames();
            base.animation.frameName = "base";
            base.resetSizeFromFrame();
        }
        base.x += -base.width * 0.5;
        base.y += -base.height * 0.5;
        base.scrollFactor.set();
        base.solid = false;

        #if FLX_DEBUG
        base.ignoreDrawDebug = true;
        #end

        add(base);
    }

    /**
     * Creates the thumb of the analog stick.
     */
    function createThumb(?Graphic:FlxGraphicAsset):Void
    {
        thumb = new FlxSprite(0, 0, Graphic);
        if (Graphic == null)
        {
            thumb.frames = FlxAssets.getVirtualInputFrames();
            thumb.animation.frameName = "thumb";
            thumb.resetSizeFromFrame();
        }
        thumb.scrollFactor.set();
        thumb.solid = false;

        #if FLX_DEBUG
        thumb.ignoreDrawDebug = true;
        #end

        add(thumb);
    }

    /**
     * Creates the touch zone. It's based on the size of the background.
     * The thumb will react when the mouse is in the zone.
     */
    function createZone():Void
    {
        if (base != null && _radius == 0)
            _radius = base.width * 0.5;

        _zone.set(x - _radius, y - _radius, 2 * _radius, 2 * _radius);
    }

    /**
     * Clean up memory.
     */
    override public function destroy():Void
    {
        super.destroy();

        _zone = FlxDestroyUtil.put(_zone);

        _analogs.remove(this);
        onUp = null;
        onDown = null;
        onOver = null;
        onPressed = null;
        thumb = null;
        base = null;

        #if FLX_TOUCH
        _currentTouch = null;
        _tempTouches = null;
        #end
    }

    /**
     * Update the behavior.
     */
    override public function update(elapsed:Float):Void
    {
        var offAll:Bool = true;

        #if FLX_MOUSE
        _point.set(FlxG.mouse.screenX, FlxG.mouse.screenY);

        if (!updateAnalog(_point, FlxG.mouse.pressed, FlxG.mouse.justPressed, FlxG.mouse.justReleased))
        {
            offAll = false;
        }
        #end

        // There is no reason to get into the loop if their is already a pointer on the analog
        #if FLX_TOUCH
        if (_currentTouch != null)
        {
            _tempTouches.push(_currentTouch);
        }
        else
        {
            for (touch in FlxG.touches.list)
            {
                var touchInserted:Bool = false;

                for (analog in _analogs)
                {
                    // Check whether the pointer is already taken by another analog.
                    // TODO: check this place. This line was 'if (analog != this && analog._currentTouch != touch && touchInserted == false)'
                    if (analog == this && analog._currentTouch != touch && !touchInserted)
                    {
                        _tempTouches.push(touch);
                        touchInserted = true;
                    }
                }
            }
        }

        for (touch in _tempTouches)
        {
            _point = touch.getWorldPosition(FlxG.camera, _point);

            if (!updateAnalog(_point, touch.pressed, touch.justPressed, touch.justReleased, touch))
            {
                offAll = false;
                break;
            }
        }
        #end

        if ((status == HIGHLIGHT || status == NORMAL) && _amount != 0)
        {
            _amount -= _amount * _ease * FlxG.updateFramerate / 60;

            if (Math.abs(_amount) < 0.1)
            {
                _amount = 0;
                _direction = 0;
            }
        }

        thumb.x = x + Math.cos(_direction) * _amount * _radius - (thumb.width * 0.5);
        thumb.y = y + Math.sin(_direction) * _amount * _radius - (thumb.height * 0.5);

        if (offAll)
        {
            status = NORMAL;
        }

        #if FLX_TOUCH
        _tempTouches.splice(0, _tempTouches.length);
        #end

        super.update(elapsed);
    }

    function updateAnalog(TouchPoint:FlxPoint, Pressed:Bool, JustPressed:Bool, JustReleased:Bool, ?Touch:FlxTouch):Bool
    {
        var offAll:Bool = true;

        #if FLX_TOUCH
        // Use the touch to figure out the world position if it's passed in, as
        // the screen coordinates passed in touchPoint are wrong
        // if the control is used in a group, for example.
        if (Touch != null)
        {
            TouchPoint.set(Touch.screenX, Touch.screenY);
        }
        #end

        if (_zone.containsPoint(TouchPoint) || (status == PRESSED))
        {
            offAll = false;

            if (Pressed)
            {
                #if FLX_TOUCH
                if (Touch != null)
                {
                    _currentTouch = Touch;
                }
                #end
                status = PRESSED;

                if (JustPressed)
                {
                    if (onDown != null)
                    {
                        onDown();
                    }
                }

                if (status == PRESSED)
                {
                    if (onPressed != null)
                    {
                        onPressed();
                    }

                    var dx:Float = TouchPoint.x - x;
                    var dy:Float = TouchPoint.y - y;

                    var dist:Float = Math.sqrt(dx * dx + dy * dy);

                    if (dist < 1)
                    {
                        dist = 0;
                    }

                    _direction = Math.atan2(dy, dx);
                    _amount = Math.min(_radius, dist) / _radius;

                    acceleration.x = Math.cos(_direction) * _amount;
                    acceleration.y = Math.sin(_direction) * _amount;
                }
            }
            else if (JustReleased && status == PRESSED)
            {
                #if FLX_TOUCH
                _currentTouch = null;
                #end

                status = HIGHLIGHT;

                if (onUp != null)
                {
                    onUp();
                }

                acceleration.set();
            }

            if (status == NORMAL)
            {
                status = HIGHLIGHT;

                if (onOver != null)
                {
                    onOver();
                }
            }
        }

        return offAll;
    }

    /**
     * Returns the angle in degrees.
     */
    public function getAngle():Float
    {
        return _direction * FlxAngle.TO_DEG;
    }

    /**
     * Whether the thumb is pressed or not.
     */
    public var pressed(get, never):Bool;

    inline function get_pressed():Bool
    {
        return status == PRESSED;
    }

    /**
     * Whether the thumb is just pressed or not.
     */
    public var justPressed(get, never):Bool;

    function get_justPressed():Bool
    {
        #if FLX_MOUSE
        return FlxG.mouse.justPressed && status == PRESSED;
        #end

        #if FLX_TOUCH
        if (_currentTouch != null)
        {
            return _currentTouch.justPressed && status == PRESSED;
        }
        #end

        return false;
    }

    /**
     * Whether the thumb is just released or not.
     */
    public var justReleased(get, never):Bool;

    function get_justReleased():Bool
    {
        #if FLX_MOUSE
        return FlxG.mouse.justReleased && status == HIGHLIGHT;
        #end

        #if FLX_TOUCH
        if (_currentTouch != null)
        {
            return _currentTouch.justReleased && status == HIGHLIGHT;
        }
        #end

        return false;
    }

    override function set_x(X:Float):Float
    {
        super.set_x(X);
        createZone();

        return X;
    }

    override function set_y(Y:Float):Float
    {
        super.set_y(Y);
        createZone();

        return Y;
    }
}