michael.yql
michael.yql

Reputation: 33

How can I properly rotate SKSpriteNodes about a point other than the origin?

The Problem

I have a set of SKSpriteNodes within a GameScene that are positioned in an orderly manner i.e. their positions are constantly spaced e.g. (0, 0), (0, 150), (0, 300). I want to be able to rotate all of the ships about the centroid of their positions and have their heading point to the direction in which they are being rotated to. Please take a look at the diagram below to understand what I mean:

enter image description here

What I've Tried

I have tried putting all of the sprite nodes within a container SKNode and rotating the SKNode. This causes the children to rotate as desired since they are all positioned relative to the parent node. However, I want to avoid doing this as I want to have control over each of the individual sprite node's zRotation and position instead of 'cheating' and having a container node do it for me.

To fix this, I have tried applying the standard 2D rotation matrix to the positions of the SKSpriteNodes. The code below is not an exact replica, but it roughly describes what I've tried to do:

@objc func sliderValueDidChange(_ sender: UISlider) { 

    // just assume I have access to the GameScene from the GameViewController
    guard let scene = view.scene as? GameScene else { return } 
    let nodes = scene.nodes 

    // prevSliderValue is the value of the slider from the previous call to 
    // this function. the slider has a range of values from 0 to 359
    let angleChange = (prevSliderValue - CGFloat(sender.value)) / 180 * .pi

    // calculating the centroid
    var cenX = CGFloat.zero
    var cenY = CGFloat.zero
    // nodes is a list of [SKSpriteNodes]
    for node in nodes {    
        cenX += node.position.x
        cenY += node.position.y
    }
    cenX = cenX / CGFloat(nodes.count)
    cenY = cenY / CGFloat(nodes.count)
    
    // applying the rotation matrix to each node's position
    for node in nodes { 
        // calculating the new position
        let newX = node.position.x * cos(angleChange) - node.position.y * sin(angleChange) 
        let newY = node.position.x * sin(angleChange) + node.position.y * cos(angleChange) 
        node.position = CGPoint(x: newX, y: newY)

        // calculating the new angle to point at 
        // let a = -atan2(node.position.y - cenY, node.position.x - cenX) + .pi/2
        // node.zRotation = a
        /*
         atan2 gives the angle relative to the X axis, but SKSpriteNodes  
         zRotation are relative to the Y axis, hence I'm adding pi/2 
         furthermore, I want a positive angle of rotation to be clockwise,
         so I'm negating the return value of the atan2 function
        */ 
    }
}

The result of this code has so far been unsuccessful. The nodes are roughly able to rotate clockwise to about 180º along an arc relative to the centroid. But for any value past 180º, the nodes basically stop rotating/moving and just spasm on the spot. Trying to reverse the rotation (i.e. going from 180º back to 0º) does cause the nodes to rotate counterclockwise, but they don't reach to their original spot (i.e. their ending position can be a few degrees off from perfectly vertical). Furthermore, if I drag the slider up and down a few times, for whatever reason which eludes me, the nodes end up moving closer and closer until they converge on the centroid.

Is there something wrong with how I'm calculating the final positions? I've realized that my calculation of newX and newY aren't relative to the centroid but instead are relative to the origin, which messes up my brain even more. I've been stuck on this for a few days and I can already feel myself starting to burn out. Any help or pointers would be greatly appreciated.

EDIT

I was able to get it working without using a container node. Here's the rough code:

    @objc func sliderValueDidChange(_ sender: UISlider) {
        
        guard let scene = view.scene as? GameScene else { return }
        let nodes = scene.nodes
        
        let angleChange = (prevSliderVal - CGFloat(sender.value)) / 180 * .pi
        var cenX = CGFloat.zero
        var cenY = CGFloat.zero
        for node in nodes {
            cenX += node.position.x
            cenY += node.position.y
        }
        cenX = cenX / CGFloat(nodes.count)
        cenY = cenY / CGFloat(nodes.count)
        
        for node in nodes {
            let translatedPos = CGPoint(x: node.position.x - cenX,
                                        y: node.position.y - cenY)
            let newX = translatedPos.x * cos(angleChange) - translatedPos.y * sin(angleChange)
            let newY = translatedPos.x * sin(angleChange) + translatedPos.y * cos(angleChange)
            node.position = CGPoint(x: newX, y: newY)
            
            let a = CGFloat(sender.value) / 180 * .pi
            node.zRotation = -a
            
        }
        
        prevSliderVal = CGFloat(sender.value)
    }

Looks like I was overcomplicating the problem in my head. In order to rotate the sprites about a point other than the origin, I just need to translate the node's positions such that they are relative to the centroid, then apply the rotation matrix.

Upvotes: 0

Views: 72

Answers (1)

Max Gribov
Max Gribov

Reputation: 398

Looks like you need origin property of SKSpriteNode. Origin defines center for rotation, translation and scale.

For example, if you need to rotate your sprite around top left corner, you can just do this:


let sprite = SKSpriteNode()

// top left corner
sprite.origin = CGPoint(x: 0, y: 0)
sprite.zRotation = CGFloat(90) / 180 * .pi

If you want to rotate group of sprites around one point you can make them as a childs of other empty sprite and rotate parent sprite:


let arrowSprites = [SKSpriteNode(imageNamed: "Arrow"), 
                    SKSpriteNode(imageNamed: "Arrow"), 
                    SKSpriteNode(imageNamed: "Arrow")]

let groupSprite = SKSpriteNode()
arrowSprites.enumerated().ForEach { (index, sprite) in

  sprite.position = CGPoint(x: 0, y: CGFloat(index) * 100)
  groupSprite.addChild(sprite)
}

// bottom center
groupSprite.origin = CGPoint(x: 0.5, y: 1)
groupSprite.zRotation = CGFloat(90) / 180 * .pi

More advanced techniques (like flocking behavior for group of sprites) you can achieve by using GameplayKit API. Read about GKAgent and GKBehavior, and how it can be used to control groups of sprites.

Upvotes: 1

Related Questions