Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I rotate a SpriteNode with a one finger “touch and drag”

How do I rotate a SpriteNode with a one finger “touch and drag” so that:

  • It doesn’t jerk around
  • It moves in a circle (I’ve successfully accomplished this part several times- both with a code only solution and with an SKS file)
  • It produces a meaningful value (as a physical control knob would)
  • It moves while my finger is on it but not when my finger is off

The things I’ve tried:

Using CGAffineTransform’s CGAffineTransformMakeRotation to effect a rotation of the knob in SpriteKit. But I cannot figure out how to use CGAffineTransformMakeRotation on a SpriteNode. I could put a different sort of object into my Scene or on top of it, but that’s just not right.

For example, Matthijs Hollemans’ MHRotaryKnob https://github.com/hollance/MHRotaryKnob . I translated Hollemans knob from Objective C to Swift 4 but ran into trouble attempting to use it in SpriteKit to rotate sprites. I didn’t get that because I could not figure out how to use knobImageView.transform = CGAffineTransformMakeRotation (newAngle * M_PI/180.0); in Swift with SpriteKit. I know I could use Hollemans Objective C class and push a UIImage over the top of my scene, but that doesn’t seem like the best nor most elegant solution.

I also translated Wex’s solution from Objective C to Swift Rotate image on center using one finger touch Using Allan Weir’s suggestions on dealing with the CGAffineTransform portions of the code https://stackoverflow.com/a/41724075/1678060 But that doesn’t work.

I've tried setting the zRotation on my sprite directly without using .physicalBody to no avail. It has the same jerky movement and will not stop where you want it to stop. And moves in the opposite direction of your finger drag- even when you put the '-' in front of the radian angle.

I’ve also tried 0x141E’s solution on Stack Overflow: Drag Rotate a Node around a fixed point This is the solution posted below using an .sks file (somewhat modified- I've tried the un-modified version and it is no better). This solution jerks around, doesn’t smoothly follow my finger, cannot consistently move the knob to a specific point. Doesn’t matter if I set physicsBody attributes to create friction, mass, or angularDamping and linearDamping or reducing the speed of the SKSpriteNode.

I have also scoured the Internet looking for a good solution in Swift 4 using SpriteKit, but so far to no avail.

import Foundation
import SpriteKit

class Knob: SKSpriteNode
{
    var startingAngle: CGFloat?
    var currentAngle: CGFloat?
    var startingTime: TimeInterval?
    var startingTouchPoint: CGPoint?

    override init(texture: SKTexture?, color: UIColor, size: CGSize) {
        super.init(texture: texture, color: color, size: size)
        self.setupKnob()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.setupKnob()
    }
    func setupKnob() {
        self.physicsBody = SKPhysicsBody(circleOfRadius: CGFloat(self.size.height))
        self.physicsBody?.pinned = true
        self.physicsBody?.isDynamic = true
        self.physicsBody?.affectedByGravity = false
        self.physicsBody?.allowsRotation = true
        self.physicsBody?.mass = 30.0
        //self.physicsBody?.friction = 0.8
        //self.physicsBody?.angularDamping = 0.8
        //self.physicsBody?.linearDamping = 0.9
        //self.speed = 0.1

        self.isUserInteractionEnabled = true
    }
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            let location = touch.location(in:self)
            let node = atPoint(location)
            startingTouchPoint = CGPoint(x: location.x, y: location.y)
            if node.name == "knobHandle" {
                let dx = location.x - node.position.x
                let dy = location.y - node.position.y
                startingAngle = atan2(dy, dx)
            }
        }
    }
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches{
            let location = touch.location(in:self)
            let node = atPoint(location)
            guard startingAngle != nil else {return}
            if node.name == "knobHandle" {
                let dx:CGFloat = location.x - node.position.x
                let dy:CGFloat = location.y - node.position.y
                var angle: CGFloat = atan2(dy, dx)

                angle = ((angle) * (180.0 / CGFloat.pi))
                let rotate = SKAction.rotate(toAngle: angle, duration: 2.0, shortestUnitArc: false)
                self.run(rotate)

                startingAngle = angle
            }
        }
    }
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        var touch: UITouch = touches.first!
        var location: CGPoint = touch.location(in: self)

        self.removeAllActions()
        startingAngle = nil
        startingTime = nil
    }
}

Edit: If I remove the conversion to degrees and change the duration of the SKAction to 1.0 in SKAction.rotate(toAngle:duration: 1.0, shortestUnitArc:) then it almost works: not as jerky, but still jerks; the lever doesn't change directions well- meaning sometimes if you attempt to move it opposite of the direction it was traveling it continues to go the old direction around the anchorPoint instead of the new direction you're dragging it.

Edit 2: GreatBigBore and I discussed both the SKAction rotation and the self.zRotation- the code above and the code below.

Edit 3: sicvayne suggested some code for the SKScene and I've adapted to SKSpriteNode (below). It doesn't move consistently or allow to you stop in a specific place.

import Foundation
import SpriteKit

class Knob: SKSpriteNode {

    var fingerLocation = CGPoint()

    override init(texture: SKTexture?, color: UIColor, size: CGSize) {
        super.init(texture: texture, color: color, size: size)
        self.setupKnob()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.setupKnob()
    }
    func setupKnob() {
        self.isUserInteractionEnabled = true
    }
    func rotateKnob(){
        let radians = atan2(fingerLocation.x - self.position.x, fingerLocation.y - self.position.y)
        self.zRotation = -radians
    }
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
        }
    }
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {

            fingerLocation = touch.location(in: self)
        }
        self.rotateKnob()
    }
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    }
    /*override func update(_ currentTime: TimeInterval) { //this is a SKScene function
     rotateKnob()
     }*/
}
like image 333
William Chadwick Avatar asked Mar 24 '18 01:03

William Chadwick


2 Answers

I usually do something like this without any jittering or jerking issues.

var fingerLocation = CGPoint()
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

    for touch: AnyObject in touches {
        fingerLocation = touch.location(in: self)


    }
}
func rotatePlayer(){
    let radians = atan2(fingerLocation.x - playerNode.position.x, fingerLocation.y - playerNode.position.y)
    playerNode.zRotation = -radians//this rotates the player
}

override func update(_ currentTime: TimeInterval) {

       rotatePlayer()


}

Depending on how your images are facing, you're probably going to have to mess around with the radians. In my case, my "player" image is facing upwards. Hope this helped.

like image 151
sicvayne Avatar answered Sep 29 '22 08:09

sicvayne


The Math was wrong. Here's what I learned you need: Mathematical formula for getting angles

How this looks in swift:

if point.x.sign == .minus {
    angle = atan(point.y/point.x) + CGFloat.pi/2
} else {
    angle = atan(point.y/point.x) + CGFloat.pi/2 + CGFloat.pi
}

Also, you have to get the coordinates of another object in the scene because the entire coordinate system rotates with the object:

let body = parent?.childNode(withName: "objectInScene")
let point = touch.location(in: body!)
like image 40
William Chadwick Avatar answered Sep 29 '22 08:09

William Chadwick