How to reference the node from within a custom SKAction Swift - swift

I can create a custom SKAction as follows:
extension SKAction {
class func move(from: CGPoint, halfWayTo: CGPoint, duration: TimeInterval) -> SKAction {
let midPoint = CGPoint(x: (from.x + halfWayTo.x)/2, y: (from.y + halfWayTo.y)/2 )
return SKAction.move(to: midPoint, duration: duration)
}
}
which I then use like this:
//Setup
let node = SKNode()
node.position = CGPoint(x: 50, y: 50)
let destination = CGPoint(x: 100, y: 100)
//Move from node's current position half way towards the destination
let action = SKAction.move(from: node.position, halfWayTo: destination, duration: 1)
node.run(action)
but in keeping with the standard:
SKAction.move(to: destination, duration: 1)
I would rather be able to say:
let action = SKAction.move(halfWayTo: destination, duration: 1)
node.run(action)
but I don't know how to refer to 'node' from inside the custom SKAction so that I can get it's position and calculate the halfway point?

You use self.
extension SKAction {
class func move(halfWayTo: CGPoint, duration: TimeInterval) -> SKAction {
let from = self.position
let midPoint = CGPoint(x: (from.x + halfWayTo.x)/2, y: (from.y + halfWayTo.y)/2 )
return SKAction.move(to: midPoint, duration: duration)
}
}
Then call with (as you said)
node.move(halfWayTo: B, duration: 1)

I'm still not sure whether the SKAction has a reference to the node it is applied to - even when it's running - or even if it does I'm not sure whether there's a way to access it. If anyone knows that it does please let me know.
I've thought of one approach as follows:
Instead of extending SKAction, extend SKNode as follows:
extension SKNode {
func move(halfWayTo: CGPoint, duration: TimeInterval) -> SKAction {
let from = self.position
let midPoint = CGPoint(x: (from.x + halfWayTo.x)/2, y: (from.y + halfWayTo.y)/2 )
return SKAction.move(to: midPoint, duration: duration)
}
}
and then use as follows:
//Setup
let node = SKNode()
node.position = CGPoint(x: 50, y: 50)
let destination = CGPoint(x: 100, y: 100)
//Move from node's current position half way towards the destination
let action2 = node.move(halfWayTo: destination, duration: 1)
node.run(action2)

Related

Using UIViewPropertyAnimator to drive animation along curved path?

I have an interruptible transition that is driven by UIViewPropertyAnimator and I would like to animate a view along a non linear path to its destination.
Doing some research I've found that the way to do this is by using:
CAKeyframeAnimation. However, I'm having issue coordinating the animations also I'm having issue with the CAKeyframeAnimation fill mode.
It appears to be reverse even though I've set it to forwards.
Below is the bezier path:
let bezierPath = self.generateCurve(from: CGPoint(x: self.itemStartFrame.midX, y: self.itemStartFrame.midY), to: CGPoint(x: self.itemEndFrame.midX, y: self.itemEndFrame.midY), bendFactor: 1, thickness: 1)
func generateCurve(from: CGPoint, to: CGPoint, bendFactor: CGFloat, thickness: CGFloat) -> UIBezierPath {
let center = CGPoint(x: (from.x+to.x)*0.5, y: (from.y+to.y)*0.5)
let normal = CGPoint(x: -(from.y-to.y), y: (from.x-to.x))
let normalNormalized: CGPoint = {
let normalSize = sqrt(normal.x*normal.x + normal.y*normal.y)
guard normalSize > 0.0 else { return .zero }
return CGPoint(x: normal.x/normalSize, y: normal.y/normalSize)
}()
let path = UIBezierPath()
path.move(to: from)
let midControlPoint: CGPoint = CGPoint(x: center.x + normal.x*bendFactor, y: center.y + normal.y*bendFactor)
let closeControlPoint: CGPoint = CGPoint(x: midControlPoint.x + normalNormalized.x*thickness*0.5, y: midControlPoint.y + normalNormalized.y*thickness*0.5)
let farControlPoint: CGPoint = CGPoint(x: midControlPoint.x - normalNormalized.x*thickness*0.5, y: midControlPoint.y - normalNormalized.y*thickness*0.5)
path.addQuadCurve(to: to, controlPoint: closeControlPoint)
path.addQuadCurve(to: from, controlPoint: farControlPoint)
path.close()
return path
}
Path visualization:
let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 2
containerView.layer.addSublayer(shapeLayer)
Here's the CAKeyframeAnimation that resides inside of the UIViewPropertyAnimator
let curvedPathAnimation = CAKeyframeAnimation(keyPath: "position")
curvedPathAnimation.path = bezierPath.cgPath
curvedPathAnimation.fillMode = kCAFillModeForwards
curvedPathAnimation.calculationMode = kCAAnimationPaced
curvedPathAnimation.duration = animationDuration
self.attatchmentView.layer.add(curvedPathAnimation, forKey: nil)
So far this animation along the correct path, however the animation reverse back to it's starting position and is not in sync with the property animator.
Any suggestions?

How to create realistic spinning wheel in SpriteKit

I am trying to create a spinning fortune wheel action via SKAction. I have a SKNode which used as wheel, this SKNode is a circle that divided to four quarters (each quarter in different color). also I set an SKAction (which is repeating for 10 counts) that spin the SKNode around fixed point (the node's center). The problem is that the action is running well but it stops suddenly and not slowing down - like a real wheel. I don't really have an idea how to set this animation, I mean to slow the spinning down before the action is stop.
Here is my code so far:
class GameScene: SKScene {
let colors = [SKColor.yellow, SKColor.red, SKColor.blue, SKColor.purple]
override func didMove(to view: SKView) {
createWheel()
let sq = CGRect(x: size.width/2, y: size.height/2, width: 300, height: 300)
let sqx = SKShapeNode(rect: sq)
sqx.lineWidth = 2
sqx.fillColor = .clear
sqx.setScale(1.0)
addChild(sqx)
}
func createWheel() {
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 0, y: -200))
path.addArc(withCenter: CGPoint.zero,radius: 200,startAngle: CGFloat(0.0), endAngle: CGFloat(3.0 * Double.pi / 2),clockwise: false)
path.addLine(to: CGPoint(x: 200, y: 0))
let obstacle = obstacleByDuplicatingPath(path, clockwise: true)
obstacle.position = CGPoint(x: size.width/2, y: size.height/2)
addChild(obstacle)
let rotateAction = SKAction.rotate(byAngle: CGFloat((3.0 * CGFloat(Double.pi / 2)) - 90), duration: 0.5)
//obstacle.run(SKAction.repeatForever(rotateAction))
obstacle.run(SKAction.repeat(rotateAction, count: 10))
}
func obstacleByDuplicatingPath(_ path: UIBezierPath, clockwise: Bool) -> SKNode {
let container = SKNode()
var rotationFactor = CGFloat(Double.pi / 2)
if !clockwise {
rotationFactor *= -1
}
for i in 0...3 {
let section = SKShapeNode(path: path.cgPath)
section.fillColor = colors[i]
section.strokeColor = colors[i]
section.zRotation = rotationFactor * CGFloat(i);
let origin = CGPoint(x: 0.0, y: 0.0)
switch i {
case 0:
section.position = CGPoint(x: (origin.x + 10), y: (origin.y - 10))
case 1:
section.position = CGPoint(x: (origin.x + 10), y: (origin.y + 10))
case 2:
section.position = CGPoint(x: (origin.x - 10), y: (origin.y + 10))
case 3:
section.position = CGPoint(x: (origin.x - 10), y: (origin.y - 10))
default:
print("bolbol")
}
container.addChild(section)
}
return container
}
}
edit:
I was thinking about it and I tried to do it via SKAction, I set another action but this time I set their duration to a long one. first it run a action of duration 0.5, then of 2 and at end of 4. I looks pretty good but still not smooth as I want it to be.
here is my code:
let rotateAction = SKAction.rotate(byAngle: CGFloat(2.0 * CGFloat(M_PI)), duration: 0.5)
let rotateAction2 = SKAction.rotate(byAngle: CGFloat(2.0 * CGFloat(M_PI)), duration: 2)
let rotateAction3 = SKAction.rotate(byAngle: CGFloat(2.0 * CGFloat(M_PI)), duration: 4)
let wait = SKAction.wait(forDuration: 5)
let g1 = SKAction.repeat(rotateAction, count: 10)
let group = SKAction.group([wait, g1, rotateAction2, rotateAction3])
what do you think? there is any way to do it better??
edit 2:
Continued to #Ali Beadle answer, I tried to do it via physics body, the problem now is the when I drag finger on the screen the SKShapeNode (shape) in continue to rotate and never stops. can you detect what is wrong?
class GameScene: SKScene {
var start: CGPoint?
var end:CGPoint?
var startTime: TimeInterval?
let shape = SKShapeNode.init(rectOf: CGSize(width: 150, height: 150))
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector(dx: 0, dy: -9.8)
let sceneBody = SKPhysicsBody.init(edgeLoopFrom: self.frame)
sceneBody.friction = 0
self.physicsBody = sceneBody
shape.fillColor = SKColor.red
shape.position = CGPoint(x: self.size.width/2, y: self.size.height/2)
shape.physicsBody = SKPhysicsBody.init(rectangleOf: CGSize(width: 50, height: 50))
shape.physicsBody?.affectedByGravity = false
shape.physicsBody?.isDynamic = true
addChild(shape)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {return}
self.start = touch.location(in: self)
self.startTime = touch.timestamp
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {return}
self.end = touch.location(in: self)
var dx = ((self.end?.x)! - (self.start?.x)!)
var dy = ((self.end?.y)! - (self.start?.y)!)
let magnitude:CGFloat = sqrt(dx*dx+dy*dy)
if(magnitude >= 25){
let dt:CGFloat = CGFloat(touch.timestamp - self.startTime!)
if dt > 0.1 {
let speed = magnitude / dt
dx = dx / magnitude
dy = dy / magnitude
print("dx: \(dx), dy: \(dy), speed: \(speed) ")
}
}
let touchPosition = touch.location(in: self)
if touchPosition.x < (self.frame.width / 2) {
self.shape.physicsBody?.angularVelocity = 10
self.shape.physicsBody?.applyAngularImpulse(-180)
} else {
self.shape.physicsBody?.angularVelocity = 10
self.shape.physicsBody?.applyAngularImpulse(180)
}
}}
I have created an open source prize spinning wheel in Spritekit that uses physics for realistic movement and flapper control. It also allows the user to drag the wheel to spin or generates a random spin by pushing the center of the wheel.
https://github.com/hsilived/SpinWheel
You can add realistic movement like this by using the built-in Physics simulation of SpriteKit. This will allow you to give your wheel a mass and friction and then use forces to rotate it. It will then slow down realistically.
In outline see Simulating Physics in the Apple Documentation:
To use physics in your game, you need to:
Attach physics bodies to nodes in the node tree and configure their physical properties. See SKPhysicsBody.
Define global characteristics of the scene’s physics simulation, such as gravity. See SKPhysicsWorld.
Where necessary to support your gameplay, set the velocity of physics bodies in the scene or apply forces or impulses to them. ...
The most appropriate method for your wheel is probably to make the wheel pinned to the scene and then rotate it with applyAngularImpulse.

Changing speed of node / Swift 3.0

I am trying to change the speed of my node. Right now my node(s) just move around the screen wherever you touch - very basic for testing. I just wanted my first node (bubble1 - refer to code) to move a little quicker so I can change the speed again and so on.. Thanks for your help.
*For now I just want to change the speed of "bubble1", nothing else. I looked at other posts on how to do this, but none very really up to date. So I posted my own. =)
import SpriteKit
import GameplayKit
class GameScene: SKScene {
//var simpleS = SKSpriteNode()
var ball = SKSpriteNode(imageNamed: "Ball")
var bubble = SKSpriteNode(imageNamed: "square")
var bubble1 = SKSpriteNode(imageNamed: "square")
var bubble2 = SKSpriteNode(imageNamed: "square")
var bubble3 = SKSpriteNode(imageNamed: "square")
var bubble4 = SKSpriteNode(imageNamed: "square")
override func didMove(to view: SKView) {
//simpleS = self.childNode(withName: "simpleS") as! SKSpriteNode
//simpleS.physicsBody?.affectedByGravity = true
//simpleS.setScale(3.0)
ball.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
ball.setScale(0.1)
//self.addChild(ball)
bubble.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
bubble.setScale(0.3)
self.addChild(bubble)
bubble1.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
bubble1.setScale(0.3)
self.addChild(bubble1)
bubble2.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
bubble2.setScale(0.3)
self.addChild(bubble2)
bubble3.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
bubble3.setScale(0.3)
self.addChild(bubble3)
bubble4.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
bubble4.setScale(0.3)
self.addChild(bubble4)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
//simpleS.run(SKAction.moveTo(x: location.x, duration: 0.5))
//simpleS.run(SKAction.moveTo(y: location.y, duration: 0.5))
ball.run(SKAction.moveTo(x: location.x, duration: 0.0))
ball.run(SKAction.moveTo(y: location.y, duration: 0.0))
bubble.run(SKAction.moveTo(x: location.x, duration: 0.3))
bubble.run(SKAction.moveTo(y: location.y, duration: 0.3))
bubble1.run(SKAction.moveTo(x: location.x, duration: 0.4))
bubble1.run(SKAction.moveTo(y: location.y, duration: 0.4))
bubble2.run(SKAction.moveTo(x: location.x, duration: 0.5))
bubble2.run(SKAction.moveTo(y: location.y, duration: 0.5))
bubble3.run(SKAction.moveTo(x: location.x, duration: 0.6))
bubble3.run(SKAction.moveTo(y: location.y, duration: 0.6))
bubble4.run(SKAction.moveTo(x: location.x, duration: 0.8))
bubble4.run(SKAction.moveTo(y: location.y, duration: 0.8))
}
}
There are a couple of ways you can do this.
Change the duration, as this will speed it up, but I don't think this is what you're asking. I think you want to know how to speed up an SKAction....
Change the rate of execution of a SKAction via its speed property
The speed property is documented here.
You'll need to do a couple of things to make this value accessible and uniquely addressable for bubble1
Make the SKAction on bubble1 unique, so you can control it. There's two ways to to this:
give it a unique name in its '.name' property, look up and find that
have a reference to bubble1 and address its SKAction (a
Tell the SKAction affecting bubble1 to run faster by changing its .speed property
Or... you can be lazy and just adjust the speed value of all actions on any given node, with this: https://developer.apple.com/reference/spritekit/sknode/1483036-speed

How to pass the NSTimer as parameter in function in SpriteKit swift

I have a simple function to move my sprite for predefine time duration. I want that the time duration which i pass in to function as time duration is passing in function parameter as NSTimer. My function is as below!
func moveGooses(){
let path = UIBezierPath()
path.moveToPoint(CGPoint(x:self.parentScene.frame.width*0.99 , y: self.parentScene.frame.height*0.90))
path.addCurveToPoint(CGPoint( x:self.parentScene.frame.width*0.01 , y: self.parentScene.frame.height*0.90), controlPoint1: CGPoint(x: self.parentScene.frame.width*0.01, y: self.parentScene.frame.height*0.90) , controlPoint2: CGPoint(x: self.parentScene.frame.width*0.01 , y: self.parentScene.frame.height*0.90))
let anim = SKAction.followPath(path.CGPath, asOffset: false, orientToPath: false, duration: 3.0)
let seq = SKAction.sequence([anim])
let removeAction = SKAction.runBlock{
let myGameScene = self.parentScene as GameScene
self.removeFromParent()
}
self.runAction(SKAction.sequence([seq,removeAction]))
}
Your question is vague but I believe this is what you're trying to do according to question title:
func timerAsParam(timer: NSTimer) {
//timer is a local nstimer
}

Random movement of sprite

I'm new here so i apologize if i've entered my question wrongly. That said, i'm having an issue making my sprite move into random locations around the screen. here is my code
func random() ->CGFloat{
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(#min: CGFloat, max:CGFloat) ->CGFloat{
return random()*(max-min)+min
}
dino.position = CGPoint(x: size.width + dino.size.width/2, y: actualY)
// Add the monster to the scene
addChild(dino)
// Determine speed of the monster
let actualDuration = random(min: CGFloat(3.0), max: CGFloat(4.0))
let randomNum = CGPoint(x:Int (arc4random()%1000), y:Int (arc4random()%1000))
// Create the actions
var actionMove = SKAction:CGPoint(CGPoint(x:randomNum, y:randomNum)), duration:NSTimeInterval;(actualDuration)
let actionMoveDone = SKAction.removeFromParent()
dino.runAction(SKAction.sequence([actionMove, actionMoveDone]))
thank you for helping, anything helps at this point
The function you are using is not valid. Use SKAction.moveTo instead.
dino.position = CGPoint(x: size.width + dino.size.width/2, y: 10)
// Add the monster to the scene
addChild(dino)
// Determine speed of the monster
let actualDuration = NSTimeInterval(random(min: CGFloat(3.0), max: CGFloat(4.0)))
let randomNum = CGPoint(x:Int (arc4random() % 1000), y:Int (arc4random() % 1000))
var actionMove = SKAction.moveTo(randomNum, duration: actualDuration) // Changed line
let actionMoveDone = SKAction.removeFromParent()
dino.runAction(SKAction.sequence([actionMove, actionMoveDone]))