I need to be able to get the given input's distance inside the joystick, bounded within -100% and 100% and an unbounded version of it, get the position of where the mouse's position projects to the joystick's ring and get the angle from the joystick to the given input that also wouldn't need unit conversion since all of the inputs would always be in within terms of a given unit.


1 Answer 1


I happen to have written an algorithm that does exactly that!

import kotlin.math.*

 * As long as all your input is in X units, you shouldn't need any kind of conversion
 * If you don't want the joystick to gravitate towards its origin position after some time,
 * use the UI viewport's units instead of game viewport's units (as the UI viewport's camera doesnt move/gravitate)
data class Joystick (var initialX: Float, var initialY: Float, var radius: Float = 1f){
    var active = false
    var x = 0f; var y = 0f; var degree = 0f; var stickDistPercentageX = 0f; var stickDistPercentageY = 0f
    var distanceX = 0f; var distanceY = 0f; var realStickDistPercentageX = 0f; var realStickDistPercentageY = 0f
    var cachedValues = Array(9){1f}.apply {
        this[0] = x
        this[1] = y
        this[2] = degree
        this[3] = stickDistPercentageX
        this[4] = stickDistPercentageY
        this[5] = distanceX
        this[6] = distanceY
        this[7] = realStickDistPercentageX
        this[8] = realStickDistPercentageY

    fun updateValues(mouseX: Float, mouseY: Float) : Array<Float>
        var distanceY = mouseY - initialY
        var distanceX = mouseX - initialX
        if(distanceX == 0f || distanceY == 0f) return cachedValues 
        // not sure if I should keep this check, the atan2 function seems to function normally despite such inputs anyway.
        var degree = atan2(distanceY,distanceX)

        var outerCircleHalfR = sqrt(distanceX*distanceX+distanceY*distanceY)
        var realStickDistPercentageX = cos(degree) *  abs(outerCircleHalfR/radius)
        var realStickDistPercentageY = sin(degree) *  abs(outerCircleHalfR/radius)
        var stickDistPercentageX = cos(degree) *  abs(outerCircleHalfR/radius).coerceIn(-1f, 1f)
        var stickDistPercentageY = sin(degree) *  abs(outerCircleHalfR/radius).coerceIn(-1f, 1f)

        var x = cos(degree) * radius
        var y = sin(degree) * radius

        this.x = x
        this.y = y
        this.degree = degree
        this.stickDistPercentageX = stickDistPercentageX
        this.stickDistPercentageY = stickDistPercentageY
        this.distanceX = distanceX
        this.distanceY = distanceY
        this.realStickDistPercentageX = realStickDistPercentageX
        this.realStickDistPercentageY = realStickDistPercentageY

        return cachedValues

and an example usage:

if(joystick.active) {
    run joy@
        joystick.updateValues(unProjectMouse.x, unProjectMouse.y)
            joystick.stickDistPercentageX * delta * IMV * 10,
            joystick.stickDistPercentageY * delta * IMV * 10, 0f)
        if(!DEBUG) return@joy
        uiDebug.color = Color.YELLOW
        uiDebug.circle(joystick.initialX, joystick.initialY, joystick.radius, 50)
        uiDebug.color = Color.WHITE
        uiDebug.circle(joystick.initialX + (joystick.stickDistPercentageX * joystick.radius),
                       joystick.initialY + (joystick.stickDistPercentageY * joystick.radius), joystick.radius/3, 50)
        uiDebug.color = Color.CYAN
        uiDebug.circle(joystick.initialX + joystick.x, joystick.initialY + joystick.y, joystick.radius/5, 50)

override fun touchDown(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean {
    centralScreen.joystick.initialX = centralScreen.unProjectMouse.x
    centralScreen.joystick.initialY = centralScreen.unProjectMouse.y
    centralScreen.joystick.active = true
    return false

override fun touchUp(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean {
    centralScreen.joystick.active = false
    return false

A screenshot of an illustration of the algorithm:
illustration of the algorithm

Also if you input a negative number for the radius, luckily, nothing breaks, and the only side effect (as far as I observed) is that the stick and the dot of the joystick happens to be in the direction opposite to the direction of the mouse

Now I'm sure that a lot of this code needs some explanation, so there we go.

First of all we call the updateValues function with the unprojected positions of the mouse to the viewport (The units of the InitialX, InitialY, radius, and the arguments of the updateValues function need to be consistent).

For those that don't know what Unproject is, (Non-LibGDX users I mean) | If you know it, skip this part.

Say the "screen" is 100PX wide and 100 PX tall and the camera in the program has a worldWidth of 5, which means that 1 unit equals 20 Pixels on the screen. and say, the camera is at the position, x,y: 5,5, the unproject function first converts the mouse positions into the internal units of the camera (the viewport), then adds the camera offset (how far away from the origin is it, AKA the position) and there you have your unprojected value!

The stickDistPercentage value of the joystick is how far away it is from the radius in percentages, the value ranges from -1 (-100%) to +1 (100%). Since it is within -1 to +1, you can directly use it in multiplicative operations without any further conversion of it. The realStickDistPercentage is the same as this, just that it is not in the bounds of -100% to 100%

The IMV stands for InverseMouseMovement (just realized that I named it mouse movement); either -1 or 1, pull or push basically.

Line 11 draws the maximum bounds of the joystick. 50 stands for how circle-y it should look, it is outside our topic.

Line 14 draws the "stick" of the joystick that has a radius of Joystick.radius/3

Line 17 draws the dot on the ring itself.

The x, y is the dot in the ring of the circle, it isn't necessarily mandatory for a joystick but I kept it here because it might be useful in different cases.

The degree variable is the degree between the initial position and the given input position basically, don't know how to further describe it.

The distance is the distance between the initial position and the given input position

  • \$\begingroup\$ Please use Stack Exchange's built in image posting/hosting instead of third party image links. Over time, third party links tend to break. Refer to the bottom of the formatting help page for more info on how to post images here. \$\endgroup\$
    – Pikalek
    Jan 25, 2022 at 17:18

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .