Swift with Spritekit - Joystick with knob Tutorial

2.3k Views Asked by At

Im working on an SpriteKit Tutorial with Swift these days.

I programmed Joystick with a knob, that can be moved everywhere within the joystick circle.

Heres the relevant code snippet:

for touch in touches {
let position = touch.location(in: joystick)

let length = sqrt(pow(position.y, 2) + pow(position.x, 2))
let angle = atan2(position.y, position.x)

if knobRadius > length {
    joystickKnob.position = position
} else {
    joystickKnob.position = CGPoint(x: cos(angle) * knobRadius, y: sin(angle) * knobRadius)
}}

Here is a picture from the tutorial:

enter image description here

But now I want to do this a little different.

I want that the Knob only can be moved on the X and Y axis like a cross - like Up, Down, Left, right on a rail.

enter image description here

I really do understand the code - I think its more like a math problem :)

Can somebody tell me how to move the knob within a cross on rails?

2

There are 2 best solutions below

1
Knight0fDragon On BEST ANSWER

To keep your angle on the cross hairs, you need to round the angle to an interval of pi/2

to do that, just do:

angle = (angle * 2 / CGFloat.pi).rounded() * (CGFloat.pi / 2)

Below I have modified your code to work a lot nicer, and allows you to avoid the branching.

I am using the base radius instead of the knob radius, so that you can never exceed the base.

for touch in touches {
    let position = touch.location(in: joystick)

    let radius = min(baseRadius,sqrt(pow(position.y, 2) + pow(position.x, 2))
    // a nicer way to get radius
    //let radius = min(baseRadius,position.distance(to:CGPoint.zero)) 
    let angle = atan2(position.y, position.x)
    if crossHairStick
    {
        angle = (angle * 2 / CGFloat.pi).rounded() * (CGFloat.pi / 2)
    }
    joystickKnob.position = CGPoint(x: cos(angle) * radius, y: sin(angle) * radius)
}

If you want to keep the knob inside of the base, subtract the knob radius. (This assumes both radius are positive, otherwise use absolute value of the radius)

for touch in touches {
    let position = touch.location(in: joystick)

    let radius = min(baseRadius - knobRadius,sqrt(pow(position.y, 2) + pow(position.x, 2))
    let angle = atan2(position.y, position.x)
    if crossHairStick
    {
        angle = (angle * 2 / CGFloat.pi).rounded() * (CGFloat.pi / 2)
    }
    joystickKnob.position = CGPoint(x: cos(angle) * radius, y: sin(angle) * radius)
}
1
rob mayoff On

So you want to constrain angle to be a multiple of .pi / 2 (90 degrees). The atan2 function returns a value in the range -.pi ... +.pi. Convert it from radians to units of quarter-turns (right angles), round to an integer, then convert back to radians.

let touchAngle = atan2(position.y, position.x) // -.pi ... +.pi
let quarterTurns = touchAngle * 2 / .pi // -2 ... +2
let integralQuarterTurns = quarterTurns.rounded() // -2, -1, 0, +1, or +2
let angle = integralQuarters * .pi / 2

Alternatively, look at which of abs(position.x) and abs(position.y) is larger, and use that to determine angle:

let angle: CGFloat
switch abs(position.x) > abs(position.y) {
case true where position.x > 0: angle = 0
case true where position.x < 0: angle = .pi
case false where position.y > 0: angle = .pi / 2
case false where position.y < 0: angle = .pi / -2
default: angle = 0 // position == .zero so joystick is actually centered
}