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.