SceneKit Tutorial With Swift Part 5: Particle Systems
In the fifth installment of our SceneKit With Swift tutorial, you’ll wrap up your Geometry Fighter game by adding cool particle systems. By Chris Language.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
SceneKit Tutorial With Swift Part 5: Particle Systems
30 mins
- Getting Started
- Particle Systems
- Creating a Trail Particle System
- Configuring the Trail Particle System
- Emitter Attributes
- Simulation Attributes
- Image Attributes
- Image Sequence Attributes
- Rendering Attributes
- Physics Attributes
- Life Cycle Attributes
- Heads-up Displays
- Touch Handling
- Naming Nodes
- Adding Touch Handling
- Using the Touch Handler
- Challenge
- Shaping Particle Explosions
- Adding Juice
- Where to Go From Here?
Heads-up Displays
In this short section, you’ll use the Game Utils to quickly add a little heads-up display to show your player’s remaining lives, best score and current score. The code behind the scenes uses a SpriteKit label and uses the output of the label as a texture for a plane. This is a powerful technique!
Add the following new property to GameViewController.swift, right below spawnTime
:
var game = GameHelper.sharedInstance
This lets you quickly access the GameHelper
shared instance, which contains a set of methods to do the heavy lifting for you.
Add the following method to the bottom of GameViewController
, below createTrail()
:
func setupHUD() {
game.hudNode.position = SCNVector3(x: 0.0, y: 10.0, z: 0.0)
scnScene.rootNode.addChildNode(game.hudNode)
}
Here, you make use of game.hudNode
from the helper library. You set the HUD node’s position and add it to the scene.
Next, you need to call setupHUD()
from somewhere. Add the following line to the bottom of viewDidLoad()
:
setupHUD()
Now that you have a heads-up display, you need to keep it up to date. Add the following call to game.updateHUD()
to the bottom of renderer(_: updateAtTime:)
game.updateHUD()
Build and run; you’ll see your display at the top of the screen as shown below:
Your game now has a nifty little HUD with a life counter, the high score and the current score.
Okay, the heads-up display is nice, but it’s high time to add some interaction to your game.
Touch Handling
As is usually the case, enabling touch in your app isn’t as straightforward as one would hope.
The first step is to understand how touch handling works in 3D. The image below shows a touch point in a side view of your scene and how SceneKit translates that touch point into your 3D scene to determine which object you’re touching:
So what steps do you take to handle the user’s touch event?
- Get touch location: First, you need to get the location of the user’s touch on the screen.
-
Convert to view coordinates: After that, you need to translate that touch location to a location relative to the
SCNView
instance that’s presenting the scene. - Fire a ray for a hit test: Once you’ve established a touch location relative to the view, SceneKit can perform a hit test for you by firing off a ray (no, not that Ray! :]) into your scene and returning a list of objects that intersect with the ray.
Naming Nodes
Before you can activate the touch ray of death, you need a way to identify each spawned object. The simplest approach is to give them names.
Add the following to spawnShape()
, right after you add the particle system to geometryNode
:
if color == UIColor.black {
geometryNode.name = "BAD"
} else {
geometryNode.name = "GOOD"
}
True to the spirit of the black-hatted villains of old Western movies, you assign the moniker "BAD"
to black-colored objects and "GOOD"
to all others.
Adding Touch Handling
Next, you need to write a method that you’ll later call when you detect the user has tapped a particular node.
Add the following method to the bottom of GameViewController
, below setupHUD()
:
func handleTouchFor(node: SCNNode) {
if node.name == "GOOD" {
game.score += 1
node.removeFromParentNode()
} else if node.name == "BAD" {
game.lives -= 1
node.removeFromParentNode()
}
}
This method checks the moniker of the touched node; good nodes increase the score and bad (black) nodes reduce the number of lives by one. In either case, you remove the node from the screen as it’s destroyed.
Using the Touch Handler
To capture the user’s touches, you’ll use touchesBegan(_: withEvent:)
, which is called every time the player touches the screen.
To implement your own version, add the following to GameViewController
, right below handleTouchFor(_:)
:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// 1
let touch = touches.first!
// 2
let location = touch.location(in: scnView)
// 3
let hitResults = scnView.hitTest(location, options: nil)
// 4
if let result = hitResults.first {
// 5
handleTouchFor(node: result.node)
}
}
Taking each numbered comment in turn:
- Grab the first available touch. There can be more than one if the player uses multiple fingers.
-
Translate the touch location to a location relative to the coordinates of
scnView
. -
hitTest(_: options:)
gives you an array ofSCNHitTestResult
objects that represent any intersections of the ray starting from the spot inside the view the user touched, and going away from the camera. - Check the first result from the hit test.
- Finally, you pass the first result node to your touch handler, which will either increase your score — or cost you a life!
One final step. You don’t need the camera control anymore so change the line in setupView()
as follows:
scnView.allowsCameraControl = false
Build and run; get ready to unleash your deadly finger of annihilation! :]
Tap on the spawned objects and make them disintegrate into thin air. Whoo-hoo! :]
Challenge
Time to up the cool factor — and what’s cooler than explosions? Absolutely nothing, right?
That brings you to the challenge of this tutorial, and that is to create another particle system and name it Explode.scnp. See if you can figure out what attributes to modify to make those particles explode.
The effect should look something similar to this:
You can use the following image as a starting point for your particle system:
Shaping Particle Explosions
Now that you’ve created the explosion particle system, you need to add some code to make those nodes explode. You’re going to use some special properties to make the explosion take the same shape as whatever node you touch.
Add the following to the bottom of GameViewController
, below touchesBegan(_: withEvent)
:
// 1
func createExplosion(geometry: SCNGeometry, position: SCNVector3,
rotation: SCNVector4) {
// 2
let explosion =
SCNParticleSystem(named: "Explode.scnp", inDirectory:
nil)!
explosion.emitterShape = geometry
explosion.birthLocation = .surface
// 3
let rotationMatrix =
SCNMatrix4MakeRotation(rotation.w, rotation.x,
rotation.y, rotation.z)
let translationMatrix =
SCNMatrix4MakeTranslation(position.x, position.y,
position.z)
let transformMatrix =
SCNMatrix4Mult(rotationMatrix, translationMatrix)
// 4
scnScene.addParticleSystem(explosion, transform: transformMatrix)
}
Here’s the play-by-play for the above code:
-
createExplosion(_: position: rotation:)
takes three parameters:geometry
defines the shape of the particle effect, whileposition
androtation
help place the explosion into the scene. -
This loads Explode.scnp and uses it to create an emitter. The emitter uses
geometry
asemitterShape
so the particles will emit from the surface of the shape. -
Enter the Matrix! :] Don’t be scared by these three lines; they simply provide a combined rotation and position (or translation) transformation matrix to
addParticleSystem(_: withTransform:)
. -
Finally, you call
addParticleSystem(_: wtihTransform)
onscnScene
to add the explosion to the scene.
You’re so close to replicating those great Hollywood explosions! Add the following line twice inside handleTouchFor(_:)
— once to the “good” if
block and once to the “bad” else
block, right before you remove node
from the parent:
createExplosion(geometry: node.geometry!,
position: node.presentation.position,
rotation: node.presentation.rotation)
This uses the presentation
property to retrieve the position
and rotation
parameters of node
. You then call createExplosion(_: position: rotation:)
with those parameters.
presentation
because the physics simulation is currently moving the node.
Build and run; tap away and make those nodes explode!