Conway’s Game of Life

The Game of Life is a zero player game which was originaly intended for two-dimensional. In the Game of Life, there is a grid that contains many cells which are either alive or dead. The game is played by itself and follows four important rules. If an alive cell has less than two neighbors, it will die. If an alive cell has two to three neighbors, it will live. If a dead cell has exactly three neighbors, it will become alive. Finally if an alive cell has more than three neighbors, it will die. These are the basic rules of the Game of Life. Feel free to read more about the amazing game that ispired this project at these links:

Creating the Project

For this project we will start by creationg a ARKit Swift Xcode project. Once you have clicked Augmented Reality App choose any name and location, keep in mind to select Scenekit as your graphical framework.

Create a AR App template

Creating the Cell

First let us create a new Swift File named CellOfLife.swift. For this class we will want to have a box to represent the cell, a color that the box will be, and if the cell is alive or not.

import SceneKit

class CellOfLife: SCNNode {
    // A default alive color the cells will use
    private let aliveColor = UIColor.white.withAlphaComponent(0.75)
    // The box that will represent the cell
    private var boxNode: SCNNode
    // A color that can be set and the box will use
    public var color: UIColor? {
        didSet {
            self.boxNode.geometry?.firstMaterial?.diffuse.contents = color ?? aliveColor
        }
        
    }
    // If the cell is dead the box will be hidden
    public var isAlive: Bool {
        didSet {
            boxNode.isHidden = !isAlive
        }
    }
    
    // Creates a cell with a SCNBox
    init(isAlive alive: Bool, nodeWidth: CGFloat, nodeHeight: CGFloat) {
        let box = SCNBox(width: nodeWidth, height: nodeHeight, length: nodeWidth, chamferRadius: 0)
        // Set the firstMaterial to the aliveColor
        box.firstMaterial?.diffuse.contents = aliveColor
        boxNode = SCNNode(geometry: box)
        isAlive = alive
        super.init()
        addChildNode(boxNode)
        boxNode.isHidden = !isAlive
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Creating the Cube

Now that we created the cells we can now create the cube! Let’s create a new Swift File and name it CubeOfLife.swift. For this class we will need a three dementinal array for cells we just create and one for just if they are alive or not. To start let us add the variable we will need for this class.

import SceneKit

class CubeOfLife: SCNNode {
    var life: [[[Bool]]] = []
    var cellsOfLife: [[[CellOfLife]]] = []
    var size: Int
    var zSize: Int
    var width: CGFloat
    var height: CGFloat
    var isBuilt = false

    init(n: Int, width: CGFloat, height: CGFloat, withAliveCells cells: [float3]? = nil, nHeight: Int = 5) {
        self.size = n
        self.zSize = nHeight
        self.width = width
        self.height = height
        super.init()
        setupLife(withAliveCells: cells)
    }

// ...

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

SetupLife Function

For the SetupLife function we want to randomaly generate a cube or take in alive cell loctions.

private func setupLife(withAliveCells cellLocations: [float3]? = nil) {
        for x in (0 ..< size) {
            var plane: [[Bool]] = []
            for y in (0 ..< size) {
                var row: [Bool] = []
                for z in (0 ..< zSize) {
                    if let cells = cellLocations {
                        // Center the Location
                        let count = cells.filter { Int($0.x) + Int(size / 2) == x &&
                                                    Int($0.y) + Int(size / 2) == y &&
                                                    Int($0.z + 1) == z }
                        row.append(!count.isEmpty)
                        
                    } else {
                        // Random!
                        row.append(Bool.random())
                    }
                }
                plane.append(row)
            }
            life.append(plane)
        }
    }

Quick Helper Functions

For retreiving the cells from the array we don’t want to get a index out of bounds! So we will make a helper function to get the values and make sure that we are not trying to get a value that is not in the array.

private func get(_ x: Int, _ y: Int, _ z: Int) -> Bool? {
        if x > 0, y > 0, z > 0, x < size, y < size, z < zSize {
            let value = life[x][y][z]
            
            return value
        }
        return nil
        
    }
    
    private func get(_ x: Int, _ y: Int, _ z: Int, from: [[[Bool]]]) -> Bool? {
        if x > 0, y > 0, z > 0, x < from.count, y < from.count, z < from.count {
            let value = from[x][y][z]
            
            return value
        }
        return nil
        
    }

Building the Boxes

Now we will create the build function. For this function, it should only run once so it will trigger our isBuilt flag.

func build() {
        for x in (0 ..< size) {
            var plane: [[CellOfLife]] = []
            for y in (0 ..< size) {
                var row: [CellOfLife] = []
                for z in (0 ..< zSize) {
                    // Get if the cell is alive
                    let isAlive = life[x][y][z]
                    // Get the width and height
                    let nodeWidth = width / CGFloat(size)
                    let nodeHeight = height / CGFloat(size)
                    // Create the basic cell
                    let cell = CellOfLife(isAlive: isAlive, nodeWidth: nodeWidth, nodeHeight: nodeHeight)
                    // Set the postion for the cell
                    cell.position =  SCNVector3((CGFloat(x) * nodeWidth) - width / 2, (CGFloat(y) * nodeHeight) - width / 2, CGFloat(z) * nodeWidth)
                    // Calculate the distance from the center
                    let node1Pos = SCNVector3ToGLKVector3(cell.position)
                    let node2Pos = SCNVector3ToGLKVector3(SCNVector3(CGFloat(position.x) + nodeWidth / 2, CGFloat(position.y) + nodeHeight / 2, CGFloat(position.z) + nodeWidth / 2))
                    let distance = GLKVector3Distance(node1Pos, node2Pos)
                    // Set the color of the box
                    let color = UIColor(red: CGFloat(255 - (x * 10)) / 255.0, green: CGFloat(255 - (y * 10)) / 255.0, blue: CGFloat(255 - (z * 10)) / 255.0, alpha: CGFloat(1 - distance))
                    cell.color = color
                    // Add the cell to the cube of life
                    addChildNode(cell)
                    row.append(cell)
                }
                
                plane.append(row)
            }
            cellsOfLife.append(plane)
        }
        // The cube has been built
        isBuilt = true
        // Start the timer
        Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(tick), userInfo: nil, repeats: true)
    }

Updating the Cube

Next we will create a simple function to go through the cube and update all the cells.

func update() {
        for x in (0 ..< size) {
            for y in (0 ..< size) {
                for z in (0 ..< zSize) {
                    let cell = cellsOfLife[x][y][z]
                    let isAlive = life[x][y][z]
                    cell.isAlive = isAlive
                }
            }
        }
    }

Tick for the Timer

Finaly the last function we need to create it the tick method that happens everytime the timer fires. For this method we will go through ever cell and get all of the neighbors of that cell. We will then get the sum of the neighbors alive. For the three dimentional Game of Life our rules will be the following:

  • If an Alive Cell has 0 – 3 neighbors it will die
  • If an Alive Cell has 4 – 6 neighbors it will live
  • If a Dead Cell has 4 neighbors it will become alive
  • If an Alive Cell has > 6 neighbors it will die
   @objc
    func tick() {
        var newGen: [[[Bool]]] = []
        for x in (0 ..< size) {
            var plane: [[Bool]] = []
            for y in (0 ..< size) {
                var row: [Bool] = []
                for z in (0 ..< zSize) {
                    let neighbors: [Bool?] = [
                        // Bottom
                        get(x-1, y-1, z-1),
                        get(x, y-1, z-1),
                        get(x, y, z-1),
                        get(x, y+1, z-1),
                        get(x+1, y+1, z-1),
                        get(x-1, y+1, z-1),
                        get(x+1, y-1, z-1),
                        get(x-1, y, z-1),
                        get(x+1, y, z-1),
                        // Sides
                        get(x-1, y-1, z),
                        get(x, y-1, z),
                        get(x, y+1, z),
                        get(x+1, y+1, z),
                        get(x-1, y+1, z),
                        get(x+1, y-1, z),
                        get(x-1, y, z),
                        get(x+1, y, z),
                        // Top
                        get(x-1, y-1, z+1),
                        get(x, y-1, z+1),
                        get(x, y, z+1),
                        get(x, y+1, z+1),
                        get(x+1, y+1, z+1),
                        get(x-1, y+1, z+1),
                        get(x+1, y-1, z+1),
                        get(x-1, y, z+1),
                        get(x+1, y, z+1),
                        ]
                    
                    let neighborsSum = neighbors.compactMap { $0 }.map{ $0 ? 1 : 0 }.reduce(0,+)
                    switch neighborsSum {
                    case 0 ... 3:
                        row.append(false)
                    case 4 ... 6:
                        if let isAlive = get(x, y, z) {
                            if isAlive {
                                row.append(true)
                            } else {
                                row.append(neighborsSum == 4)
                            }
                        } else {
                            row.append(false)
                        }
                    default:
                        row.append(false)
                    }
                }
                plane.append(row)
            }
            newGen.append(plane)
        }
        life = newGen
        update()
    }

Creating Life!

First we will go into the ViewController.swift and change the viewDidLoad to load a blank scene. We also want to set the planeDetection for the World Tracking to be horizontal.

import UIKit
import SceneKit
import ARKit

// Based of off: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life

class ViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet var sceneView: ARSCNView!
    private var isSpawned = false
    private var cube: CubeOfLife?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Set the view's delegate
        sceneView.delegate = self
        
        // Show statistics such as fps and timing information
        sceneView.showsStatistics = true
        
        // Set the scene to the view
        sceneView.scene = SCNScene()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // Create a session configuration
        let configuration = ARWorldTrackingConfiguration()
        
        configuration.planeDetection = .horizontal

        // Run the view's session
        sceneView.session.run(configuration)
    }
    
// ...
}

Renderer ARSCNViewDelegate

Next we need to use the renderer method from the ARSCNViewDelegate. For this we want to see if the anchor passed is a ARPlaneAnchor and then we will build create the CubeOfLife.

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        if !isSpawned {
            if let planeAnchor = anchor as? ARPlaneAnchor {
                // Create plane
                let planeWidth = CGFloat(planeAnchor.extent.x)
                let planeHeight = CGFloat(planeAnchor.extent.z)
                
                let plane = SCNPlane(width: planeWidth, height: planeHeight)
                
                let planeNode = SCNNode()
                planeNode.position = SCNVector3(planeAnchor.center.x, 0, planeAnchor.center.z)
                planeNode.transform = SCNMatrix4MakeRotation(-Float.pi / 2, 1, 0, 0)
                plane.firstMaterial?.diffuse.contents = UIColor.black.withAlphaComponent(0.75)
                planeNode.geometry = plane
                
                // Create Cube of Life
                cube = CubeOfLife(n: 10, width: planeWidth / 2, height: planeWidth / 2, nHeight: 10)
                cube?.position = planeNode.position
                
                planeNode.addChildNode(cube!)
                
                node.addChildNode(planeNode)
                isSpawned.toggle()
            }
        }
    }

TouchesEnded

Finally we will let the user touch the screen to spawn the cube. We will override the touchesEnded function to build the cube once we have the plane spawned in augmented reality.

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        if cube?.isBuilt ?? false {
            cube?.tick()
        } else {
            cube?.build()
        }
    }

Final Results

Once you are done, you should have the following results:

If you have had any troubles or want to look at the source code it is posted on GitHub

If you’d like to learn more about how CRi can help you with IOS development please contact our team or contact us at gig@clientresourcesinc.com.

Swift Developer with a passion to turn coffee into code. Currently diving into Objective-C and Swift using MetalKit, ARKit, and SceneKit. Feel free to follow me on GitLab or join my open source agile development team oneleif.

Contact