+1 Share 0 Shares

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.

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.