Implementing an advanced rotation behaviour with Swift and SpriteKit

237 views Asked by At

I'm a bit stuck with implementing the following behaviour for the triangle using Xcode, Swift and SpriteKit:

Angle behaviour example

The triangle should turn towards the ball, once the ball enters the screen. Once the triangles orientation lines up with the ball, its orientation follows the ball until the ball completely left the screen. Then the triangle either returns to its original position (if there isn't another ball inside the screen again) or it orientates towards the next ball.

The code for following the ball (images 3 and 4) should be fine:

func updateAngle(from triangle: CGPoint, to ball: CGPoint) {
  let deltaX = ball.x - triangle.x
  let deltaY = ball.y - triangle.y
  let angle =  atan2(deltaY, deltaX)

  triangle.zRotation = angle - 90 * degreesToRadians
}

I tried matching the position the ball reached after a certain amount of time with an SKAction rotating the triangle and then follow it, but the ball shouldn't have to move at a constant speed. So that doesn't work.

Being able to EaseIn (and EaseOut) the triangle's rotation when turning towards the ball initially would be the icing on the cake.

Any ideas much appreciated.

1

There are 1 answers

0
nontomatic On

Someone posted a solution earlier, but now, as I wanted to accept it, it seems to be deleted.

In the meantime I came up with my own solution anyway which seems to implement the behaviour I described in my question just fine:

import SpriteKit

// Constants
let screenWidth = UIScreen.mainScreen().bounds.width
let screenHeight = UIScreen.mainScreen().bounds.height

let pi = CGFloat(M_PI)
let degreesToRadians = pi / 180


class GameScene: SKScene {

  var triangle = Triangle()

  override func didMoveToView(view: SKView) {
    /* Setup your scene here */
    scaleMode = .ResizeFill

    addChild(triangle)
  }

  override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
    let circle = Circle()
    addChild(circle)

    circle.move {
      circle.removeFromParent()
      self.triangle.state = .Releasing
      println("Releasing")
    }
  }

  func updateAngle(from origin: CGPoint, to target: CGPoint) {
    let deltaX = target.x - origin.x
    let deltaY = target.y - origin.y
    let angle =  atan2(deltaY, deltaX)

    switch triangle.state {
    case .Locked:
      let x: CGFloat = CGFloat(-M_PI)
      triangle.zRotation = angle - 90 * degreesToRadians
    case .Acquiring, .Releasing:
      let angleDifference: CGFloat = (angle - triangle.zRotation) * degreesToRadians
      let ease: CGFloat = 0.7
      triangle.zRotation += angleDifference * ease
    default:
      break
    }

    if triangle.zRotation >= angle - 90 * degreesToRadians {
      switch triangle.state {
      case .Acquiring:
        triangle.state = .Locked
        println("Locked")
      case .Releasing:
        triangle.state = .Ready
        println("Ready")
      default:
        break
      }
    }
  }

  override func update(currentTime: CFTimeInterval) {
    /* Called before each frame is rendered */

    if let circle = childNodeWithName("circle") as? Circle {
      if circle.position.x < screenWidth + circle.size.width/2 && circle.position.x > -circle.size.width/2 {
        switch triangle.state {
        case .Ready, .Releasing:
          triangle.state = .Acquiring
          println("Acquiring")
        case .Acquiring, .Locked:
          updateAngle(from: triangle.position, to: circle.position)
        }
      }
    }
    if triangle.state == .Releasing {
      updateAngle(from: triangle.position, to: CGPoint(x: triangle.position.x, y: screenHeight/4))
    }
  }
}


enum TriangleState {
  case Ready, Acquiring, Locked, Releasing
}

class Triangle: SKSpriteNode {

  var state: TriangleState

  init() {
    state = .Ready
    let texture = SKTexture(imageNamed: "Spaceship.png")
    super.init(texture: texture, color: nil, size: texture.size())

    setScale(0.25)
    position = CGPointMake(screenWidth / 2.0, screenHeight / 2.0)
    zRotation = CGFloat(-M_PI)
  }

  required init?(coder aDecoder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
  }
}


class Circle: SKSpriteNode {

  init() {
    let texture = SKTexture(imageNamed: "Spaceship.png")
    super.init(texture: texture, color: nil, size: texture.size())

    name = "circle"
    setScale(0.25)
    position = CGPointMake(screenWidth * 1.3, screenHeight / 4.0)
  }

  required init?(coder aDecoder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
  }

  func move(completion: () -> ()) {
    let move = SKAction.moveTo(CGPointMake(-size.width/2, screenHeight / 4.0), duration: 6.0)
    runAction(move, completion: completion)
  }
}

Upon touch the target object will appear and move, while the triangle follows along and releases to its initial position, once the target is gone. On top of that, if there is another target object available, the triangle will acquire that new target.

I differentiated between a .Ready and .Releasing state, so that different additional behaviour can be implemented.

Please feel free to try out and let me know if you come across a bug.