Monkey: virtual stick

Strict
Import mojo

Class VirtualStickTestApp Extends App
    Const PLAYFIELD_WIDTH:Float = 200
    Const PLAYFIELD_HEIGHT:Float = 200
    Const PLAYER_SPEED:Float = 5

    ' our virtual stick
    Field mystick:MyStick

    ' the "player"'s location
    Field playerX:Float = PLAYFIELD_WIDTH/2
    Field playerY:Float = PLAYFIELD_HEIGHT/2
    Field playfieldX:Float
    Field playfieldY:Float = 10

    Method OnCreate:Int()
        mystick = New MyStick
        mystick.SetRing(100, DeviceHeight()-100, 40)
        mystick.SetStick(0, 0, 15)
        mystick.SetDeadZone(0.2)
        mystick.SetTriggerDistance(5)
        playfieldX = DeviceWidth()-PLAYFIELD_WIDTH-10
        SetUpdateRate 30
        Return 0
    End

    Method OnUpdate:Int()
        ' update the stick usage
        UpdateStick()

        ' update the player position
        If mystick.GetVelocity() <> 0 Then
            playerX += mystick.GetDX() * PLAYER_SPEED
            playerY -= mystick.GetDY() * PLAYER_SPEED
            If playerX < 0 Then
                playerX = 0
            Elseif playerX > PLAYFIELD_WIDTH Then
                playerX = PLAYFIELD_WIDTH
            End
            If playerY < 0 Then
                playerY = 0
            Elseif playerY > PLAYFIELD_HEIGHT Then
                playerY = PLAYFIELD_HEIGHT
            End
        End
        Return 0
    End

    Method OnRender:Int()
        Cls(0,0,0)

        mystick.DoRenderRing()
        mystick.DoRenderStick()
        DrawOutlineRect(playfieldX, playfieldY, PLAYFIELD_WIDTH, PLAYFIELD_HEIGHT)
        DrawCircle(playfieldX + playerX, playfieldY + playerY, 5)

        ' some test info
        DrawText("angle="+mystick.GetAngle(), 10, 10)
        DrawText("vel="+mystick.GetVelocity(), 10, 30)
        DrawText("dx="+mystick.GetDX(), 10, 50)
        DrawText("dy="+mystick.GetDY(), 10, 70)
        Return 0
    End

    Method UpdateStick:Void()
        If mystick.GetTouchNumber() < 0 Then
            #if TARGET="android" Then
                For Local i:Int = 0 To 31
                    If TouchHit(i) And mystick.GetTouchNumber() < 0 Then
                        mystick.StartTouch(TouchX(i), TouchY(i), i)
                    End
                End
            #else
                If MouseHit(0) Then
                    mystick.StartTouch(MouseX(), MouseY(), 0)
                End
            #endif
        End

        If mystick.GetTouchNumber() >= 0 Then
            #if TARGET="android" Then
                If TouchDown(mystick.GetTouchNumber()) Then
                    mystick.UpdateTouch(TouchX(mystick.GetTouchNumber()), TouchY(mystick.GetTouchNumber()))
                Else
                    mystick.StopTouch()
                End
            #else
                If MouseDown(0) Then
                    mystick.UpdateTouch(MouseX(), MouseY())
                Else
                    mystick.StopTouch()
                End
            #endif
        End
    End
End

Class MyStick Extends VirtualStick
    Method RenderRing:Void(x:Float, y:Float)
        SetColor 0, 0, 255
        Super.RenderRing(x, y)
        SetColor 255, 255, 255
    End

    Method RenderStick:Void(x:Float, y:Float)
        SetColor 0, 255, 0
        Super.RenderStick(x, y)
        SetColor 255, 255, 255
    End
End

Class VirtualStick
Private
    ' the coordinates and dimensions for the virtual stick's ring (where the user will first touch)
    Field ringX:Float
    Field ringY:Float
    Field ringRadius:Float

    ' the coordinates and dimensions for the stick (what the user is pushing around)
    ' X/Y is relative to the centre of the ring, and positive Y points up
    Field stickX:Float = 0
    Field stickY:Float = 0
    Field stickRadius:Float
    Field stickAngle:Float
    Field stickPower:Float

    ' where the user first touched
    Field firstTouchX:Float
    Field firstTouchY:Float

    ' power must always be >= this, or we return 0
    Field deadZone:Float

    ' we need to move the stick this much before it triggers
    Field triggerDistance:Float = -1
    Field triggered:Bool = False

    ' the index of the touch event that initiated the stick movement
    Field touchNumber:Int = -1

    ' clips the stick to be within the ring, and updates angles, etc.
    Method UpdateStick:Void()
        If touchNumber>=0 Then
            Local length:Float = Sqrt(stickX*stickX+stickY*stickY)
            stickPower = length/ringRadius
            If stickPower > 1 Then stickPower = 1

            If stickPower < deadZone Then
                stickPower = 0
                stickAngle = 0
                stickX = 0
                stickY = 0
            Else
                If stickX = 0 And stickY = 0 Then
                    stickAngle = 0
                    stickPower = 0
                Elseif stickX = 0 And stickY > 0 Then
                    stickAngle = 90
                Elseif stickX = 0 And stickY < 0 Then
                    stickAngle = 270
                Elseif stickY = 0 And stickX > 0 Then
                    stickAngle = 0
                Elseif stickY = 0 And stickX < 0 Then
                    stickAngle = 180
                Elseif stickX > 0 And stickY > 0 Then
                    stickAngle = ATan(stickY/stickX)
                Elseif stickX < 0 Then
                    stickAngle = 180+ATan(stickY/stickX)
                Else
                    stickAngle = 360+ATan(stickY/stickX)
                End
                If length > ringRadius Then
                    stickPower = 1
                    stickX = Cos(stickAngle) * ringRadius
                    stickY = Sin(stickAngle) * ringRadius
                End
            End
        End
    End

Public

    Method GetTouchNumber:Int()
        Return touchNumber
    End

    ' the angle in degrees that the user is pushing, going counter-clockwise from right
    Method GetAngle:Float()
        Return stickAngle
    End

    ' the strength of the movement (0 means dead centre, 1 means at the edge of the ring (or past it)
    Method GetVelocity:Float()
        Return stickPower
    End

    ' based on the angle and velocity, get the DX
    Method GetDX:Float()
        Return Cos(stickAngle) * stickPower
    End

    ' based on the angle and velocity, get the DY
    Method GetDY:Float()
        Return Sin(stickAngle) * stickPower
    End

    ' we just touched the screen at point (x,y), so start "controlling" if we touched inside the ring
    Method StartTouch:Void(x:Float, y:Float, touchnum:Int)
        If touchNumber < 0 Then
            If (x-ringX)*(x-ringX) + (y-ringY)*(y-ringY) <= ringRadius*ringRadius Then
                touchNumber = touchnum
                firstTouchX = x
                firstTouchY = y
                triggered = False
                If triggerDistance <= 0 Then
                    triggered = True
                    stickX = x-ringX
                    stickY = ringY-y
                End
                UpdateStick()
            End
        End
    End

    ' a touch just moved, so we may need to update the stick
    Method UpdateTouch:Void(x:Float, y:Float)
        If touchNumber>=0 Then
            If Not triggered Then
                If (x-firstTouchX)*(x-firstTouchX)+(y-firstTouchY)*(y-firstTouchY) > triggerDistance*triggerDistance Then
                    triggered = True
                End
            End
            If triggered Then
                stickX = x - ringX
                stickY = ringY - y
                UpdateStick()
            End
        End
    End

    ' we just released a touch, which may have been this one
    Method StopTouch:Void()
        If touchNumber>=0 Then
            touchNumber = -1
            stickX = 0
            stickY = 0
            stickAngle = 0
            stickPower = 0
            triggered = False
        End
    End

    Method DoRenderRing:Void()
        RenderRing(ringX, ringY)
    End

    Method DoRenderStick:Void()
        RenderStick(ringX+stickX, ringY-stickY)
    End

    ' draws the stick (may be overridden to do images, etc.)
    Method RenderStick:Void(x:Float, y:Float)
        DrawCircle(x, y, stickRadius)
    End

    ' draws the outside ring (may be overridden to do images, etc.)
    Method RenderRing:Void(x:Float, y:Float)
        DrawCircle(x, y, ringRadius)
    End

    ' set the location and radius of the ring
    Method SetRing:Void(ringX:Float, ringY:Float, ringRadius:Float)
        Self.ringX = ringX
        Self.ringY = ringY
        Self.ringRadius = ringRadius
    End

    ' set the location and radius of the stick
    Method SetStick:Void(stickX:Float, stickY:Float, stickRadius:Float)
        Self.stickX = stickX
        Self.stickY = stickY
        Self.stickRadius = stickRadius
    End

    Method SetDeadZone:Void(deadZone:Float)
        Self.deadZone = deadZone
    End

    Method SetTriggerDistance:Void(triggerDistance:Float)
        Self.triggerDistance = triggerDistance
    End
End

Function Main:Int()
    New VirtualStickTestApp
    Return 0
End

Function DrawOutlineRect:Void(x:Float, y:Float, width:Float, height:Float)
    DrawLine(x, y, x+width, y)
    DrawLine(x, y, x, y+height)
    DrawLine(x+width, y, x+width, y+height)
    DrawLine(x, y+height, x+width, y+height)
End