Select Page

# 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()
boxNode.isHidden = !isAlive
}

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)
}

// ...

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
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,+)
case 0 ... 3:
row.append(false)
case 4 ... 6:
if let isAlive = get(x, y, z) {
if isAlive {
row.append(true)
} else {
}
} 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?

// 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

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
{"ft_lang_label":"__label__en","ft_lang_prob":0.6242996,"math_prob":0.97071904,"size":10285,"snap":"2020-45-2020-50","text_gpt3_token_len":2923,"char_repetition_ratio":0.11633109,"word_repetition_ratio":0.09288654,"special_character_ratio":0.29674283,"punctuation_ratio":0.19952941,"nsfw_num_words":0,"has_unicode_error":false,"math_prob_llama3":0.97224367,"pos_list":[0,1,2],"im_url_duplicate_count":[null,4,null],"WARC_HEADER":"{\"WARC-Type\":\"response\",\"WARC-Date\":\"2020-12-04T10:31:56Z\",\"WARC-Record-ID\":\"<urn:uuid:a15209ef-ecac-4eff-8e19-ea454cf4dc92>\",\"Content-Length\":\"89360\",\"Content-Type\":\"application/http; msgtype=response\",\"WARC-Warcinfo-ID\":\"<urn:uuid:0f15c0a9-dbc4-4648-9316-66e4c77dabc0>\",\"WARC-Concurrent-To\":\"<urn:uuid:cd703e4e-ec12-4d77-be58-c685eecec9ab>\",\"WARC-IP-Address\":\"104.198.109.137\",\"WARC-Target-URI\":\"https://www.clientresourcesinc.com/2018/10/11/arkit-cube-game/\",\"WARC-Payload-Digest\":\"sha1:QOCK7PCYDGGQDLK2UNIYHXKHU2H4UJEP\",\"WARC-Block-Digest\":\"sha1:5E3J4KDL5NGU3JSC4UHTVKO2J65X3YOG\",\"WARC-Identified-Payload-Type\":\"text/html\",\"warc_filename\":\"/cc_download/warc_2020/CC-MAIN-2020-50/CC-MAIN-2020-50_segments_1606141735600.89_warc_CC-MAIN-20201204101314-20201204131314-00357.warc.gz\"}"}