What is the proper way to end a CAEmitterLayer in Swift? - swift

I've mostly seen examples of continuous emitters in Swift, and I've found one example in Obj-C by setting the birthRates of the emitter cells to 0.0, but it doesn't seem to work, so I must be doing something wrong. In my example, I can see the message that the birth rate was set to 0 sixteen times, but the particles continue to flow endlessly.
#IBAction func particleBtnAction(_ sender: Any) {
let emitter = CAEmitterLayer()
emitter.emitterPosition = CGPoint(x: self.view.frame.size.width / 2, y: -10)
emitter.emitterShape = kCAEmitterLayerLine
emitter.emitterSize = CGSize(width: self.view.frame.size.width, height: 2.0)
emitter.emitterCells = generateEmitterCells()
self.view.layer.addSublayer(emitter)
// perform selector after 1.5 seconds when particles start
perform(#selector(endParticles), with: emitter, afterDelay: 1.5)
}
private func generateEmitterCells() -> [CAEmitterCell] {
var cells:[CAEmitterCell] = [CAEmitterCell]()
for index in 0..<16 {
let cell = CAEmitterCell()
cell.birthRate = 4.0
cell.lifetime = 1.0
cell.lifetimeRange = 0
cell.velocity = 0.7
cell.velocityRange = 0
cell.emissionLongitude = CGFloat(Double.pi)
cell.emissionRange = 0.5
cell.spin = 3.5
cell.spinRange = 0
cell.scaleRange = 0.25
cell.scale = 0.1
cells.append(cell)
}
return cells
}
#objc func endParticles(emitterLayer:CAEmitterLayer) {
for emitterCell in emitterLayer.emitterCells! {
emitterCell.birthRate = 0.0
print("birth rate set to 0")
}
}

Setting the CAEmitterLayer's lifetime to zero stops any new emitterCells being emitted:
#objc func endParticles(emitterLayer:CAEmitterLayer) {
emitterLayer.lifetime = 0.0
}

You can use key paths to assign a name to each cell and loop through them, changing each cell's property when you want to change them:
private func generateEmitterCells() -> [CAEmitterCell] {
var cells:[CAEmitterCell] = [CAEmitterCell]()
for index in 0..<16 {
let cell = CAEmitterCell()
cell.birthRate = 4.0
cell.lifetime = 1.0
cell.lifetimeRange = 0
cell.velocity = 0.7
cell.velocityRange = 0
cell.emissionLongitude = CGFloat(Double.pi)
cell.emissionRange = 0.5
cell.spin = 3.5
cell.spinRange = 0
cell.scaleRange = 0.25
cell.scale = 0.1
cell.name = "cell\(index)" // cell name
cells.append(cell)
}
return cells
}
#objc func endParticles(emitterLayer:CAEmitterLayer) {
for i in 0..<(emitterLayer.emitterCells?.count ?? 0){
emitterLayer.setValue(0.0, forKeyPath: "emitterCells.cell\(i).birthRate")
print("birth rate set to 0")
}
}

You might try setting the isHidden property when you want to endParticles
emitter.isHidden = true
But note that all the cells instantly vanish, no matter when they were emitted, or their lifetime.
Another possibility would be to set all the scale related properties to 0, and then the lifetime and birthrate would not matter, as newly emitted cells would not be visible.
cell.scaleSpeed = 0
cell.scaleRange = 0
cell.scale = 0

What worked for me is this:
let emmitter = CAEmitterLayer()
let cell = makeCell()
emmitter.emitterCells = [cell]
view.layer.addSublayer(emmitter)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
emmitter.removeFromSuperlayer()
}

Related

Fill the CALayer with different color when timer reaches to 5 seconds swift ios

In my app i am using a third party library which does circular animation just like in the appstore app download animation. i am using the external file which i have placed in my project. it works but when the timer reaches to 5 seconds the fill color should be red. Currently the whole layer red color applies, i want to only apply the portion it has progressed. i am attaching the video and the third party file. Please can anyone help me with this as what changes should i make to the library file or is there better solution
same video link in case google drive link doesnt work
External Library link which i am using
video link
external third file file
My code snippet that i have tried:
func startGamePlayTimer(color: UIColor)
{
if !isCardLiftedUp {
self.circleTimerView.isHidden = false
self.circleTimerView.timerFillColor = color
Constants.totalGamePlayTime = 30
self.circleTimerView.startTimer(duration: CFTimeInterval(Constants.totalGamePlayTime))
}
if self.gamePlayTimer == nil
{
self.gamePlayTimer?.invalidate()
let timer = Timer.scheduledTimer(timeInterval: 1.0,
target: self,
selector: #selector(updateGamePlayTime),
userInfo: nil,
repeats: true)
timer.tolerance = 0.05
RunLoop.current.add(timer, forMode: .common)
self.gamePlayTimer = timer
}
}
#objc func updateGamePlayTime()
{
if Constants.totalGamePlayTime > 0
{
Constants.totalGamePlayTime -= 1
if Constants.totalGamePlayTime < 5
{
SoundService.playSound(sound: .turn_timeout)
self.circleTimerView.timerFillColor = UIColor.red.withAlphaComponent(0.7)
}
}
else
{
self.stopGamePlayTimer()
}
}
where circleTimerView is the view which animates as the time progresses
You can achieve that with CAKeyframeAnimation on the StrokePath.
I've Update the code in CircleTimer View as below. You can modified fill color and time as per your need.
#objc func updateGamePlayTime()
{
if Constants.totalGamePlayTime > 0
{
Constants.totalGamePlayTime -= 1
if Constants.totalGamePlayTime < 5
{
SoundService.playSound(sound: .turn_timeout)
// self.circleTimerView.timerFillColor = UIColor.red.withAlphaComponent(0.7) // <- Comment this line
}
}
else
{
self.stopGamePlayTimer()
}
}
open func drawFilled(duration: CFTimeInterval = 5.0) {
clear()
if filledLayer == nil {
let parentLayer = self.layer
let circleLayer = CAShapeLayer()
circleLayer.bounds = parentLayer.bounds
circleLayer.position = CGPoint(x: parentLayer.bounds.midX, y: parentLayer.bounds.midY)
let circleRadius = timerFillDiameter * 0.5
let circleBounds = CGRect(x: parentLayer.bounds.midX - circleRadius, y: parentLayer.bounds.midY - circleRadius, width: timerFillDiameter, height: timerFillDiameter)
circleLayer.fillColor = timerFillColor.cgColor
circleLayer.path = UIBezierPath(ovalIn: circleBounds).cgPath
// CAKeyframeAnimation: Changing Fill Color on when timer reaches 80% of total time
let strokeAnimation = CAKeyframeAnimation(keyPath: "fillColor")
strokeAnimation.keyTimes = [0, 0.25, 0.75, 0.80]
strokeAnimation.values = [timerFillColor.cgColor, timerFillColor.cgColor, timerFillColor.cgColor, UIColor.red.withAlphaComponent(0.7).cgColor]
strokeAnimation.duration = duration;
circleLayer.add(strokeAnimation, forKey: "fillColor")
parentLayer.addSublayer(circleLayer)
filledLayer = circleLayer
}
}
open func startTimer(duration: CFTimeInterval) {
drawFilled(duration: duration) // <- Need to pass duration here
if useMask {
runMaskAnimation(duration: duration)
} else {
runDrawAnimation(duration: duration)
}
}
UPDATE:
CAKeyframeAnimation:
You specify the keyframe values using the values and keyTimes properties.
For example:
let colorKeyframeAnimation = CAKeyframeAnimation(keyPath: "backgroundColor")
colorKeyframeAnimation.values = [UIColor.red.cgColor,
UIColor.green.cgColor,
UIColor.blue.cgColor]
colorKeyframeAnimation.keyTimes = [0, 0.5, 1]
colorKeyframeAnimation.duration = 2
This animation will run for the 2.0 duration.
We have 3 values and 3 keyTimes in this example.
0 is the initial point and 1 be the last point.
It shows which associated values will reflect at particular interval [keyTimes] during the animation.
i.e:
KeyTime 0.5 -> (2 * 0.5) -> At 1 sec during animation -> UIColor.green.cgColor will be shown.
Now to answer your question from the comments, Suppose we have timer of 25 seconds and we want to show some value for last 10 seconds, we can do:
strokeAnimation.keyTimes = [0, 0.25, 0.50, 0.60]
strokeAnimation.values = [timerFillColor.cgColor,timerFillColor.cgColor, timerFillColor.cgColor, timerFillColor.cgColor, UIColor.red.withAlphaComponent(0.7).cgColor]
strokeAnimation.duration = 25.0 // Let's assume duration is 25.0

Glow buttons with a delay in between

I'm trying to light up a sequence of buttons in order with a small delay in between but I just can't figure out how to glow each button separately with a small delay in between without freezing all code.
at this point I got this, which waits a second and for some reason lights up both buttons at the same time after.
The array given to the method contains values from 1-3 referencing one of the 3 buttons in order
private func showSequence(sequence: Array<Int>){
for i in sequence {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.buttonArray[i-1].doGlowAnimation(withColor: .white, withEffect: .big)
}
}
}
And the glow effect I found online, code:
extension UIView {
enum GlowEffect: Float {
case small = 0.4, normal = 2, big = 15
}
func doGlowAnimation(withColor color: UIColor, withEffect effect: GlowEffect = .normal) {
layer.masksToBounds = false
layer.shadowColor = color.cgColor
layer.shadowRadius = 0
layer.shadowOpacity = 1
layer.shadowOffset = .zero
let glowAnimation = CABasicAnimation(keyPath: "shadowRadius")
glowAnimation.fromValue = 0
glowAnimation.toValue = effect.rawValue
glowAnimation.beginTime = CACurrentMediaTime()+0.3
glowAnimation.duration = CFTimeInterval(0.3)
glowAnimation.fillMode = .removed
glowAnimation.autoreverses = true
glowAnimation.isRemovedOnCompletion = true
layer.add(glowAnimation, forKey: "shadowGlowingAnimation")
}
}

How to make sprites follow a random pattern within a circle?

I am makeing a game in which I want that the enemies move following a random pattern within a circle. I already made that the enemies spawn randomly in all the sides of the screen, but the problem is that I dont know how to make the enemies move following a random pattern within a circle just like the image.
class GameScene: SKScene, SKPhysicsContactDelegate {
var circuloPrincipal = SKSpriteNode(imageNamed: "circulo")
var enemigoTimer = NSTimer()
}
override func didMoveToView(view: SKView) {
circuloPrincipal.size = CGSize(width: 225, height: 225)
circuloPrincipal.position = CGPoint(x: frame.width / 2, y: frame.height / 2)
circuloPrincipal.color = colorAzul
circuloPrincipal.colorBlendFactor = 1.0
circuloPrincipal.name = "circuloPrincipal"
circuloPrincipal.zPosition = 1.0
self.addChild(circuloPrincipal)
override func touchesBegan(touches: Set, withEvent event: UIEvent?) {
enemigoTimer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("enemigos"), userInfo: nil, repeats: true)
}
func enemigos() {
let enemigo = SKSpriteNode(imageNamed: "enemigo")
enemigo.size = CGSize(width: 25, height: 25)
enemigo.zPosition = 2.0
enemigo.name = "enemigo"
let posisionRandom = arc4random() % 4
switch posisionRandom {
case 0:
enemigo.position.x = 0
let posisionY = arc4random_uniform(UInt32(frame.size.height))
enemigo.position.y = CGFloat(posisionY)
self.addChild(enemigo)
break
case 1:
enemigo.position.y = 0
let posisionX = arc4random_uniform(UInt32(frame.size.width))
enemigo.position.x = CGFloat(posisionX)
self.addChild(enemigo)
break
case 2:
enemigo.position.y = frame.size.height
let posisionX = arc4random_uniform(UInt32(frame.size.width))
enemigo.position.x = CGFloat(posisionX)
self.addChild(enemigo)
break
case 3:
enemigo.position.x = frame.size.width
let posisionY = arc4random_uniform(UInt32(frame.size.height))
enemigo.position.y = CGFloat(posisionY)
self.addChild(enemigo)
break
default:
break
}
enemigo.runAction(SKAction.moveTo(circuloPrincipal.position, duration: 1.4))
}
Try to add this code:
let randomY = CGFloat(Int.random(-Int(circuloPrincipal.frame.height/2)...Int(circuloPrincipal.frame.height/2)))
let randomX = CGFloat(Int.random(-Int(circuloPrincipal.frame.width/2)...Int(circuloPrincipal.frame.width/2)))
let slopeToCirculoPrincipal = (enemigo.position.y - circuloPrincipal.position.y + randomY ) / (enemigo.position.x - circuloPrincipal.position.x + randomX)
let constant = enemigo.position.y - slopeToCirculoPrincipal * enemigo.position.x
let finalX : CGFloat = enemigo.position.x < circuloPrincipal.position.x ? 1500.0 : -1500.0 // Set it to somewhere outside screen size
let finalY = constant + slopeToCirculoPrincipal * finalX
let distance = (enemigo.position.y - finalY) * (enemigo.position.y - finalY) + (enemigo.position.x - finalX) * (enemigo.position.x - finalX)
let enemigoSpeed : CGFloat = 100.0
let timeToCoverDistance = sqrt(distance) / enemigoSpeed
let moveAction = SKAction.moveTo(CGPointMake(finalX, finalY), duration: NSTimeInterval(timeToCoverDistance))
let removeAction = SKAction.runBlock { () -> Void in
enemigo.removeFromParent()
}
enemigo.runAction(SKAction.sequence([moveAction,removeAction]))
Instead of:
enemigo.runAction(SKAction.moveTo(circuloPrincipal.position, duration: 1.4))
Also you need to put this extension somewhere in your project:
extension Int
{
static func random(range: Range<Int> ) -> Int
{
var offset = 0
if range.startIndex < 0 // allow negative ranges
{
offset = abs(range.startIndex)
}
let mini = UInt32(range.startIndex + offset)
let maxi = UInt32(range.endIndex + offset)
return Int(mini + arc4random_uniform(maxi - mini)) - offset
}
}

How to animate a matrix changing the sprites one by one?

I´m making a little game were I have a matrix compose for SKSpriteNode and numbers, when the game its over I´m trying to make an animation were I go over the matrix changing only the sprite one by one following the order of the numbers. Look the
Board (The squares are in a Sknode and the number in other Sknode)
The Idea is change the sprite to other color and wait 2 sec after change the next but I can´t do it. I don't know how to change the sprite one by one. I make this function "RecoverMatrix()", this change the sprites but all at once, it is as if not take the wait, he change all the sprites and before wait the 2 sec.
func RecoverMatrix() {
var cont = 1
TileLayer.removeAllChildren()
numLayer.removeAllChildren()
let imageEnd = SKAction.setTexture(SKTexture(imageNamed: "rectangle-play"))
let waiting = SKAction.waitForDuration(2)
var scene: [SKAction] = []
var tiles: [SKSpriteNode] = []
while cont <= 16 {
for var column = 0; column < 4; column++ {
for var row = 0; row < 4; row++ {
if matrix[column][row].number == cont {
let label = SKLabelNode()
label.text = "\(matrix[column][row])"
label.fontSize = TileHeight - 10
label.position = pointForBoard(column, row: row)
label.fontColor = UIColor.whiteColor()
let tile = SKSpriteNode()
tile.size = CGSize(width: TileWidth - 3, height: TileHeight - 3)
tile.position = pointForBoard(column, row: row, _a: 0)
TileLayer.addChild(tile)
numLayer.addChild(label)
tiles.append(tile)
scene.append(SKAction.sequence([imageEnd, waiting]))
tile.runAction(imageEnd)
runAction(waiting)
didEvaluateActions()
}
}
}
cont++
}
for tile in tiles {
tile.runAction(SKAction.sequence(scene))
self.runAction(SKAction.waitForDuration(1))
}
}
So, I need help, I don't find the way to make this animation. I really appreciate the help. Thanks!
This is how you can run an action on every node at the same time (using a loop to loop through all the tiles):
class GameScene: BaseScene, SKPhysicsContactDelegate {
var blocks: [[SKSpriteNode]] = []
override func didMoveToView(view: SKView) {
makeBoard(4, height: 4)
colorize()
}
func makeBoard(width:Int, height:Int) {
let distance:CGFloat = 50.0
var blockID = 1
//make a width x height matrix of SKSpriteNodes
for j in 0..<height {
var row = [SKSpriteNode]()
for i in 0..<width {
let node = SKSpriteNode(color: .purpleColor(), size: CGSize(width: 30, height: 30))
node.name = "\(blockID++)"
if let nodeName = node.name {node.addChild(getLabel(withText: nodeName))}
else {
//handle error
}
node.position = CGPoint(x: frame.midX + CGFloat(i) * distance,
y: frame.midY - CGFloat(j) * distance )
row.append(node)
addChild(node)
}
blocks.append(row)
}
}
func colorize() {
let colorize = SKAction.colorizeWithColor(.blackColor(), colorBlendFactor: 0, duration: 0.5)
var counter = 0.0
let duration = colorize.duration
for row in blocks {
for sprite in row {
counter++
let duration = counter * duration
let wait = SKAction.waitForDuration(duration)
sprite.runAction(SKAction.sequence([wait, colorize]))
}
}
}
func getLabel(withText text:String) -> SKLabelNode {
let label = SKLabelNode(fontNamed: "ArialMT")
label.fontColor = .whiteColor()
label.text = text
label.fontSize = 20
label.horizontalAlignmentMode = .Center
label.verticalAlignmentMode = .Center
return label
}
}
And the result:
So basically, as I said in the comments, you can run all the actions at the same moment, it is just about when the each action will start.
You seem to imagine that runAction(waiting) means that you code pauses and waits, pausing between loops. It doesn't (and in fact there is no way to do that). Your code loops through all the loops, now, KABOOM, immediately.
Thus, all the actions are configured immediately and are performed together.

SKEmitterNode iOS 8 vs iOS 9 How to get the same result?

I have a game similar to fruit ninja using Swift -> SpriteKit. Everything is working fine on iOS 8 but on iOS 9 SKEmitterNode is having a bit strange behavior. This is what I get for my blade effect on both:
func emitterNodeWithColor(color:UIColor)->SKEmitterNode {
let emitterNode:SKEmitterNode = SKEmitterNode()
emitterNode.particleTexture = SKTexture(imageNamed: "spark.png")
emitterNode.particleBirthRate = 3000
emitterNode.particleLifetime = 0.2
emitterNode.particleLifetimeRange = 0
emitterNode.particlePositionRange = CGVectorMake(0.0, 0.0)
emitterNode.particleSpeed = 0.0
emitterNode.particleSpeedRange = 0.0
emitterNode.particleAlpha = 0.8
emitterNode.particleAlphaRange = 0.2
emitterNode.particleAlphaSpeed = -0.45
emitterNode.particleScale = 0.5
emitterNode.particleScaleRange = 0.001
emitterNode.particleScaleSpeed = -1
emitterNode.particleRotation = 0
emitterNode.particleRotationRange = 0
emitterNode.particleRotationSpeed = 0
emitterNode.particleColorBlendFactor = 1
emitterNode.particleColorBlendFactorRange = 0
emitterNode.particleColorBlendFactorSpeed = 0
emitterNode.particleColor = color
emitterNode.particleBlendMode = SKBlendMode.Add
return emitterNode
}
let emitter:SKEmitterNode = emitterNodeWithColor(color)
emitter.targetNode = target
emitter.zPosition = 0
tip.addChild(emitter)
This is the method I am using with all the options. It is the same for both but the result is different. Any ideas how can I make the effect in iOS 9 to be the same as iOS 8 ?
I'm facing the exact same issue in my project.
The emitter's performance is low in iOS9 (Metal version not finished?), so Apple shut off the interpolation of the drawing to get back the performance a little (The drawing rate is limited to 60 fps, anything between two frames is not rendered).
My solution is to implement the tail myself, which is simple:
class TailNode: SKSpriteNode {
var tailTexture: SKTexture!
var tailSize: CGSize! = CGSizeMake(30, 30)
var tailColor: SKColor!
var tailBlendMode: SKBlendMode!
var initialAlpha: CGFloat = 0.6
var initialScale: CGFloat = 0
var finalScale: CGFloat = 1
var particleLife: NSTimeInterval = 0.1
var running: Bool = false
var particleAction: SKAction!
var lastParticle: SKSpriteNode?
var battleScene: BattleScene {
return self.scene as! BattleScene
}
convenience init(tailTexture: SKTexture, tailSize: CGSize, tailColor: SKColor, tailBlendMode: SKBlendMode, initialAlpha: CGFloat, initialScale: CGFloat, finalScale: CGFloat, particleLife: NSTimeInterval) {
self.init(texture: nil, color: SKColor.whiteColor(), size: CGSize(width: 0, height: 0))
self.tailTexture = tailTexture
self.tailSize = tailSize
self.tailColor = tailColor
self.tailBlendMode = tailBlendMode
self.initialAlpha = initialAlpha
self.initialScale = initialScale
self.finalScale = finalScale
self.particleLife = particleLife
let fadeAction = SKAction.fadeAlphaTo(0, duration: particleLife)
let scaleAction = SKAction.scaleTo(finalScale, duration: particleLife)
let removeAction = SKAction.removeFromParent()
self.particleAction = SKAction.sequence([SKAction.group([fadeAction, scaleAction]), removeAction])
}
func updateWithTimeSinceLastUpdate(interval: NSTimeInterval) {
if running {
let particlePosition = battleScene.convertPoint(battleScene.convertPoint(self.position, fromNode: self.parent!),
toNode:battleScene.worldLayers[.UnderCharacter]!)
if lastParticle == nil || lastParticle!.parent == nil {
lastParticle = nil
} else {
let lastPosition = lastParticle!.position
let x = lastPosition.x + (particlePosition.x - lastPosition.x)*0.5
let y = lastPosition.y + (particlePosition.y - lastPosition.y)*0.5
newParticleAtPosition(CGPointMake(x, y), withDelay: interval*0.5)
}
lastParticle = newParticleAtPosition(particlePosition, withDelay: interval)
}
}
func newParticleAtPosition(position: CGPoint, withDelay delay: NSTimeInterval) -> SKSpriteNode {
let myParticle = SKSpriteNode(texture: tailTexture, color: tailColor, size: tailSize)
myParticle.colorBlendFactor = 1
myParticle.blendMode = tailBlendMode
myParticle.alpha = initialAlpha
myParticle.setScale(initialScale)
myParticle.position = position
battleScene.addNode(myParticle, atWorldLayer: .UnderCharacter)
myParticle.runAction(SKAction.sequence([SKAction.waitForDuration(delay), particleAction]))
return myParticle
}
}