Custom Particle System for iOS

2019-06-08 11:43发布

问题:

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?

回答1:

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:

Anyways, here is the final result:

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).



回答2:

What you want is completely possible, probably even in real time. Unfortunately to do such a thing the way you describe with moving particles as being a particle for each pixel would be best done with a pixel shader. I don't know of a clean method that would allow you to draw on top of the scene with a pixel shader otherwise all you would need is a pixel shader that takes the pixels and moves them out from the center. I personally wouldn't try to do this unless I built the game with my own custom game engine in place of spritekit.

That being said I'm not sure a pixel per pixel diffusion is the best thing in most cases. Expecially if you have cartoony art. Many popular games will actually make sprites for fragments of the object they expect to shader. So like if it's an airplane you might have a sprite for the wings with perhaps even wires hanging out of this. Then when it is time to shatter the plane, remove it from the scene and replace the area with the pieces in the same shape of the plane... Sorta like a puzzle. This will likely take some tweaking. Then you can add skphysicsbodies to all of these pieces and have a force push them out in all directions. Also this doesn't mean that each pixel gets a node. I would suggest creatively breaking it into under 10 pieces.

And as whirlwind said you could all ways get things looking "like" it actually disintegrated by using an emitter node. Just make the spawn area bigger and try to emulate the color as much as possible. To make the ship dissappear you could do a fade perhaps? Or Mabye an explosion sprite over it? Often with real time special effects and physics, or with vfx it is more about making it look like reality then actually simulating reality. Sometimes you have to use trickery to get things to look good and run real-time.

If you want to see how this might look I would recommend looking at games like jetpac joyride.

Good luck!