This is a very simple spotlight effect that can be used to show silhouettes. No shaders or blend modes, just a couple sprites. The way this effect works is that you make the foreground and background elements the same color and then simply add an element with contrasting color between them.

Playground

I will demo this with a SpriteKit Playground but this could just as easily be implemented with UIKit.

First, create a playground and do some setup to show the scene.

import PlaygroundSupport
import SpriteKit

class GameScene: SKScene {
}

let sceneView = SKView(frame: CGRect(x:0 , y:0, width: 640, height: 480))
let scene = GameScene(size: sceneView.bounds.size)
sceneView.presentScene(scene)

PlaygroundSupport.PlaygroundPage.current.liveView = sceneView

Now we can see the preview in the Live View assistant editor. It’s just a black screen right now but not for long!

Scene layout

Next let’s layout the view.

override func didMove(to view: SKView) {
        // add game over label
        let gameOverLabel = SKLabelNode(text: "GAME OVER")
        gameOverLabel.fontName = "AvenirNext-Heavy"
        gameOverLabel.fontColor = backgroundColor
        gameOverLabel.position = CGPoint(x: size.width/2, y: size.height/2)
        addChild(gameOverLabel)
        
        // add try again label
        let tryAgainLabel = SKLabelNode(text: "TRY AGAIN")
        tryAgainLabel.fontName = "AvenirNext"
        tryAgainLabel.fontColor = backgroundColor
        tryAgainLabel.position = CGPoint(x: size.width/2, y: 16)
        addChild(tryAgainLabel)
        
        // add spotlight
        let shapeNode = SKShapeNode(circleOfRadius: 30)
        shapeNode.fillColor = .white
        shapeNode.strokeColor = .white
        shapeNode.glowWidth = 90
        shapeNode.position = CGPoint(x: size.width/2, y: size.height/2)
        shapeNode.zPosition = -1
        addChild(shapeNode)
}

Let’s review what’s going on here.

First we add the foreground elements, in this case there are just two labels. The first label “GAME OVER” is centered in the screen while the second label “TRY AGAIN” is toward the bottom of the screen. The important part of the labels is that the fontColor of both labels is set to the same as the scenes backgroundColor.

The background color is set to the default color which is a dark gray. We could set the backgroundColor property to whatever we want and the foreground objects will match it, however dark colors work really well so we’ll just stick to the dark gray.

The last bit of this function is creating a shapeNode to represent the spotlight. I would usually use a sprite for this which would give much more character to the light source. For example, you could make the sprite look like the pattern of a flashlight.

For simplicity I decided to create this simple effect with a shape node. The important part is that the color (white) is contrasting to the background. Also notice that I’ve set the zPosition to -1. This means that it will be between the foreground elements (zPosition = 0) and the backgroundColor.

This is what the scene looks like. You can imagine that this is a light shining in the middle of the screen allowing us to read the words “GAME OVER”. Notice how the letters seem to fade at the edges. This very simple method is fairly convincing.

Animation

With a little animation it’s even more so. Add this to the end of didMove(to:)

let sequence = [
    SKAction.moveBy(x: 20, y: 10, duration: 0.75),
    SKAction.moveBy(x: -40, y: -20, duration: 1.5),
    SKAction.moveBy(x: 40, y: 0, duration: 1.5),
    SKAction.moveBy(x: -20, y: 10, duration: 0.75),
]
sequence.forEach { $0.timingMode = .easeInEaseOut }
let animation = SKAction.sequence(sequence)

shapeNode.run(animation)

This just adds some movement on the light. Imagine this with sound effect of stammering footsteps.

Remember that we also added a “TRY AGAIN” label. We don’t see it on the screen because it’s blending in with the background. I was thinking of making the light fall to the floor to reveal this label. Replace that last line.

// shapeNode.run(animation)
shapeNode.run(animation) {
    shapeNode.physicsBody = SKPhysicsBody(circleOfRadius: 1)
}
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)

What we are doing here is adding a completion block to the animation. Once the animation is done running we give the shapeNode a physicsBody. This will cause the node to be dynamic and become affected by gravity.

Notice that we gave the physics body a small radius. If it had the radius of the shapeNode then it would just look like a ball hitting the ground. The light source is what would be hitting the ground here and that will be much smaller than the light projected from it.

Finally, we also need to add an edge loop for the scene’s physics body so that we have the ground with which it makes contact.

This just needs some sound effects for a light hitting the floor and some stumbling and falling over.

Restart

For a simple try again handler we can just look at touch up events. Let’s just restart this scene.

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    let scene = GameScene(size: size)
    let reveal = SKTransition.fade(with: backgroundColor, duration: 0.5)
    view?.presentScene(scene, transition: reveal)
}

This is a simple convenience for reviewing the animation.

Bonus

What else can we do with this and what improvements can be made?

As I mentioned earlier, one improvement would be to use a sprite to represent the light rather than a shape node. Image, toward the end of the animation, the light rolling along the floor before revealing the “try again” label.

A flicker might be a nice touch as well. Perhaps by quickly adjusting opacity and/or size.

We could add a visual representation of the light source as well. The light source would be place in font of the foreground elements. Think of a torch, possibly with particle fire and smoke effects. Of course we would change the light color to match.

 

Here’s the final playground. spotlight.playground