This project was my first go at writing a Combine powered application. It is a simple game of Breakout (in the Wozniak tradition).
macOS Catalina 10.15
Xcode 11.0, Swift 5
Well, Apple describes Combine as "a declarative Swift API for processing values over time." What else basically boils down to values over time? That's right, videogames. And what game would be most appropriate? Breakout, of course.
This game of Breakout uses SpriteKit for graphics and input and Combine for the back end.
After the initial SpriteKit setup, the game calls the one static start
function on the GameEngine
enum
.
static func start(in scene: SKScene, inputSubject: InputSubject) -> AnyPublisher<([Sprite], Int, Int), GameError>
This function returns an AnyPublisher
with an Output
of [Sprite]
1.
This Publisher
is connected to a Subscriber
via the sink
method. This method takes two closures: one is called when the Publisher
publishes a new value and the other closure is called when the Publisher
sends a completion.
In CombineBreakout's case, the Publisher
publishes Sprite
s until the game ends with a Game Over. The Sprite
s are sent to an instance of the Renderer
class, which is responsible for updating the scene.
The game pipeline is broken up into several stages. Each stage is handled by an operator. This turned out to be an excellent way to learn about a few of Publisher
's operators, which I'm going to attempt to pass on here. As each operator returns a Publisher
it is easy to chain them into complex pipelines.
Every (realtime) game ever made relies on a timer to drive the game forward. Luckily Apple provides the Foundation Timer class that provides the subsecond precision we need (60hz!) and, when introducing Combine, they added a publish
method that returns a TimerPublisher
.
static func publish(every interval: TimeInterval, tolerance: TimeInterval? = nil, on runLoop: RunLoop, in mode: RunLoop.Mode, options: RunLoop.SchedulerOptions? = nil) -> Timer.TimerPublisher
Our pipeline starts with the TimerPublisher
. It is worth pointing out that TimerPublisher
doesn't start publishing until you call connect()
or autoconnect()
operator. This is inherited from its conformance to ConnectablePublisher
.
CombineBreakout calls autoconnect()
which causes the TimerPublisher
to start publishing events when the Subscriber
connects.
Input
Of course sprites and an update loop are great, but a game isn't a game until you can control it. Input in SpriteKit is handled via traditional input events, working them into the game pipeline required a bit of plumbing.
For CombineBreakout we want to control the paddle with the mouse. In order to do this in SpriteKit, we need to listen for mouseMoved
events2. These events are then published via a CurrentValueSubject
. This Subject
3 publishes the current input event (which it caches).
These input events are then fed into the pipeline by using the combineLatest
operator. This does as the name suggests - combining the latest value of the specified Publisher
(in this case, the last mouse moved event) and the target (the TimerPublisher
's output, the date of the latest event).
The next few stages of the pipeline use the map
operator to transform the mouse move event coordinates into a new paddle Point
and update the ball Point
, transform these raw Point
s into clipped Point
s and check for collisions.
The final map
transforms these coordinates into the array of Sprite
s.
tryMap
is exactly the same as map
, but it takes a throw
ing closure, allowing this stage in the pipeline to throw
a GameError.GameOver
error when the player runs out of lives. This causes the Publisher
to send a completion, ending the game loop.
As tryMap
throws a generic Error
and our Publisher
has a GameError
Failure
type, we need to transform the Error
into a GameError
. This is done by calling the mapError
operator.
Wraps our Publisher
in a type erased AnyPublisher
.
Hopefully this readme and this code helps someone understand how Combine's Publisher
s and their operators function. I highly recommend watching Apple's WWDC sessions: Introducing Combine and Combine in Practice.
Combine promises to bring reactive programming to mainstream Swift. It represents a slightly different way of architecting your application, but ultimately will lead to writing less code that is easier to reason about and is (hopefully) more stable.
Footnotes
-
Actually, the
Output
is a tuple of the sprites and the player's score and remaining lives. ↩ -
It is worth mentioning that SpriteKit doesn't track mouse movement events by default. I needed to setup a tracking area, as mentioned on this stackoverflow post ↩
-
A
Subject
is aPublisher
that provides a method that allows imperitive code to inject values into thePublisher
pipeline. ↩