BenBtg BenBtg - 1 year ago 93
iOS Question

Custom Particle System for iOS

I want to create a particle system on

iOS
using sprite kit where I define the colour of each individual particle. As far as I can tell this isn't possible with the existing
SKEmitterNode
.
It seems that best I can do is specify general behaviour. Is there any way I can specify the starting colour and position of each particle?

Answer Source

This can give you a basic idea what I was meant in my comments. But keep in mind that it is untested and I am not sure how it will behave if frame rate drops occur.

This example creates 5 particles per second, add them sequentially (in counterclockwise direction) along the perimeter of a given circle. Each particle will have different predefined color. You can play with Settings struct properties to change the particle spawning speed or to increase or decrease number of particles to emit.

Pretty much everything is commented, so I guess you will be fine:

Swift 2

import SpriteKit

struct Settings {

    static var numberOfParticles = 30
    static var particleBirthRate:CGFloat = 5   //Means 5 particles per second, 0.2 means one particle in 5 seconds etc.
}

class GameScene: SKScene {

    var positions       = [CGPoint]()
    var colors          = [SKColor]()

    var emitterNode:SKEmitterNode?

    var currentPosition = 0

    override func didMoveToView(view: SKView) {

        backgroundColor = .blackColor()


        emitterNode = SKEmitterNode(fileNamed: "rain.sks")

        if let emitter = emitterNode {

            emitter.position =  CGPoint(x: CGRectGetMidX(frame), y: CGRectGetMidY(frame))
            emitter.particleBirthRate = Settings.particleBirthRate
            addChild(emitter)


            let radius = 50.0
            let center = CGPointZero

            for var i = 0; i <= Settings.numberOfParticles; i++ {

                //Randomize color
                colors.append(SKColor(red: 0.78, green: CGFloat(i*8)/255.0, blue: 0.38, alpha: 1))

                //Create some points on a perimeter of a given circle (radius = 40)
                let angle = Double(i) * 2.0 * M_PI / Double(Settings.numberOfParticles)
                let x = radius * cos(angle)
                let y = radius * sin(angle)


                let currentParticlePosition = CGPointMake(CGFloat(x) + center.x, CGFloat(y) + center.y)

                positions.append(currentParticlePosition)

                if i == 1 {
                    /*
                    Set start position for the first particle.
                    particlePosition is starting position for each particle in the emitter's coordinate space. Defaults to (0.0, 0,0).
                    */
                    emitter.particlePosition = positions[0]
                    emitter.particleColor = colors[0]

                    self.currentPosition++
                }

            }

            // Added just for debugging purposes to show positions for every particle.
            for particlePosition in positions {

                let sprite = SKSpriteNode(color: SKColor.orangeColor(), size: CGSize(width: 1, height: 1))
                sprite.position = convertPoint(particlePosition, fromNode:emitter)
                sprite.zPosition = 2
                addChild(sprite)
            }


            let block = SKAction.runBlock({

                // Prevent strong reference cycles.
                [unowned self] in

                if self.currentPosition < self.positions.count {

                    // Set color for the next particle
                    emitter.particleColor = self.colors[self.currentPosition]

                    // Set position for the next particle. Keep in mind that particlePosition is a point in the emitter's coordinate space.
                    emitter.particlePosition = self.positions[self.currentPosition++]

                }else {

                    //Stop the action
                    self.removeActionForKey("emitting")
                    emitter.particleBirthRate = 0
                }

           })


            // particleBirthRate is a rate at which new particles are generated, in particles per second. Defaults to 0.0.

            let rate = NSTimeInterval(CGFloat(1.0) / Settings.particleBirthRate)

            let sequence = SKAction.sequence([SKAction.waitForDuration(rate), block])

            let repeatAction = SKAction.repeatActionForever(sequence)


            runAction(repeatAction, withKey: "emitting")
        }

    }
}

Swift 3.1

import SpriteKit

struct Settings {

    static var numberOfParticles = 30
    static var particleBirthRate:CGFloat = 5   //Means 5 particles per second, 0.2 means one particle in 5 seconds etc.
}

class GameScene: SKScene {

    var positions = [CGPoint]()
    var colors = [SKColor]()

    var emitterNode: SKEmitterNode?

    var currentPosition = 0

    override func didMove(to view: SKView) {

        backgroundColor = SKColor.black


        emitterNode = SKEmitterNode(fileNamed: "rain.sks")

        if let emitter = emitterNode {

            emitter.position = CGPoint(x: frame.midX, y: frame.midY)
            emitter.particleBirthRate = Settings.particleBirthRate
            addChild(emitter)


            let radius = 50.0
            let center = CGPoint.zero

            for var i in 0...Settings.numberOfParticles {

                //Randomize color
                colors.append(SKColor(red: 0.78, green: CGFloat(i * 8) / 255.0, blue: 0.38, alpha: 1))

                //Create some points on a perimeter of a given circle (radius = 40)
                let angle = Double(i) * 2.0 * Double.pi / Double(Settings.numberOfParticles)
                let x = radius * cos(angle)
                let y = radius * sin(angle)


                let currentParticlePosition = CGPoint.init(x: CGFloat(x) + center.x, y: CGFloat(y) + center.y)

                positions.append(currentParticlePosition)

                if i == 1 {
                    /*
                    Set start position for the first particle.
                    particlePosition is starting position for each particle in the emitter's coordinate space. Defaults to (0.0, 0,0).
                    */
                    emitter.particlePosition = positions[0]
                    emitter.particleColor = colors[0]

                    self.currentPosition += 1
                }

            }

            // Added just for debugging purposes to show positions for every particle.
            for particlePosition in positions {

                let sprite = SKSpriteNode(color: SKColor.orange, size: CGSize(width: 1, height: 1))
                sprite.position = convert(particlePosition, from: emitter)
                sprite.zPosition = 2
                addChild(sprite)
            }


            let block = SKAction.run({

                // Prevent strong reference cycles.
                [unowned self] in

                if self.currentPosition < self.positions.count {

                    // Set color for the next particle
                    emitter.particleColor = self.colors[self.currentPosition]

                    // Set position for the next particle. Keep in mind that particlePosition is a point in the emitter's coordinate space.
                    emitter.particlePosition = self.positions[self.currentPosition]

                    self.currentPosition += 1

                } else {

                    //Stop the action
                    self.removeAction(forKey: "emitting")
                    emitter.particleBirthRate = 0
                }

            })


            // particleBirthRate is a rate at which new particles are generated, in particles per second. Defaults to 0.0.

            let rate = TimeInterval(CGFloat(1.0) / Settings.particleBirthRate)

            let sequence = SKAction.sequence([SKAction.wait(forDuration: rate), block])

            let repeatAction = SKAction.repeatForever(sequence)


            run(repeatAction, withKey: "emitting")
        }

    }
}

Orange dots are added just for debugging purposes and you can remove that part if you like.

Personally I would say that you are overthinking this, but I might be wrong because there is no clear description of what you are trying to make and how to use it. Keep in mind that SpriteKit can render a bunch of sprites in a single draw call in very performant way. Same goes with SKEmitterNode if used sparingly. Also, don't underestimate SKEmitterNode... It is very configurable actually.

Here is the setup of Particle Emitter Editor:

Particle Emitter Editor

Anyways, here is the final result:

emitter

Note that nodes count comes from an orange SKSpriteNodes used for debugging. If you remove them, you will see that there is only one node added to the scene (emitter node).

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download