康威的生命游戏

The Game of Life 是一款零玩家游戏,最初是为二维设计的。在生命游戏中,有一个网格包含许多活的或死的细胞。游戏自行进行,并遵循四个重要规则。如果一个活细胞的邻居少于两个,它就会死亡。如果一个活细胞有两到三个邻居,它就会存活。如果一个死细胞正好有三个邻居,它就会变成活细胞。最后,如果一个活细胞有超过三个邻居,它就会死亡。这些是生命游戏的基本规则。请随时通过以下链接阅读更多关于这个项目的精彩游戏的信息:

创建项目

对于这个项目,我们将首先创建一个 ARKit Swift Xcode 项目。单击增强现实应用程序选择任何名称和位置后,请记住选择 Scenekit 作为图形框架。

创建 AR 应用模板

创建单元格

首先让我们创建一个名为 CellOfLife.swift 的新 Swift 文件。对于这个类,我们需要一个框来表示单元格,框的颜色,以及单元格是否活着。

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

创建立方体

现在我们创建了单元格,我们现在可以创建多维数据集了!让我们创建一个新的 Swift 文件并将其命名为 CubeOfLife.swift。对于这个类,我们将需要一个用于我们刚刚创建的细胞的三个基本数组,一个用于判断它们是否还活着。首先让我们添加这个类所需的变量。

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 函数

对于 SetupLife 函数,我们希望随机生成一个立方体或接收活细胞位置。

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

快速助手功能

为了从数组中检索单元格,我们不想让索引越界!因此,我们将创建一个辅助函数来获取值,并确保我们不会尝试获取不在数组中的值。

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
        
    }

建造盒子

现在我们将创建构建函数。对于这个函数,它应该只运行一次,所以它会触发我们的 isBuilt 标志。

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

更新多维数据集

接下来,我们将创建一个简单的函数来遍历多维数据集并更新所有单元格。

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 方法。对于这种方法,我们将遍历每个单元格并获取该单元格的所有邻居。然后我们将获得邻居的总和。对于生命的三维游戏,我们的规则如下:

  • 如果一个活细胞有 0 - 3 个邻居,它就会死亡
  • 如果一个 Alive Cell 有 4 – 6 个邻居,它将存活
  • 如果一个死细胞有 4 个邻居,它就会变成活细胞
  • 如果一个活细胞有 > 6 个邻居,它就会死亡
   @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()
    }

创造生活!

首先,我们将进入 ViewController.swift 并更改 viewDidLoad 以加载一个空白场景。我们还想将 World Tracking 的 planeDetection 设置为水平的。

import UIKit
import SceneKit
import ARKit

// Based of off: //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)
    }
    
// ...
}

渲染器 ARSCNViewDelegate

接下来我们需要使用 ARSCNViewDelegate 中的渲染器方法。为此,我们想查看传递的锚点是否为 ARPlaneAnchor,然后我们将构建创建 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 函数来构建立方体。

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

最终结果

完成后,您应该有以下结果:

如果您遇到任何麻烦或想查看其发布的源代码 GitHub

如果您想了解有关 CRi 如何帮助您的更多信息 IOS development please contact 我们的队伍 or contact us at [email protected].

迅速 开发人员,热衷于将咖啡变成代码。目前正在使用 MetalKit、ARKit 和 SceneKit 深入研究 Objective-C 和 Swift。欢迎关注我 GitLab 或加入我的开源敏捷开发团队 独角兽.

接触