Drag Rotate a Node around a fixed point
Asked Answered
Z

3

9

I'm trying to create a spinable node similar to the "prize wheel" in this question. So far I have the flinging capability, adding angular impulses on a physics body using a UIPanGestureRecognizer that works really well. I can also stop the spinning using a touch.

Now I'm trying to allow fine adjustment of the wheel using a drag or swipe gesture so if the player isn't happy with what they end up with they can manually spin/drag/rotate it to their favoured rotation.

Currently I save the location of the touch in the touchesBegan and try to increment the zRotation of my node in the update loop.

The rotation doesn't follow my finger and is jerky. I'm not sure if I'm getting an accurate enough read on the finger movement or if the change position of the finger isn't being accurately translated into radians. I suspect detecting the touch and then dealing with it in the update isn't a great solution.

Here's my code.

    override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent?) {
    if let touch = touches.first as? UITouch {
        var location = touch.locationInView(self.view)
        location = self.convertPointFromView(location)

        mostRecentTouchLocation = location

        let node = nodeAtPoint(location)
        if node.name == Optional("left") && node.physicsBody?.angularVelocity != 0
        {
            node.physicsBody = SKPhysicsBody(circleOfRadius:150)
            node.physicsBody?.applyAngularImpulse(0)
            node.physicsBody?.pinned = true
        }
    }
}

    override func update(currentTime: CFTimeInterval) {
        /* Called before each frame is rendered */
        if mostRecentTouchLocation != CGPointZero{
        let node = nodeAtPoint(mostRecentTouchLocation)
        if node.name == Optional("left")
        {
            var positionInScene:CGPoint = mostRecentTouchLocation
            let deltaX:Float = Float(positionInScene.x) - Float(node.position.x)
            let deltaY:Float = Float(positionInScene.y) - Float(node.position.y)
            let angle:CGFloat = CGFloat(atan2f(deltaY, deltaX))
            let maths:CGFloat = angle - (CGFloat(90) * (CGFloat(M_PI) / 180.0))
            node.zRotation += maths
            mostRecentTouchLocation = CGPointZero
        }
    }
}

I've spread some of the maths across multiple lines in the update to make debugging a bit easier.

I can add the PanGestureRecognizer code if needed but I'll try to keep it short for now.

EDIT Here is my latest code based on GilderMan's recommendation. I think it's working better but the rotation is far from smooth. It's jumping in large increments and not following the finger well. Does this mean there's something wrong with my angle calculation?

    override func didSimulatePhysics() {
    if mostRecentTouchLocation != CGPointZero {
        let node = nodeAtPoint(mostRecentTouchLocation)
        if node.name == Optional("left")
        {
            var positionInScene:CGPoint = mostRecentTouchLocation
            let deltaX:Float = Float(positionInScene.x) - Float(node.position.x)
            let deltaY:Float = Float(positionInScene.y) - Float(node.position.y)
            let angle:CGFloat = CGFloat(atan2f(deltaY, deltaX))
            node.zRotation += angle
            println(angle)
            mostRecentTouchLocation = CGPointZero
        }
    }
}
Zoogloea answered 21/8, 2015 at 14:48 Comment(3)
There was nothing wrong with the angle conversion in your original postMelliemelliferous
So the atan2f result is in radians which is the correct unit for adding to the zRotation?Zoogloea
Yes, the units for zRotation and atan2f are radiansMelliemelliferous
M
10

The following code simulates a prize wheel that spins based on touch. As the user's finger moves, the wheel rotates proportionately to the speed of the finger. When the user swipes on the wheel, the wheel will spin proportionately to the velocity of the swipe. You can change the angularDamping property of the physics body to slow or increase the rate at which the wheel comes to a stop.

class GameScene: SKScene {
    var startingAngle:CGFloat?
    var startingTime:TimeInterval?

    override func didMove(to view: SKView) {
        let wheel = SKSpriteNode(imageNamed: "Spaceship")
        wheel.name = "wheel"
        wheel.setScale(0.5)
        wheel.physicsBody = SKPhysicsBody(circleOfRadius: wheel.size.width/2)
        // Change this property as needed (increase it to slow faster)
        wheel.physicsBody!.angularDamping = 0.25
        wheel.physicsBody?.pinned = true
        wheel.physicsBody?.affectedByGravity = false
        addChild(wheel)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            let location = touch.location(in:self)
            let node = atPoint(location)
            if node.name == "wheel" {
                let dx = location.x - node.position.x
                let dy = location.y - node.position.y
                // Store angle and current time
                startingAngle = atan2(dy, dx)
                startingTime = touch.timestamp
                node.physicsBody?.angularVelocity = 0
            }
        }
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches{
            let location = touch.location(in:self)
            let node = atPoint(location)
            if node.name == "wheel" {
                let dx = location.x - node.position.x
                let dy = location.y - node.position.y

                let angle = atan2(dy, dx)
                // Calculate angular velocity; handle wrap at pi/-pi
                var deltaAngle = angle - startingAngle!
                if abs(deltaAngle) > CGFloat.pi {
                    if (deltaAngle > 0) {
                        deltaAngle = deltaAngle - CGFloat.pi * 2
                    }
                    else {
                        deltaAngle = deltaAngle + CGFloat.pi * 2
                    }
                }
                let dt = CGFloat(touch.timestamp - startingTime!)
                let velocity = deltaAngle / dt

                node.physicsBody?.angularVelocity = velocity

                // Update angle and time
                startingAngle = angle
                startingTime = touch.timestamp
            }
        }
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        startingAngle = nil
        startingTime = nil
    }
}
Melliemelliferous answered 21/8, 2015 at 22:30 Comment(4)
Wow that's incredible thanks so much! At about the 8 o'clock the sprite seems to flip around, have you seen that behaviour?Zoogloea
Yes, I'll try to fix thatMelliemelliferous
That works really well, thanks a lot. I'm going to spend some time going through this to make sure I fully understand it.Zoogloea
I've added a check to make sure that starting angle isn't nil in the touchesmoved for when a touch starts off of the node and moves on. Works great otherwise!Zoogloea
N
0

The zRotation of a SKNode is in radians. You can remove your conversion to degrees.

You may wish to do the angle adjustment in the didSimulatePhysics in order to compute the zRotation after physics have been applied. (This may not apply directly to this situation, but is good practice further down the line.)

Nyssa answered 21/8, 2015 at 20:41 Comment(8)
Thanks ill give that a tryZoogloea
Thanks for your sugguestion! This is much closer than what I had achieved but still doesn't follow the finger well, I think my angle calculation must be off. I'm also slightly concerned that clearly the mostRecentTouchLocation might not be doing what I think it does. Any ideas? I've put the latest code above.Zoogloea
Okay, so I'm going to assume that you are trying to have the wheel edge follow your finger. You have an incrementation of the angle, all you need to do is set the angle. So something like node.zRotation = angle instead of node.zRotation += angle. Then it should follow your finger.Nyssa
That seems to set the rotation of the sprite back close to 0 (at the 12 position if it were a clock). Doesn't that mean i should be using += as the rotation is tiny and should be added on?Zoogloea
Wait, are we trying to calculate the position where the finger turns the wheel to, or are we working on something else? Your question seems to be directed at control with a finger. You may need to add or subtract M_PI_2 (Pi / 2) to the calculated angle though for the finger code.Nyssa
I'm trying to drag the wheel around a fixed centre point and then apply angular momentum and the end of the gesture.Zoogloea
Maybe I miss-read the question, but are you trying to have direct control of the wheel, where as you rotate your finger around the center of the wheel, the wheel moves at the exact rotation that you moved your finger? Or are you having the wheel accelerate faster the more you move your finger from the center/straight up position?Nyssa
Let us continue this discussion in chat.Zoogloea
C
0

I tried two set of coodes below.

Code Sample 1

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        for touch in touches {
            let location = touch.location(in: self)
            
            let node = atPoint(location)
            guard let safeNode = node.parent else { break }
            
            if safeNode.name == "wheel" {
                let wheel = node.parent!
                

                let dx = location.x
                let dy = location.y
             
                startingAngle = atan2(dx, dy)
                startingTime = touch.timestamp
                
            }
        }
    }
    
    
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {

        for touch in touches {
            let location = touch.location(in: self)
            let node = atPoint(location)
            guard let safeNode = node.parent else { break }
            if safeNode.name == "wheel" {
                let wheel = node.parent!

                if !isRotating {

                    let dx = location.x
                    let dy = location.y

                    let angle = atan2(dx, dy)
                    
                    wheel.zRotation = -angle
                    //update angle and time
                    startingAngle = angle
                    startingTime = touch.timestamp

                    
                }
            }
        }
    }

Code Sample 2

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    
    for touch in touches {
        let location = touch.location(in: self)
        
        let node = atPoint(location)
        guard let safeNode = node.parent else { break }
        
        if safeNode.name == "wheel" {
            let wheel = node.parent!
            

            let dx = location.x
            let dy = location.y
            
            startingAngle = atan2(dx, dy)
            startingTime = touch.timestamp
            
        }
    }
}



    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {

        for touch in touches {
            let location = touch.location(in: self)
            let node = atPoint(location)
            guard let safeNode = node.parent else { break }
            if safeNode.name == "wheel" {
                let wheel = node.parent!


                if !isRotating {

                    let dx = location.x
                    let dy = location.y

                    let angle = atan2(dx, dy)

                    let deltaAngle = angle - startingAngle!
                    
                    wheel.zRotation -= deltaAngle

                    //update angle and time
                    startingAngle = angle
                    startingTime = touch.timestamp
                    
                }
            }
        }
    }

The only difference between Code Sample 1 & 2 is the method of finding zRotation of the wheel.

Code Sample 1 uses wheel.zRotation = -angle. It ensures wheel follows the finger position exactly. But there is a bug that the wheel will jump position when you touch two distant locations in wheel then move your fingure. I am still unable to fix this bug.

Code Sample 2 uses wheel.zRotation -= deltaAngle to stack the change in angle of each finger movement. It supposes to generate the same result of code sample 1 but it does not exactly follow the finger movement.

I found out the reason by comparing the zPosition of Code Sample 1 and 2 with same finger movement. The deltaAngle in Code Sample 2 is slightly less (up to 8 decimal places)so the wheel does not exactly follow the fingure movement.

Compunction answered 24/3, 2022 at 12:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.