Apple Engine

Apple, iPhone, iOS, その周辺のことについて

WWDC 2017 の SceneKit サンプル Fox 2 を調べる その18

前回に続き BaseComponent.swift の前にそこで使用している GameplayKit の「Agents, Goals, and Behaviors」について見てゆく。

 

Agents, Goals, and Behaviors とは?

エージェントに対して、なんらかの振る舞いと目標までのゴールを設定すると、エージェントはゴールに対してその動作を行うという機能。
Fox2 ではプレイヤーキャラの Max が近づくと敵が接近、離れたり、Max が遠ざかると一定の場所の周りを動いたりする。 この機能の計算結果から敵の移動座標値を取得しノードへ座標を渡す。

使用するエージェントは X, Y 軸の座標を使用する 2D と X, Y, Z 軸の座標を使用する 3D があり、 Fox2 では敵が高低差のない所にいるため 2D を使用している。

 

実装の流れ

  • GKAgent でエージェントをつくる
  • ひとつまたは複数の GKGoal でゴールまでの振る舞いを設定する
  • ゴールまで何かをさせたい GKAgent に対して GKBehavior で GKGoal とゴールとなる GKAgent を設定する。

 

ゴールへの振る舞いの種類

一応、振る舞いのまとめ。
GKGoal のイニシャライズ時に設定する。

 

一般的な動きの振る舞い

init メソッド 説明
init(toSeekAgent: GKAgent) 指定したエージェントの位置に向かって移動する
init(toFleeAgent: GKAgent) 指定したエージェントの位置に離れるように移動する
init(toReachTargetSpeed: Float) 指定した値の速度まで加速や減速する
init(toWander: Float) 指定した値の速度で、さまよったり、前進やランダムにまわったりする

 

回避や追跡の振る舞い

init メソッド 説明
init(toAvoid: [GKAgent], maxPredictionTime: TimeInterval) 他のエージェントの動作を考慮して、指定されたエージェントと衝突しないようなゴールを設定する
init(toAvoid: [GKObstacle], maxPredictionTime: TimeInterval) エージェントが指定された静的障害(GKObstacle)と衝突しないようにするためのゴールを設定する
init(toInterceptAgent: GKAgent, maxPredictionTime: TimeInterval) ターゲットの動きを考慮して、エージェントが指定された他のエージェントを追いかけることを目的としたゴールを設定する

 

群れの振る舞い

init メソッド 説明
init(toSeparateFrom: [GKAgent], maxDistance: Float, maxAngle: Float) 他のエージェントのグループから指定された距離を維持するゴールを設定する
init(toAlignWith: [GKAgent], maxDistance: Float, maxAngle: Float) 他のエージェントのグループから指定された方向を整列させるゴールを設定する
init(toCohereWith: [GKAgent], maxDistance: Float, maxAngle: Float) 他のエージェントのグループから指定された位置の近くに留まるゴールを設定する

 

パスに沿って動く振る舞い

init メソッド 説明
init(toStayOn: GKPath, maxPredictionTime: TimeInterval) 指定されたパス内でエージェントの位置を維持するゴールを設定する
init(toFollow: GKPath, maxPredictionTime: TimeInterval, forward: Bool) 指定されたパスに沿うようにエージェントの位置をゴールとして設定する

 

 

簡単なサンプルつくってみる

f:id:x67x6fx74x6f:20180529193349g:plain

SceneKit のテンプレートの宇宙船を半径 20 で回転。
球(SCNShpere)に GKGoal の toSeekAgent を設定し宇宙船を追いかける。

 

初期設定

新規で SceneKit テンプレート のプロジェクトを作成。 前回の方法で GamaplayKit をインポートする。

 

ship.scn の修正

ship と shipMesh の間の階層に空のノードを追加し、X座標を -20 にする。

f:id:x67x6fx74x6f:20180530120357p:plain

 

GameViewController の修正

GameplayKit と SCNSceneRenderer をいじるのため、インポートとGameViewController クラス宣言の UIViewController の横にイカを追加。

import GameplayKit
class GameViewController: UIViewController, SCNSceneRendererDelegate {
    ...
}

 

メンバ変数

追いかける元となる宇宙船のエージェントと agent1 と、追いかける球のエージェント agent2、shipNode 宇宙船の移動位置を参照するノードと球ののノードをクラス内で参照できるようにメンバ変数にする。 差分の時間を保持するため TimeInterval も設定する。

var agent1 = GKAgent2D()
var agent2 = GKAgent2D()

var shipNode:SCNNode!
var shipNode:SCNNode!

var previousUpdateTime: TimeInterval = 0

 

カメラ位置変更

宇宙船が半径 20 出回るためカメラの位置と回転を変更

cameraNode.position = SCNVector3(x: 0, y: 10, z: 60)
cameraNode.eulerAngles = SCNVector3(-0.125,0,0)

 

宇宙船のアニメーション変更

宇宙船のアニメーションが速すぎるため、duration の 1 を 3 に変更。  

ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 3)))

 

ノードの初期設定

ship.runAction の下に ship のシーンファイルで設定した shipNode を設定する。

shipNode = ship.childNode(withName: "shipNode", recursively: true)

 

追いかける球の設定。

ballNode = SCNNode(geometry: SCNSphere(radius: 3))
ballNode.position = SCNVector3(0,0,0)
scene.rootNode.addChildNode(ballNode)

 

エージェント初期設定

agent1 は宇宙船の位置を保持するエージェントの設定をする。

agent1.position = float2(shipNode.worldPosition.x, shipNode.worldPosition.z)

 

agent2 は宇宙船を元に追いかけるエージェントの設定をする。

agent2.position = float2(0, 0)
agent2.mass = 0.3
agent2.maxAcceleration = 16
agent2.maxSpeed = 30
agent2.behavior = GKBehavior(goal: GKGoal(toSeekAgent: agent1), weight: 1)

 

SCNSceneRendererDelegate

再生毎に処理する SCNSceneRendererDelegate を設定する。

ひとまず、SCNView で delegate を設定。

scnView.delegate = self

 

viewDidLoad の設定は終了。

カッコを抜け renderer の関数を作成。

その内で calcTime で経過時間を設定。 agent1 に shipNode の X、Z 座標を設定し、agent2 で update 関数を呼ぶと追跡している位置が更新される。

その後、ballNode に agent2 の位置を渡し移動させる

最後に現在の時間を previousUpdateTime に渡す。

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    
    var calcTime: TimeInterval = 0
    
    if previousUpdateTime != 0 {
        calcTime = time - previousUpdateTime
    }

    agent1.position = float2(shipNode.worldPosition.x, shipNode.worldPosition.z)
    agent2.update(deltaTime: calcTime)

    ballNode.position = SCNVector3(agent2.position.x, 0, agent2.position.y)
    
    print("agent1: \(agent1.position)")
    print("agent2: \(agent2.position)")
    
    previousUpdateTime = time
}

 

ビルドすると、球体が宇宙船を追っかける

 

サンプルファイル

github.com

 

まとめ

ひとまず、GKAgent2D を軽く動かすサンプルを作成してみた。

このレベルだと、SCNAccelerationConstraint と SCNDistanceConstraint を使用した方がつくるのは速いのだが、 Fox2 のように複数の振る舞いを遷移させたり、ミックスさせたりするのに便利だし、GKConponent と絡めると流用ができる。

 

次回は BaseComponent.swift を見てゆく。