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