MATTREAGAN
iOS & macOS engineer,
designer, game creator
Author: Matt Reagan
« Previous | Next »

SpriteKit Shatter Effect
In a previous post I presented an approach for creating a disintegration effect using Apple's SpriteKit framework, in which text appeared to blow apart in the wind. While working on a recent game project, I needed something a bit more dramatic: explosions.

Specifically, I needed a way to break apart a single 2D sprite as though it was made of glass. From an API standpoint, I wanted to extend SKSpriteNode so that for the desired effect I could simply call myNode.shatter() and it would do the rest.

This article discusses the implementation and how it works. Although the technique is very basic, it is versatile and can be used for a number of different effects. Also, although this article is SpriteKit-specific, the technique can be used easily with nearly any graphics library.

 Get the Swift source code for this article on GitHub: /matthewreagan/SpriteShatter

Final Effect



The effect is shown here in the demo app which also allows the animation to be controlled via a slider to better examine how each node moves while exploding. Each piece is individually randomized, but the animation is deterministic and the rotation, position, and scale of any piece can be calculated for arbitrary timeframes if desired.

Step 1: Shattering the Sprite

The basic implementation extendeds SKSpriteNode with a shatter() function, which does the following:
  1. Hides the original sprite
  2. Creates a new stand-in parent node for the pieces
  3. Creates a grid of child nodes, each with a respective subtexture from the original sprite
  4. Calculates the angle from the sprite center
  5. Performs the shatter animation by moving each child away from the blast point
We start by adding a new parent node with the same position, z-position, and z-rotation as the original sprite. This will act as the root parent for all child nodes. Each child SKSpriteNode is given a subtexture using SKTexture(rect:,in:) based on the X,Y grid coordinate in the coordinate space of the original texture. In most cases the original SKTexture will have a -textureRect of (0,0)-(1,1). An example of how the subtexture rectangle is calculated is shown in the image to the right. Once the grid coordinates are iterated and all of our nodes are created, we can then calculate the animation properties.

Note: you'll run into problems if you attempt to create a subtexture of another subtexture without respecting the -textureRect. You can either calculate your coordinates using -textureRect, or another option is to grab a -cgImage of the node's SKTexture and create a new texture copy using that raw image data. That new texture can then be modified however you like without any dependency against the original.

With all of the above implemented, we now have a functional - albeit simplistic - shatter effect. The animation in its current form is pretty rudimentary and not very impressive-looking.

Step 2: Sharp Edges

The most obvious problem with the above animation is the rectangular pieces. It would be better if we could use triangles, but there isn't a particularly easy way in SpriteKit of obtaining non-rectangular subtextures or creating non-rectangular sprite nodes.(*) We can, however, easily create a custom CGPath and use it with SKCropNode to mask our pieces so that they look like triangles. The next change adds the following:
  1. Performs a two-step pass for each X,Y in the original grid
  2. For each pass, creates an SKCropNode and a triangular CGPath to use as its -maskingNode
We've now more than doubled the number of nodes for our animation, since each grid section will now have two triangles (consisting of a crop, mask, and the actual sprite node), but this small change has improved the visual effect significantly.
(* = SKShapeNodes can be created with custom CGPaths and provided a -fillTexture, though in practice I've encountered unexpected rendering behaviors with this approach. SKShader is also an option, but writing OpenGL ES shader code is overkill for this simple alpha masking. For these reasons SKCropNode seems to be the best - or at least simplest! - solution.)

Step 3: Randomization, Physics, Tweaks

The final changes consist mostly of tweaking the animation parameters to make things look a bit better. Although simulated physics aren't used in the demo, SpriteKit supports physics out-of-the-box and it would be fairly trivial to make the pieces bounce realistically or be influenced by the SKScene's -physicsWorld.

As part of the animation adjustments, a calculation was added to check the distance of each piece from the 'blast point' and increase its speed accordingly. These calculations can be seen in the demo app by checking the Heatmap checkbox, which colors each piece based on this value (using -colorBlendFactor).

While writing the demo I also added animation options to allow the progress of the animation to be controlled arbitrarily rather than with SKActions. In most cases this isn't necessary, and if using SpriteKit simple SKActions make animating the individual pieces straightforward.

Fun with CIFilter

For an edge-enhancement effect similar to the one in the video, you can leverage the fact that SKScene is actually a subclass of SKEffectNode. This means a CIFilter can be set on the scene and it will be applied in realtime. An example snippet is below, with an animation of the result (right).

let filter = CIFilter(name: "CIEdges")!
filter.setDefaults()
filter.setValue(25.0, forKey: "inputIntensity")
scene.filter = filter
scene.shouldEnableEffects = true

Conveniences

A few extensions are used by the shatter() function to make the code a bit easier to read (and write), and are worth a quick mention:

Random.between0And1()

This is simply a convenience for getting random values between 0.0-1.0 using drand48(). For more information about pseudorandom number generation check out this NSHipster article.

SKAction

SKAction is extended with a few functions (like byEasingIn()) to make it easier to use some of the nonlinear timing functions while still defining the action on a single line. Example:

node.run(SKAction.move(to: point).byEasingIn())
    vs.
let action = SKAction.move(to: point)
action.timingMode = .easeIn
node.run(action)

ShatterPieceNode

SKCropNode is subclassed to add an additional property to hold optional animation metadata. This is not strictly needed, however. In addition, a custom SKNode is subclassed to provide a -pieces property to easily access the shatter child nodes.

A final note about SpriteKit

In iOS 12 & macOS 10.14 I was disappointed to see that Apple made no significant changes or updates to SpriteKit. It's a fantastic framework with a great API, and the opportunities are still endless for native game development on Apple hardware. Particularly considering the exciting things happening with ARKit, frameworks which provide easy ways of getting graphics (whether 3D or 2D) quickly and flexibly onto the screen are immensely valuable.

In many ways I feel like frameworks such as SpriteKit really capture the essence of what Apple is all about: offering powerful features, but with that power comfortably obscured and progressively disclosed by an otherwise very simple and intuitive interface.

I really hope Apple will continue to invest the resources to maintain and improve SpriteKit. If any readers are currently developing with SpriteKit I would love to hear about what you're working on - get in touch! (And also, if you're curious about what I'm working on in SpriteKit, stop by my game development page.)