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

GOL test

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!