SpriteKit game scene transitions with shaders
Deon Botha• Jul 18, 2015The casual game side project I’m working on has recently entered the last 20% phase of development (no doubt where 80% of my time will be invested). Game scene transitions are something I’ve been putting off for a while that I now have the pleasure of grinding through. I wanted to achieve a very “retro” feel with the transitions – the current result is looking like this:
The video shows two transitions (picked at random from a list of many similarly styled transitions) triggered when moving between the in game experience to the world map and vice versa. The transitions are implemented using OpenGL Shaders.
I thought I’d share my approach to SpriteKit scene transition using shaders as it wasn’t as straight forward as I expected.
Transitions using Shaders
SpriteKit scene transition is typically done using one of two SKView
’s presentScene
methods, one of which takes an SKTransition
argument
Unfortunately, out the box SKTransition
only offers a limited set of transition styles and
no real support to extend these styles using a shader. So the only option available to us if
we want to transition between two scenes using a shader is:
- Attach and run the shader transition for the current scene
- Transition “immediately” to the next scene using
presentScene
- Optionally attach and run a second shader or animation on the new scene to “smooth” the transition
This approach assumes the hiding of the current scene and display of the new scene happens as two discrete steps i.e. both scenes aren’t partially visible at the same time. If your transition calls for the two scenes to be partially visible in parallel at any point you’ll have some additional work.
I really wanted to maintain SpriteKit’s simplicity for switching scenes by calling
just a single function, unfortunately this isn’t possible due to needing to update the
elapsed time for the transition shader on each SKScene
update(currentTime:)
.
In the end I settled on the following three functions to perform a transition between scenes:
These functions are implemented within a Swift class extension on SKScene
so that you can easily
call them in place of the two SKView
’s presentScene
methods shown above. We’ll take a look
at the implementation of each function in turn in the sections that follow.
Presenting the new scene
To initiate a scene transition, presentScene(scene:shaderName:transitionDuration:)
is called passing the name of a shader file & the transition animation duration. The
implementation of the function is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/* SKScene+ShaderTransition.swift */
func presentScene(scene: SKScene,
shaderName: String,
transitionDuration: NSTimeInterval) {
// Create shader and add it to the scene
var shaderContainer = SKSpriteNode(imageNamed: "dummy")
shaderContainer.name = kNodeNameTransitionShaderNode
shaderContainer.position =
CGPointMake(size.width / 2, size.height / 2)
shaderContainer.size =
CGSizeMake(size.width, size.height)
shaderContainer.shader =
createShader(shaderName,
transitionDuration:transitionDuration)
shaderContainer.zPosition = 9999
self.addChild(shaderContainer)
// remove the shader from the scene after the
// transition animation has completed:
let delayTime = dispatch_time(DISPATCH_TIME_NOW,
Int64(transitionDuration * Double(NSEC_PER_SEC)))
dispatch_after(delayTime, dispatch_get_main_queue(),
{ () -> Void in
var fadeOverlay =
SKShapeNode(rect:
CGRectMake(0, 0,
self.size.width,
self.size.height))
fadeOverlay.name = kNodeNameFadeColourOverlay
fadeOverlay.zPosition = shaderContainer.zPosition
fadeOverlay.fillColor =
SKColor(red: 131.0 / 255.0,
green: 149.0 / 255.0,
blue: 255.0 / 255.0,
alpha: 1.0)
scene!.addChild(fadeOverlay)
self.view!.presentScene(scene)
}
)
// Reset the time presentScene was called so that the
// elapsed time from now can be calculated in
// updateShaderTransitions(currentTime:)
presentationStartTime = -1
}
Lines 8-18 create an SKShader
, attach
it to a dummy SKSpriteNode
and then add the node to the view. The dummy image is just a transparent 1x1 pixel png. We give
the SKSpriteNode
a name so that we can get at it’s shader from within other methods in the
extension. The zPosition
is set to something suitably large so that it’s guaranteed to
be in the foreground.
Lines 25-44 dispatch an asynchronous block to be run when the transition animation
completes. This block is responsible for adding an SKShapeNode
that covers the new
SKScene
with an opaque overlay and then immediately presenting it.
The overlay covers the entirety of the screen and has the same fill colour that
the SKShader
transition animation ends up filling the screen with. The new scene can now call
completeShaderTransition()
when it wants to complete the transition and gracefully
fade in.
Updating the shader transition
The shader itself has several uniform variables one of which is u_elapsed_time
–
this variable indicates the total time that has elapsed since the start of the transition.
Updating this uniform variable is the responsibility of the SKScene
extension’s
updateShaderTransition(currentTime:)
method:
presentationStartTime
is set to -1
whenever presentScene
is called, it then gets
set to the currentTime
next time updateShaderTransition
is called and from then on
we have a reference point as to when the transition animation began.
The above needs to be called from every SKScene
update(currentTime:)
method
that wants to support shader transitions:
Completing the transition
Once the presented scene is ready to be revealed (perhaps immediately, or perhaps after loading all required
assets, etc) it calls completeShaderTransition()
. This function retrieves the SKShapeNode
overlay and fades it out to gracefully reveal the new scene and complete the transition:
Example retro transition shader
The shader itself has several uniform variables (in addition to those that SpriteKit provides)
u_total_animation_duration
- The total duration of the transition animationu_elapsed_time
- The time that has elapsed since callingpresentScene
. It’s updated every frame in theupdateShaderTransition(currentTime:)
methodu_fill_colour
- The individual diamond tile fill colouru_border_colour
- The individual diamond tile stroke colour
This shader creates the fade down from the top of the screen effect that can be seen as
the first transition in the video at the top of the page. The shader itself divides the screen
into NUM_COLUMNS
columns and a dynamic number of rows. Within each tileSize
xtileSize
space
in the grid a diamond shape is drawn. The diamond starts small (it’s initial scale is 0) and
gradually fills it’s tile space over singleTileAnimDuration
seconds.
Each row of tiles has it’s individual tile animation start time delayed/offset
by animStartOffset
based on the rows distance from from the top – the top row of
tiles has animStartOffset = 0
, the bottom row
animStartOffset = u_total_animation_duration - singleTileAnimDuration
and everything else
has something in between the two.
Source code
Full source code in the form of an example app can be found on GitHub