Adapting Conway’s Game of Life on watchOS Using SwiftUI and SpriteKit
Adapting Conway’s Game of Life on watchOS Using SwiftUI and SpriteKit
When I set out to bring Conway’s Game of Life—a cellular automaton simulation—to the Apple Watch, I wanted to leverage the capabilities of SpriteKit and SwiftUI to create an interactive and visually appealing experience on such a small screen. In this post, I’ll walk through how I adapted the game for watchOS and tackled key challenges in the process.
The Challenge: Conway’s Game of Life on a Tiny Screen
Conway’s Game of Life is a zero-player game that evolves based on an initial state. Each cell can either be alive or dead, and the state of the grid changes with each iteration based on a set of rules about its neighboring cells. Adapting this to the Apple Watch involved a few constraints:
- Limited Screen Real Estate: The Apple Watch has a small display, so managing the size of the grid and the cells was key.
- Performance Considerations: watchOS, though powerful, has performance limitations—especially when rendering animations or updating many sprites in real time.
Technologies Used
- SpriteKit: For rendering the cells as sprites and handling their visual states.
-
SwiftUI: To create a modern, reactive interface and easily integrate
SpriteView
into the watchOS environment.
Step 1: Setting Up the Grid Using SpriteKit
To represent the grid in Conway’s Game of Life, I used a 2D array where each element represented a cell. Each cell was rendered as an SKSpriteNode
. I chose to color live cells differently from dead cells to give the game a clear, visual distinction.
Here’s an example of how I set up the cells in GameScene.swift
:
import SpriteKit
class GameScene: SKScene {
let numberOfRows = 10
let numberOfColumns = 10
var cellGrid: [[SKSpriteNode]] = []
override func didMove(to view: SKView) {
let cellSize = CGSize(width: size.width / CGFloat(numberOfColumns),
height: size.height / CGFloat(numberOfRows))
for row in 0..<numberOfRows {
var rowSprites: [SKSpriteNode] = []
for col in 0..<numberOfColumns {
let sprite = SKSpriteNode(color: .black, size: cellSize)
sprite.position = CGPoint(x: CGFloat(col) * cellSize.width + cellSize.width / 2,
y: size.height - CGFloat(row) * cellSize.height - cellSize.height / 2)
addChild(sprite)
rowSprites.append(sprite)
}
cellGrid.append(rowSprites)
}
}
}
Step 2: Applying Conway’s Rules
I implemented Conway’s rules to determine whether each cell lives or dies in the next iteration. To do this, I checked the neighboring cells for each grid element and calculated the number of living neighbors. Based on this, I updated the grid state:
func applyConwaysRules() {
var newGrid = cellGrid
for row in 0..<numberOfRows {
for col in 0..<numberOfColumns {
let livingNeighbors = countLivingNeighbors(row: row, col: col)
let isAlive = cellGrid[row][col].color == .green
if isAlive && (livingNeighbors < 2 || livingNeighbors > 3) {
newGrid[row][col].color = .black // Dies
} else if !isAlive && livingNeighbors == 3 {
newGrid[row][col].color = .green // Becomes alive
}
}
}
cellGrid = newGrid
}
This logic runs continuously in a loop, updating the state of the grid and the colors of the sprites accordingly.
Step 3: Displaying the Game in SwiftUI Using SpriteView
One of the best parts of developing for watchOS is the simplicity of integrating SpriteKit with SwiftUI using SpriteView
. I wrapped the GameScene
in a SpriteView
and included it in my main SwiftUI view:
import SwiftUI
struct ContentView: View {
var body: some View {
SpriteView(scene: createGameScene())
.frame(width: 200, height: 200)
}
func createGameScene() -> SKScene {
let scene = GameScene(size: CGSize(width: 200, height: 200))
scene.scaleMode = .resizeFill
return scene
}
}
Step 4: Adding Tap Interaction
To make the game interactive, I added touch input. I captured the tap location on the grid and toggled the state of the tapped cell, allowing users to set the initial configuration.
func handleTap(at location: CGPoint) {
let tappedNodes = nodes(at: location)
if let tappedNode = tappedNodes.first as? SKSpriteNode {
tappedNode.color = (tappedNode.color == .green) ? .black : .green
}
}
Step 5: Optimizing for watchOS Performance
Since real-time animation and updates can be taxing on a watch, I optimized the game by:
- Reducing the size of the grid to limit the number of cells being updated each iteration.
- Limiting the update frequency to give the processor time to handle other tasks.
- Testing on physical hardware to ensure the performance is smooth.
Conclusion
Bringing Conway’s Game of Life to watchOS was a fascinating challenge, requiring careful balancing between functionality, performance, and user experience. With SpriteKit and SwiftUI working seamlessly together, I was able to create a functional and visually compelling game on one of Apple’s most constrained devices.
The result is an engaging, interactive simulation that can run smoothly on an Apple Watch, while still providing the core experience of Conway’s Game of Life. I hope this inspires you to explore more creative possibilities with SpriteKit and SwiftUI on watchOS!
By focusing on performance and integrating modern frameworks like SwiftUI and SpriteKit, you can build captivating experiences even on constrained devices like the Apple Watch!