Agents, Goals, and Behaviorsを3次元でやってみた

はじめに

以前GKObstacleで障害物を作るの記事でAgents, Goals, and BehavioursをSpriteKitを使って2次元実装したんですが今回はSceneKitとARKitを使って3次元で実装してみました。

コード

障害物なし

ObstackeなしでGKGoalに向かって移動させるだけのサンプルです。

Playerに向かってenemyが追尾していきます。障害物の配置はしていないので縦方向にだけ移動が発生します。

class ViewController: UIViewController {
    @IBOutlet weak var sceneView: ARSCNView!{
        didSet {
            sceneView.delegate = self
        }
    }
    
    var prevTime: TimeInterval = 0
    let playerAgent = GKAgent3D()
    let agentSystem = GKComponentSystem(componentClass: GKAgent3D.self)
    
    var enemyNode: SCNNode = {
        let node = SCNNode()
        node.geometry = SCNSphere(radius: 0.1)
        let material = SCNMaterial()
        node.geometry?.materials = [material]
        node.position = SCNVector3(0, 0, -10)
        return node
    }()
    
    var playerNode: SCNNode = {
        let node = SCNNode()
        node.geometry = SCNSphere(radius: 0.1)
        let material = SCNMaterial()
        material.diffuse.contents = UIColor.red
        node.geometry?.materials = [material]
        node.position = SCNVector3(0, 0, -10)
        
        //アニメーション
        let upAction = SCNAction.move(to:  SCNVector3(0, 1, -10), duration: 2)
        let downAction = SCNAction.move(to:  SCNVector3(0, -1, -10), duration: 2)
        let action = SCNAction.sequence([upAction, downAction])
        node.runAction(SCNAction.repeatForever(action))
        return node
    }()
    
    lazy var enemyAgent: GKAgent3D = {
        let agent = GKAgent3D()
        agent.maxAcceleration = 1
        agent.maxSpeed = 1
        agent.position = SIMD3<Float>(enemyNode.position.x,
                                      enemyNode.position.y,
                                      enemyNode.position.z)
        agent.delegate = self
        agent.behavior = GKBehavior(goals: [GKGoal(toSeekAgent: playerAgent)])
        return agent
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
                
        sceneView.scene.rootNode.addChildNode(playerNode)
        sceneView.scene.rootNode.addChildNode(enemyNode)
        agentSystem.addComponent(enemyAgent)
        
        let configuration = ARWorldTrackingConfiguration()
        configuration.isLightEstimationEnabled = true
        sceneView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
    }
}

extension ViewController: GKAgentDelegate {
    func agentDidUpdate(_ agent: GKAgent) {
        if let agent = agent as? GKAgent3D {
            enemyNode.position = SCNVector3(agent.position)
        }
    }
}

extension ViewController: ARSCNViewDelegate {
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        let delta = prevTime == 0 ? 0 : time - prevTime
        prevTime = time
        agentSystem.update(deltaTime: delta)
        // playerAgentの位置をplayerの座標に更新する
        playerAgent.position = SIMD3<Float>(x: playerNode.position.x,
                                            y: playerNode.position.y,
                                            z: playerNode.position.z)
    }
}

障害物あり

中心にある緑の球を障害物として登録しているので追尾している白い四角形は球にぶつからないように周りを周回しています。前後の位置が変わっていることに注目です。

import UIKit
import ARKit
import SceneKit
import GameplayKit

class ViewController: UIViewController {
    @IBOutlet weak var sceneView: ARSCNView!{
        didSet {
            sceneView.delegate = self
        }
    }
    
    var prevTime: TimeInterval = 0
    let playerAgent = GKAgent3D()
    let agentSystem = GKComponentSystem(componentClass: GKAgent3D.self)
    
    var enemyNode: SCNNode = {
        let node = SCNNode()
        node.geometry = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0)
        let material = SCNMaterial()
        node.geometry?.materials = [material]
        node.position = SCNVector3(0, 0, -10)
        return node
    }()
    
    var playerNode: SCNNode = {
        let node = SCNNode()
        node.geometry = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0)
        let material = SCNMaterial()
        material.diffuse.contents = UIColor.red
        node.geometry?.materials = [material]
        node.position = SCNVector3(0, 0, -5)
        
        //アニメーション
        let upAction = SCNAction.move(to:  SCNVector3(0, 3, -5), duration: 3)
        let downAction = SCNAction.move(to:  SCNVector3(0, -3, -5), duration: 3)
        let action = SCNAction.sequence([upAction, downAction])
        
        
        node.runAction(SCNAction.repeatForever(action))
        return node
    }()
    
    var obstacleSphereNode: SCNNode = {
        let node = SCNNode()
        
        node.geometry = SCNSphere(radius: 0.1)
        let material = SCNMaterial()
        material.diffuse.contents = UIColor.green
        node.geometry?.materials = [material]
        node.position = SCNVector3(0, 0, -5)
        return node
    }()
    
    lazy var enemyAgent: GKAgent3D = {
        let agent = GKAgent3D()
        agent.maxAcceleration = 1
        agent.maxSpeed = 5
        agent.position = SIMD3<Float>(enemyNode.position.x,
                                      enemyNode.position.y,
                                      enemyNode.position.z)
        agent.delegate = self
        
        //障害物
        let obstacle = GKSphereObstacle(radius: 0.2)
        obstacle.position = vector_float3(0, 0, -5)
        agent.behavior = GKBehavior(goals: [
            GKGoal(toSeekAgent: playerAgent),
            GKGoal(toAvoid: [obstacle], maxPredictionTime: 2.0)
            ])

        return agent
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
                
        sceneView.scene.rootNode.addChildNode(playerNode)
        sceneView.scene.rootNode.addChildNode(enemyNode)
        sceneView.scene.rootNode.addChildNode(obstacleSphereNode)
        agentSystem.addComponent(enemyAgent)
        
        let configuration = ARFaceTrackingConfiguration()
        configuration.isLightEstimationEnabled = true
        sceneView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
    }
}

extension ViewController: GKAgentDelegate {
    func agentDidUpdate(_ agent: GKAgent) {
        if let agent = agent as? GKAgent3D {
            enemyNode.position = SCNVector3(agent.position)
        }
    }
}

extension ViewController: ARSCNViewDelegate {
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        let delta = prevTime == 0 ? 0 : time - prevTime
        prevTime = time
        agentSystem.update(deltaTime: delta)
        
        playerAgent.position = SIMD3<Float>(x: playerNode.position.x,
                                            y: playerNode.position.y,
                                            z: playerNode.position.z)
    }
}

オブジェクトを増やしてみる

前回もやりましたが単体では綺麗じゃないのでたくさんに増やして動かしてみました。

数が増えるだけでだいぶカッコ良くなります、GamePlayKitはマイナーなライブラリでだいぶ忘れ去られていますがAppleがARに力を入れているので凝った表現をさくっと作りたい時はすごく便利だなと思います。

import UIKit
import ARKit
import SceneKit
import GameplayKit

class ViewController: UIViewController {
    @IBOutlet weak var sceneView: ARSCNView!{
        didSet {
            sceneView.delegate = self
        }
    }
    
    var prevTime: TimeInterval = 0
    let playerAgent = GKAgent3D()
    let agentSystem = GKComponentSystem(componentClass: GKAgent3D.self)
    
    var playerNode: SCNNode = {
        let node = SCNNode()
        node.geometry = SCNSphere(radius: 0.1)
        let material = SCNMaterial()
        material.diffuse.contents = UIColor.red
        node.geometry?.materials = [material]
        node.position = SCNVector3(0, 0, -8)
        
        //アニメーション
        let upAction = SCNAction.move(to:  SCNVector3(0, 3, -8), duration: 4)
        let downAction = SCNAction.move(to:  SCNVector3(0, -3, -8), duration: 4)
        let action = SCNAction.sequence([upAction, downAction])
        
        
        node.runAction(SCNAction.repeatForever(action))
        return node
    }()
    
    var obstacleSphereNode: SCNNode = {
        let node = SCNNode()
        
        node.geometry = SCNSphere(radius: 0.1)
        let material = SCNMaterial()
        material.diffuse.contents = UIColor.green
        node.geometry?.materials = [material]
        node.position = SCNVector3(0, 0, -8)
        return node
    }()
    
    var enemys: [SCNNode] = []
    var enemyAgents:[GKAgent] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        createEnemy(count: 200)
        sceneView.scene.rootNode.addChildNode(playerNode)
        enemys.forEach{sceneView.scene.rootNode.addChildNode($0)}
        sceneView.scene.rootNode.addChildNode(obstacleSphereNode)
        
        let configuration = ARWorldTrackingConfiguration()
        configuration.isLightEstimationEnabled = true
        configuration.environmentTexturing = .automatic
        sceneView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
    }
    
    private func createEnemy(count: Int) {
        for _ in 0..<count {
            let enemyAgent: GKAgent3D = {
                let enemy: SCNNode = {
                    let node = SCNNode()
                    node.geometry = SCNSphere(radius: 0.05)
                    let material = SCNMaterial()
                    material.lightingModel = .physicallyBased // 物理ベースのレンダリング
                    material.metalness.contents = 1.0
                    material.metalness.intensity = 1.0
                    material.roughness.intensity = 0.0
                    material.diffuse.contents = UIColor.white
                    node.geometry?.materials = [material]
                    node.position = SCNVector3(CGFloat.random(in: -10...10), CGFloat.random(in: -10...10), -10)
                    return node
                }()
                
                let agent = GKAgent3D()
 
                agent.maxAcceleration = Float.random(in: 0.0...3.0)
                agent.maxSpeed = Float.random(in: 7.0...10.0)
                agent.position = SIMD3<Float>(enemy.position.x,
                                              enemy.position.y,
                                              enemy.position.z)
                agent.delegate = self
                
                //障害物
                let obstacle = GKSphereObstacle(radius: 0.2)
                obstacle.position = vector_float3(0, 0, -8)
                agent.behavior = GKBehavior(goals: [
                    GKGoal(toSeekAgent: playerAgent),
                    GKGoal(toAvoid: [obstacle], maxPredictionTime: 2.0)
                    ])


                enemys.append(enemy)
                return agent
            }()
            enemyAgents.append(enemyAgent)
            agentSystem.addComponent(enemyAgent)
        }
    }
}

extension ViewController: GKAgentDelegate {
    func agentDidUpdate(_ agent: GKAgent) {
        if let agent = agent as? GKAgent3D,
            let index = enemyAgents.firstIndex(where: { $0 == agent }) {
            let enemy = enemys[index]
            enemy.position = SCNVector3(agent.position)
        }
    }
}

extension ViewController: ARSCNViewDelegate {
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        let delta = prevTime == 0 ? 0 : time - prevTime
        prevTime = time
        agentSystem.update(deltaTime: delta)
        
        playerAgent.position = SIMD3<Float>(x: playerNode.position.x,
                                            y: playerNode.position.y,
                                            z: playerNode.position.z)
    }
}