In Swift 4.2 - How can I animate a circle in SpriteKit - swift

I am able to draw a circle at the collision point of two objects, but I want to create a nice animation for it (clockwise). I understood how to do it in single view application but I can't use the same implementation in SpriteKit. How can I do it in SpriteKit?
This is my code for adding a circle -
func drawCircleOnCollisionPoint(arrowName:String) {
let circle:SKShapeNode = SKShapeNode(circleOfRadius: 15)
circle.position = (arrowsCollidedWithTarget[arrowName]?.contactPoint)!
circle.strokeColor = SKColor.red
circle.glowWidth = 1.0
circle.fillColor = SKColor.clear
circle.zPosition = 50
self.addChild(circle)
DispatchQueue.main.asyncAfter(deadline: .now() + timeBetweenGames) {
circle.removeFromParent()
}
}

Try this:
let action = SKAction.rotate(byAngle: -360, duration: 30)
circle.run(SKAction.repeatForever(action))

Related

How to change color of SKShapeNode randomly each time spawned?

Currently, when running this code, a colored ball runs from the top of the screen to the bottom. When it reaches the bottom it respawns at the top and drops again. I need the ball to change to a random color each time it spawns again. I have a colorChange action but it still doesn't change the color of the ball when it runs.
import SpriteKit
class GameScene: SKScene {
let ball = SKShapeNode(circleOfRadius: 20)
let label = SKLabelNode(fontNamed: "Futura")
let colorChange = SKAction.colorizeWithColor(.blueColor(), colorBlendFactor: 1, duration: 1)
let randomRed = Int(arc4random_uniform(255))
let randomGreen = Int(arc4random_uniform(255))
let randomBlue = Int(arc4random_uniform(255))
override func didMoveToView(view: SKView) {
let sceneBody = SKPhysicsBody(edgeLoopFromRect: self.frame)
self.physicsBody = sceneBody
//Ball Transition
let ballTransition = SKAction.sequence([SKAction.fadeInWithDuration(1)])
ball.runAction(ballTransition)
ball.fillColor = UIColor(red: 100, green: 100, blue: 0, alpha: 1)
ball.physicsBody = SKPhysicsBody(circleOfRadius: 25)
ball.physicsBody?.affectedByGravity = false
ball.position = CGPoint(x: self.frame.size.width/2, y: CGFloat(self.frame.size.height*1))
ballMovement()
self.addChild(ball)
self.addChild(label)
}
func ballMovement() {
let moveBall = SKAction.moveToY(0, duration: 3)
let goBackUp = SKAction.moveToY(self.frame.size.height, duration:0)
let keepFalling = SKAction.sequence([moveBall, goBackUp, colorChange])
ball.runAction(SKAction.repeatActionForever(keepFalling))
//Label Sprite
label.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
label.fontColor = SKColor.redColor()
label.fontSize = 30
}
override func update(currentTime: CFTimeInterval) {
label.text = "\(ball.position.y)"
if ball.position.y < 26 {
ball.position = CGPoint(x: self.frame.size.width/2, y: CGFloat(self.frame.size.height*1))
}
}
}
Instead of using an SKAction to change the color, just do
ball.color = UIColor...
To generate a random color check this out: How to make a random background color with Swift
and use that to generate a random color
ball.color = .randomColor()
I suggest you don't use an SKShape for an important piece that moves because I find that they can slow down the app and cause problems. I would use an SKSpritenode instead for the ball. If you then make the ball default as all white, you can change the colour by doing this:
ball.colorBlendFactor = 1
ball.color = SKColor(hue: randomNumberFunctionBetweenZeroAndOne(), saturation: 0.75, contrast: 0.5)
The hue should be a random value between 0 and 1.
If you put this inside a function, you could call the function to change the colour of the ball inside and SKAction or anywhere else.

How to scale a radius to fit in all devices?

I am makeing a game in with a ball follows a path, actually is just an illution because the scene is the one that rotates. The problem is that in some devices the ball get out of the screen and in others the radius looks very small. I have tryed to make the radius equal (orbita.size.width / 2) but it doesnt work. (orbita is the orbit that the ball follows)
class GameScene: SKScene {
let sprite = SKSpriteNode(imageNamed: "circulo")
var rotation:CGFloat = CGFloat(M_PI)
let radius:CGFloat = 168
override func didMoveToView(view: SKView) {
/* Setup your scene here */
scaleMode = .ResizeFill
node.position = view.center
// 3) Add the container to the scene
addChild(node)
// 4) Set the sprite's x position
sprite.position = CGPointMake(radius, 0)
// 5) Add the sprite to the container
node.addChild(sprite)
// 6) Rotate the container
rotate()
sprite.color = UIColor.whiteColor()
sprite.colorBlendFactor = 1.0
sprite.zPosition = 4.0
orbita = SKSpriteNode(imageNamed: "orbita")
let padding2:CGFloat = 32.0
orbita.size = CGSize(width:view.frame.size.width - padding2 , height: view.frame.size.width - padding2)
orbita.color = UIColor.whiteColor()
orbita.colorBlendFactor = 1
orbita.alpha = 1
orbita.position = view.center
self.addChild(orbita)
orbita.zPosition = 3
}
If your orbita.size is (it seems a circular orbita by "circulo" name):
orbita.size = CGSize(width:view.frame.size.width - padding2 , height: view.frame.size.width - padding2)
your radius would be:
let radius:CGFloat = (view.frame.size.width - padding2)/2

How to animate the end and the control points SWIFT

I have a CAShapeLayer im my view which has the to CGPath casted path of an instance of an Bezierpath. I now want to animate the end and the two control points so e.g. the end of that line is moving(not stopping). How to do that? I looked at CABasicAninmations but did not get how the access the end point. The same with animate with duration. How to make this happen for an endlessness time.
You can use a CADisplayLink (it's like a NSTimer, except optimally timed for animation frame updates) and update the path of the CAShapeLayer.
For example:
var displayLink: CADisplayLink?
var startTime: CFAbsoluteTime?
let duration = 2.0
var shapeLayer: CAShapeLayer?
override func viewDidLoad() {
super.viewDidLoad()
shapeLayer = CAShapeLayer()
shapeLayer?.fillColor = UIColor.clearColor().CGColor
shapeLayer?.strokeColor = UIColor.blueColor().CGColor
shapeLayer?.lineWidth = 10
shapeLayer?.frame = view.bounds
view.layer.addSublayer(shapeLayer!)
startTime = CFAbsoluteTimeGetCurrent()
displayLink = CADisplayLink(target: self, selector: "handleDisplayLink:")
displayLink?.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
func handleDisplayLink(displayLink: CADisplayLink) {
let elapsed = CFAbsoluteTimeGetCurrent() - startTime!
let percent = (elapsed % duration) / duration
let path = UIBezierPath()
let startPoint = CGPoint(x: 0, y: view.bounds.size.height / 2.0)
let controlPoint = CGPoint(x: view.bounds.size.width / 20, y: view.bounds.size.height * CGFloat(0.5 + sin(percent * M_PI * 2.0) / 2.0))
let endPoint = CGPoint(x: view.bounds.size.width - 1, y: view.bounds.size.height / 2.0)
path.moveToPoint(startPoint)
path.addQuadCurveToPoint(endPoint, controlPoint: controlPoint)
shapeLayer?.path = path.CGPath
}
That yields:
Here are some possibilities.
Configure the animation in advance, that is, by generating a series of drawings, just like the cells of a cartoon, and use simple UIImageView animation of those images.
Use CAShapeLayer and animate between two path values. Unfortunately this doesn't give you total control over the intermediate frames, but you could use keyframe animation to supply intermediate frames and thus provide additional control.
Animate the actual points of the drawing. This is the hardest to configure, but it is true animation and it gives you total control over the animation. That's what I'm doing here: I did it by creating a custom animatable property - in this case, a property representing the x-position of the bottom point of the triangle. You'd have to do that for all the points you want to animate.

Swift: how to invalidate timer on animated SKShapenode countdown?

Alright I'm building a sprite kit game and on one of my previous questions, I got the code that lets a circle skshapenode "unwind" in accordance with a timer that runs out (so that the circle has disappeared by the time the timer is done).
Here was my question: Swift, sprite kit game: Have circle disappear in clockwise manner? On timer?
And here is the code that I've been using:
func addCircle() {
circle = SKShapeNode(circleOfRadius: 50)
let circleColor = UIColor(red: 102/255, green: 204/255, blue: 255/255, alpha: 1)
circle.fillColor = circleColor
circle.strokeColor = SKColor.clearColor()
circle.position = CGPoint (x: self.frame.size.width * 0.5, y: self.frame.size.height * 0.5+45)
circle.zPosition = 200
circle.zRotation = CGFloat(M_PI_2)
addChild(circle)
countdown(circle, steps: 120, duration: 5) { //change steps to change how much of the circle it takes away at a time
//Performed when circle timer ends:
print("circle done")
self.gameSoundTrack.stop()
self.removeAllChildren()
self.removeAllActions()
self.viewController.removeSaveMe(self)
self.GOSceneNodes() //Implements GOScene
self.viewController.addGOScene(self) //Implements GOViewController
}
}
// Creates an animated countdown timer
func countdown(circle:SKShapeNode, steps:Int, duration:NSTimeInterval, completion:()->Void) {
let radius = CGPathGetBoundingBox(circle.path).width/2
let timeInterval = duration/NSTimeInterval(steps)
let incr = 1 / CGFloat(steps)
var percent = CGFloat(1.0)
let animate = SKAction.runBlock({
percent -= incr
circle.path = self.circle(radius, percent:percent)
})
let wait = SKAction.waitForDuration(timeInterval)
let action = SKAction.sequence([wait, animate])
runAction(SKAction.repeatAction(action,count:steps-1)) {
self.runAction(SKAction.waitForDuration(timeInterval)) {
circle.path = nil
completion()
}
}
}
// Creates a CGPath in the shape of a pie with slices missing
func circle(radius:CGFloat, percent:CGFloat) -> CGPath {
let start:CGFloat = 0
let end = CGFloat(M_PI*2) * percent
let center = CGPointZero
let bezierPath = UIBezierPath()
bezierPath.moveToPoint(center)
bezierPath.addArcWithCenter(center, radius: radius, startAngle: start, endAngle: end, clockwise: true)
bezierPath.addLineToPoint(center)
return bezierPath.CGPath
}
This all works well, however I need a way to invalidate the countdown for the circle if another button within the game is pressed so that everything that is performed when the timer runs out (everything after //Performed when circle timer ends: above) is NOT performed.
I have tried
circle.removeAllActions()
circle.removeFromParent()
when the button is pressed, however this just removes the circle and everything is still executed even though the circle is gone.
I'm pretty new to Swift and I can't figure out what part of those 3 functions I need to invalidate or stop in order to stop the countdown from finishing. How can I accomplish this?
You can stop an action by adding a key to the action and then run
removeActionForKey("actionKey")
To add a key to the action, replace the countdown method with the following:
// Creates an animated countdown timer
func countdown(circle:SKShapeNode, steps:Int, duration:NSTimeInterval, completion:()->Void) {
let radius = CGPathGetBoundingBox(circle.path).width/2
let timeInterval = duration/NSTimeInterval(steps)
let incr = 1 / CGFloat(steps)
var percent = CGFloat(1.0)
let animate = SKAction.runBlock({
percent -= incr
circle.path = self.circle(radius, percent:percent)
})
let wait = SKAction.waitForDuration(timeInterval)
let action = SKAction.sequence([wait, animate])
let completed = SKAction.runBlock{
circle.path = nil
completion()
}
let countDown = SKAction.repeatAction(action,count:steps-1)
let sequence = SKAction.sequence([countDown, SKAction.waitForDuration(timeInterval),completed])
runAction(sequence, withKey: "actionKey")
}

Swift, sprite kit game: Have circle disappear in clockwise manner? On timer?

Alright, so I don't know the name for this but I have a sprite kit game (a runner game) that, when it's game over, is going to have a "save me" button and a timer that runs out accordingly. When the timer runs out, you can no longer click the button and save the character.
I don't want to display this timer in text however- I want a circle that "unwinds itself," if you will, and disappears at the rate that the timer runs out. I.e. when the timer reaches 0, the circle has fully disappeared. The circle disappears degree by degree in a clockwise motion in accordance with the timer.
Here are some pictures to explain what I'm talking about.
How would I do this?
By changing the path property of an SKShapeNode at a fixed interval, you can create a frame-by-frame animation sequence. To create the animation, set the path property to a sequence of shapes that starts with a circle and ends with nothing. You can use UIBezierPath, a wrapper for CGPath, to create shapes for the animation using the following steps:
Move path's "pen" to the center of the circle
Add an arc to the path with addArcWithCenter from a startAngle to endAngle
Add a line to the path from the point on the circle corresponding to the ending angle to the center
Change the endAngle by a fixed amount
Repeat steps 1-4
Here's an implementation of the above steps:
override func didMove(to:SKView) {
let circle = SKShapeNode(circleOfRadius: 50)
circle.fillColor = SKColor.blue
circle.strokeColor = SKColor.clear
circle.zRotation = CGFloat.pi / 2
addChild(circle)
countdown(circle: circle, steps: 20, duration: 5) {
print("done")
}
}
// Creates an animated countdown timer
func countdown(circle:SKShapeNode, steps:Int, duration:TimeInterval, completion:#escaping ()->Void) {
guard let path = circle.path else {
return
}
let radius = path.boundingBox.width/2
let timeInterval = duration/TimeInterval(steps)
let incr = 1 / CGFloat(steps)
var percent = CGFloat(1.0)
let animate = SKAction.run {
percent -= incr
circle.path = self.circle(radius: radius, percent:percent)
}
let wait = SKAction.wait(forDuration:timeInterval)
let action = SKAction.sequence([wait, animate])
run(SKAction.repeat(action,count:steps-1)) {
self.run(SKAction.wait(forDuration:timeInterval)) {
circle.path = nil
completion()
}
}
}
// Creates a CGPath in the shape of a pie with slices missing
func circle(radius:CGFloat, percent:CGFloat) -> CGPath {
let start:CGFloat = 0
let end = CGFloat.pi * 2 * percent
let center = CGPoint.zero
let bezierPath = UIBezierPath()
bezierPath.move(to:center)
bezierPath.addArc(withCenter:center, radius: radius, startAngle: start, endAngle: end, clockwise: true)
bezierPath.addLine(to:center)
return bezierPath.cgPath
}
and a video clip: