
What you are going to learn
- Create your normal maps using SpriteIlluminator
- Create a side scrolling scene with parallax scrolling
- Create a normal mapped sprite
- Add light effects to the game scene
- The complete project is on GitHub
Create your normal maps
Copy the assets from to the GitHub project into your project folder. The sprites are located in LightingDemo/LightingDemo/Sprites.
The easiest way to create normal maps for your sprites is to use SpriteIlluminator. For a quick start you simply
- Drag + drop your sprites on the SpriteIlluminator window
- Select all sprites
- Apply Bevel and Emboss effects
- Publish the normal map images to the sprite directory, using the
_n
suffix
In the GitHub repository, you'll find a file called spriteilluminator-project.sip. It contains the animation and background images.
The project also contains pre-built normal maps for this tutorial.
Setup your SpriteKit project
Simply create a new empty SpriteKit project for iOS devices.
Configure the project's Device orientation as Landscape right
Drag & drop the Sprites folder onto the project.
Clean the GameScene.swift by replacing it with the following code:
import SpriteKit
class GameScene: SKScene {
var _scale: CGFloat = 1.0
var _screenH: CGFloat = 640.0
var _screenW: CGFloat = 960.0
override func didMoveToView(view: SKView) {
_screenH = view.frame.height
_screenW = view.frame.width
_scale = _screenW / 1920
}
}
This code determines the screen size and derives a content scale factor _scale
.
The scale factor allows showing 1 background tile (1920px) on the screen.
Open GameViewController.swift and change scene.scaleMode = .AspectFill
into scene.scaleMode = .ResizeFill
:
override func viewDidLoad() {`
...
scene.scaleMode = .ResizeFill
...
}
We are only using 1 set of graphics at the highest resolution that we scale down to match the different devices. We are doing this to keep the tutorial as short as possible. For a real project it is highly recommended that you use pre-scaled versions of your graphics that match your devices resolution. This will improve the frame rate.
If you start this project you'll simply see … nothing.
Creating an animated normal mapped sprite
You'll create the animated sprite in a function called initSprite
. First you have to call
this function from the didMoveToView
- simply pace it at the end of the function:
override func didMoveToView(view: SKView) {
...
initSprite()
}
Let's now add our sprite animation. This is a bit more tricky since SpriteKit does not have a way to animate normal mapped sprites out of the box. You have to create a callback action that applies the new frame and the normal map.
Create the initSprite
function.
This first block creates 2 arrays, with SKTexture
objects for the animations phases
of the sprite and the normal map. It simply iterates over the animation frames building the
file names from the string format. The character animation currently has 8 frames: 1..8.
private func initSprite() {
var animFrames = [SKTexture]()
var normals = [SKTexture]()
for index in 1...8 {
animFrames.append(SKTexture(imageNamed: String(format:"Sprites/character/%02d.png", index)))
normals.append(SKTexture(imageNamed: String(format:"Sprites/character/%02d_n.png", index)))
}
Creates a SKSpriteNode
using the first animation frame and normal map as initial data:
let sprite = SKSpriteNode(texture: animFrames[0], normalMap: normals[0])
Create an SKAction
with an actionBlock
that switches normal map and sprite at the same time.
The speed of the animation can be adjusted using fps
. The number of frames
is determined from the length of the animFrames
array:
let fps = 8.0
let anim = SKAction.customActionWithDuration(1.0, actionBlock: { node, time in
let index = Int((fps * Double(time))) % animFrames.count
(node as! SKSpriteNode).normalTexture = normals[index]
(node as! SKSpriteNode).texture = animFrames[index]
})
sprite.runAction(SKAction.repeatActionForever(anim));
Finally set the sprite's position on the screen, adjusts it's scale and add it to the scene. End the function:
sprite.position = CGPoint(x: _screenW / 2, y: _screenH / 2 - 75.0 * _scale)
sprite.setScale(_scale)
sprite.lightingBitMask = 1
addChild(sprite)
}
The important line is sprite.lightingBitMask = 1
which assigns the light to a light source.
The lights have a property called lightCategoryBitMask
. A light shines on a sprite if at least
one bit is set in both — the lightCategoryBitMask
and lightingBitMask
.
Compile and run. You should now see our character walking in front of a gray screen. Nice - but where is the light?

Adding a light source
A light is implemented using an SKLightNode
. The node itself is invisible. This is why you'll add
a sprite with a light symbol to have a better reference where it is positioned.
First add a variable to store the light's sprite and a variable to store the ambient light color. Add this
to the top of the GameScene
class:
var _lightSprite:SKSpriteNode?
var _ambientColor:UIColor?
Extend didMoveToView
to init the ambient light color and the light:
override func didMoveToView(view: SKView) {
...
_ambientColor = UIColor.darkGrayColor()
initSprite()
initLight()
}
Now add a function initLight
. Start with creating a sprite to represent the light
as a visible object in the scene. There's a small lightbulb icon which will do fine. Also set
the position and scale of the sprite:
private func initLight() {
_lightSprite = SKSpriteNode(imageNamed: "Sprites/lightbulb.png")
_lightSprite?.setScale(_scale * 2)
_lightSprite?.position = CGPointMake(_screenW - 100, _screenH - 100)
addChild(_lightSprite!);
Now create an SKLightNode
and attach it to the sprite. Note that if you don't want the
light bulb icon you can simply add the light to the scene instead. It's derived from SKNode
so it contains all the functions for positioning etc.
var light = SKLightNode();
light.position = CGPointMake(0,0)
light.falloff = 1
light.ambientColor = _ambientColor
light.lightColor = UIColor.whiteColor()
_lightSprite?.addChild(light)
}
The light has it's own color - lightColor
which you can change to create different effects. ambientColor
is a background light - it does not have any direction and shines on all
pixels of a sprite. falloff
changes how far the light is visible.
Compile and run - way better! Now you see the light's effect.

Moving the light source
To make the light movable simply add these functions to the GameScene
class. They
adjust the light sprite and it's connected child light source to the touch position:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
for touch: AnyObject in touches {
_lightSprite?.position = touch.locationInNode(self)
}
}
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
for touch: AnyObject in touches {
_lightSprite?.position = touch.locationInNode(self)
}
}
Adding some background
Let's finally add some background. A scrolling background fits this scene best. To give the scene some more depth, you'll add 2 layers: A foreground which scrolls with the character and a background.
The background is far away — so the influence of the light source should not be visible.
This is easily done by adding a background sprite that is tinted by our ambient light.
Set the ambient color on the sprite's color
property and set the colorBlendFactor
depending on how intense the tinting should be.
No need to apply any light source or normal map.
The function to add a background tile is simple:
private func addBackgroundTile(spriteFile: String) -> SKSpriteNode {
var background:SKSpriteNode
background = SKSpriteNode(imageNamed:spriteFile);
background.color = _ambientColor!
background.colorBlendFactor = 0.75
background.anchorPoint = CGPoint(x:0, y:0.5)
background.position = CGPoint(x:0, y:_screenH / 2.0);
background.setScale(_scale)
addChild(background);
return background;
}
The foreground gets a normal map to react on the light. Don't forget to set lightingBitMask
to match the light.
private func addForegroundTile(spriteFile: String, normalsFile: String) -> SKSpriteNode {
var foreground:SKSpriteNode
foreground = SKSpriteNode(texture: SKTexture(imageNamed:spriteFile),
normalMap: SKTexture(imageNamed:normalsFile));
foreground.lightingBitMask = 1
foreground.anchorPoint = CGPoint(x:0, y:0.5)
foreground.position = CGPoint(x:0, y:_screenH / 2.0);
foreground.setScale(_scale)
addChild(foreground);
return foreground;
}
With these 2 function setting the background is easy. Create an initBackground
function:`
private func initBackground() {
_backgroundSprite1 = addBackgroundTile("Sprites/background_01.png");
_backgroundSprite2 = addBackgroundTile("Sprites/background_01.png");
_foregroundSprite1 = addForegroundTile("Sprites/foreground_01.png",
normalsFile:"Sprites/foreground_01_n.png");
_foregroundSprite2 = addForegroundTile("Sprites/foreground_02.png",
normalsFile:"Sprites/foreground_02_n.png");
}
Add the variables to hold the sprites to the GameScene
class
var _backgroundSprite1: SKSpriteNode?
var _backgroundSprite2: SKSpriteNode?
var _foregroundSprite1: SKSpriteNode?
var _foregroundSprite2: SKSpriteNode?
And call it from didMoveToView
before adding the sprite or light objects:
override func didMoveToView(view: SKView) {
...
_ambientColor = UIColor.darkGrayColor()
initBackground()
initSprite()
initLight()
}

Compile & run - looks nice, eh? It's just not moving...
Notice the big drop in frame rate. This has 2 main reasons:
- iOS simulator is slow — the emulation with shaders takes a huge amount of performance. This is not only with SpriteKit but also in cocos2d and other frameworks.
The performance on the real hardware is ok - so don't worry too much about this.
- We use the high-quality images and scale them down at runtime. If you pre-scale the images to match the devices performance will be way better.
Adding some parallax scrolling
Now let's get the scene moving. Simply add the following function:
override func update(currentTime: CFTimeInterval) {
var y:CGFloat = _screenH / 2.0;
var backgroundOffset: CGFloat = -CGFloat(Int(currentTime*100) % (1920*2));
_backgroundSprite1?.position = CGPoint(
x: _scale*((backgroundOffset < -1920) ? (3840+backgroundOffset) : backgroundOffset),
y: y)
_backgroundSprite2?.position = CGPoint(
x:_scale*(1920+backgroundOffset),
y:y)
var foregroundOffset: CGFloat = -CGFloat(Int(currentTime*250) % (1920*2));
_foregroundSprite1?.position = CGPoint(
x: _scale*((foregroundOffset < -1920) ? (3840+foregroundOffset) : foregroundOffset),
y: y)
_foregroundSprite2?.position = CGPoint(
x: _scale*(1920+foregroundOffset),
y: y)
}
The function called update
is called before the next frame is rendered. It gets the currentTime
as parameter.
The function simply uses the time to update the position of the sprites.
The speed factor for the foreground is 250
, the factor for the background is 100
—
this is why the foreground moves faster than the background — giving the impression of depth.
In case you are wondering what the ?:
operator is doing in _backgroundSprite1
and _foregroundSprite1
: It places the sprite before or after the second sprite depending on the scroll position.
Start experimenting!
Now it's up to you to experiment... e.g. using different light presets:
_ambientColor = UIColor.blueColor()
light.lightColor = UIColor.redColor()

_ambientColor = UIColor(red: 0.7, green: 0.4, blue: 0.5, alpha: 1.0)
light.lightColor = UIColor.whiteColor()

_ambientColor = UIColor(red: 1.0, green: 1.0, blue: 0.8, alpha: 1.0)
light.lightColor = UIColor.whiteColor()

Or what about adding a camp fire? Use some particle effects to create the fire, place a light source in it's middle and animate the position a bit to make it flicker …
... and finally: Create your own normal maps
SpriteIlluminator makes this process easy:
