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

2

6

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://mcmap.net/q/1776199/-how-could-i-skew-shear-a-sprite-via-spritekit-like-cocos2d 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()
     }*/
}
Republican answered 24/3, 2018 at 1:59 Comment(15)
Sorry if this is a dumb question, but have you looked into rotating the sprite manually? If you're using the physics engine to try to follow a finger around, I can see why that wouldn't work. You don't want the physics engine. And now that I think of it, you probably don't want to use SKAction either.Regelate
In case I wasn't clear, by rotating the sprite manually, I mean setting the rotation property of the sprite directly, without the transforms and stuff.Regelate
@GreatBigBore, thanks for your reply. I will try that now.Republican
would it be possible to show us a video of the behavior?Hicks
I meant to show us a video of how the behavior of when it "jerks around"Hicks
This is an excellent question about an interesting problem. As soon as the 48 hours from initial posting is up, I can add a bounty, and will. There's more than one way to do this, and I've no idea which is best.Natividadnativism
Since the release of the iPad, synthesizer apps have been trying to solve this problem in different ways, with varying degrees of success [please excuse the pun]. Perhaps the best, from a user experience perspective, is Thor: propellerheads.se/thorNatividadnativism
In Thor, the further your finger wanders from the knob/dial, in a lateral position, the greater the granularity of input/influence on the knob in vertical input movements.Natividadnativism
here's a video: youtu.be/eKVfC8j2kWwRepublican
@GreatBigBore, did you mean using .zRotation on the SKSpriteNode? Because that did not work either.Republican
@WilliamChadwick Yeah, that's what I was talking about. I'd be interested to know what "didn't work" means. Please, add some details to your question about your results from zRotation--someone might find a clue in there.Regelate
@WilliamChadwick Your dx and dy calculations seem strange. I have an app that's doing stuff during touchesMoved(). In touchesBegan() I calculate my sprite-to-mouse offset with offset = mousePosition - centerOfSpritePosition, and in touchesMoved() I set the effective offset with mousePosition + offset. Also, my zRotation is in radians.Regelate
@GreatBigBore, is offset a CGPoint?Republican
@WilliamChadwick Yes. But I didn't mean for you to copy and paste all that; I was just pointing out that your math seemed strange when you calculate dx/dy. I figured you could just change your math to be similar to mine.Regelate
Let us continue this discussion in chat.Republican
R
4

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!)
Republican answered 25/5, 2018 at 15:57 Comment(1)
The Swift code works great. The Math... I don't know if I copied it from my friend's scribbles correctly.Republican
H
3

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.

Hicks answered 24/3, 2018 at 2:33 Comment(1)
This is code for the SKScene and my code is in the SKSpriteNode. I've adapted it by using the touchesEnded override to call rotatePlayer, but it still doesn't work consistently.Republican

© 2022 - 2024 — McMap. All rights reserved.