ここから本文です

3DゲームのAIをiOSのSceneKitとGameplayKitで作る基本

7/11(火) 8:10配信

@IT

●最終回は、3Dゲームの人工知能

 本記事では、ゲーム用フレームワークであるGameplayKitを使ったアプリ制作を通して人工知能(AI)について学んでいきます。その中で今回は、簡単な3Dアプリ制作を通して、3Dゲーム用の人工知能(AI)について学びます。

Xcodeのテンプレート選択画面では「Game」を選択

●SceneKitで3Dゲームプロジェクトの作成

 今回はオブジェクトが目的地に向かって移動するアプリを作っていきます。

 まずは3Dゲームのプロジェクトを作成します。Xcodeで新規プロジェクト作成を選択してください。

 テンプレート選択画面では「Game」を選択します。

 最後に、アプリの詳細を入力しますが、ここでは「SceneKit」を選択します。

 これでプロジェクトを作成できました。起動すると、飛行機が回っている画面が表示されるかと思います。

●SceneKitとは、その主な機能

 SceneKitとは、3Dゲーム用のフレームワークで、3Dゲームを作るためのさまざまな機能を提供します。

 サンプルのソースコードを基に、SceneKitの機能について簡単に見ていきます。

1つの画面を構成するSCNScene

 SceneKitでは「SCNScene」が1つ1つの画面に相当します。こちらはSpriteKitにおけるSKSceneに相当します。この上に飛行機などのオブジェクト(SCNNode)を配置することでゲームを作っていきます。

 SCNSceneはUIViewControllerの「SCNView」にセットすることで画面に表示されます。サンプルアプリでは、下記のように「ship.scn」からSCNSceneを読み込んでSCNViewにセットしています。

――――――
class GameViewController: UIViewController {
  override func viewDidLoad() {
    let scene = SCNScene(named: "art.scnassets/ship.scn")!

    // 省略

    let scnView = self.view as! SCNView
    scnView.scene = scene

    // 省略
  }

  // 省略
}
――――――

○画面タップの検知

 画面タップの検知はSpriteKitと少し違うので、注意が必要です。SpriteKitのSKSceneはUIResponderのサブクラスのため、以下のようにタップを検知できました。

――――――
// SpriteKitでは以下のようにタップを検知
class MyScene: SKScene {
  override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
  }

  override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
  }
}
――――――

 SCNSceneはUIResponderのサブクラスではないため、SCNViewにUITapGestureRecognizerを追加する必要があります。

 サンプルでは、以下のように実装しています。

――――――
class GameViewController: UIViewController {
  override func viewDidLoad() {
    // 省略

    let scnView = self.view as! SCNView

    // 省略

    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
    scnView.addGestureRecognizer(tapGesture)
  }

  func handleTap(_ gestureRecognize: UIGestureRecognizer) {
    // 省略
  }

  // 省略
}
――――――

○毎フレーム呼ばれるメソッド

 毎フレーム呼ばれるメソッドもSpriteKitと違いがあります。SpriteKitではSKSceneのupdateメソッドが毎フレーム呼ばれていました。しかしSceneKitのSCNSceneにはupdateメソッドがないため、他の方法を採る必要があります。

――――――
class MyScene: SKScene {
  override func update(_ currentTime: TimeInterval) {
  }
}
――――――

 方法としては、「SCNSceneRendererDelegate」を使うものがあります。以下のようにSCNSceneRendererDelegateの「renderer」を使うと、毎フレーム呼ばれるメソッドを実装できます。

――――――
class GameViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    let scnView = view as? SCNView
    scnView?.delegate = self
  }
}

extension GameViewController: SCNSceneRendererDelegate {
  func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    // 1フレームごとに呼ばれるメソッド

  }

}
――――――

○オブジェクトのアニメーション「SCNAction」

 オブジェクトのアニメーションには「SCNAction」を使います。

 サンプルでは、以下のように飛行機に回転アニメーションを追加しています。こちらはSpriteKitの「SKAction」とほぼ同じですが、扱う位置情報が2次元から3次元になっています。
――――――
let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))
――――――

 続いて、サンプルの飛行機を「Agents, Goals, and Behaviors」を使って移動させるアプリを作ります。

○GameViewControllerのコードを最小限に

 まずはGameViewControllerのコードを最小限にします。GameViewController.swiftを下記のように修正してください。

――――――
import UIKit
import QuartzCore
import SceneKit

class GameViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    let scnView = view as? SCNView
    scnView?.delegate = self
    scnView?.isPlaying = true
    let scene = SCNScene(named: "art.scnassets/ship.scn")
    scnView?.scene = scene

    // 光源の設置
    let lightNode = SCNNode()
    lightNode.light = SCNLight()
    lightNode.light?.type = .omni
    lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
    scene?.rootNode.addChildNode(lightNode)
    let ambientLightNode = SCNNode()
    ambientLightNode.light = SCNLight()
    ambientLightNode.light?.type = .ambient
    ambientLightNode.light?.color = UIColor.darkGray
    scene?.rootNode.addChildNode(ambientLightNode)

    // 飛行機の角度調整
    let ship = scene?.rootNode.childNode(withName: "ship", recursively: true)
    ship?.eulerAngles = SCNVector3(x: 0, y: Float.pi, z: 0)
  }

  override var prefersStatusBarHidden: Bool {
    return true
  }
}

extension GameViewController: SCNSceneRendererDelegate {
  // 1フレームごとに呼ばれるメソッド
  func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    print(time)
  }
}
――――――

 これで飛行機が配置されているだけの状態になりました。

○ルールを作成

 次は、実際に「Agents, Goals, and Behaviors」を使って飛行機を移動させます。

 まずはプロジェクト設定の下にあるボタンからGameplayKitを追加します。

 続けてGameViewControllerを下記のように修正します。

――――――
import UIKit
import QuartzCore
import SceneKit
import GameplayKit // 今回追加

class GameViewController: UIViewController {
  // 今回追加ここから
  let agentSystem = GKComponentSystem(componentClass: GKAgent3D.self)
  var prevTime: TimeInterval = 0
  let shipAgent = GKAgent3D()
  // 今回追加ここまで

  override func viewDidLoad() {
    // 省略

    // 飛行機の角度調整
    let ship = scene?.rootNode.childNode(withName: "ship", recursively: true)
    ship?.eulerAngles = SCNVector3(x: 0, y: Float.pi, z: 0)

    // 今回追加ここから
    let targetAgent = GKAgent3D()
    targetAgent.position = float3(0, 0, -300)
    shipAgent.maxAcceleration = 1
    shipAgent.maxSpeed = 1
    shipAgent.delegate = self
    shipAgent.behavior = GKBehavior(goals: [
      GKGoal(toSeekAgent: targetAgent),
      ])
    agentSystem.addComponent(shipAgent)
    // 今回追加ここまで
  }

  override var prefersStatusBarHidden: Bool {
    return true
  }
}
// 省略
――――――

 上記コードでは、飛行機のagent(shipAgent)が座標(x: 0, y: 0, z: -300)に向かうようなルールを作成しました。

○ルールに沿って移動する処理を追加

 次は、飛行機オブジェクトがそのルールに沿って移動する処理を追加します。

――――――
// 省略

extension GameViewController: SCNSceneRendererDelegate {
  // 1フレームごとに呼ばれるメソッド
  func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    // 今回追加ここから
    if prevTime == 0 {
      prevTime = time
    }
    agentSystem.update(deltaTime: time - prevTime)
    // 今回追加ここまで
  }
}

// 今回追加ここから
extension GameViewController: GKAgentDelegate {
  func agentDidUpdate(_ agent: GKAgent) {
    if let ship = (view as? SCNView)?.scene?.rootNode.childNode(withName: "ship", recursively: true), let agent = agent as? GKAgent3D {
      ship.position = SCNVector3(agent.position)
    }
  }
}
// 今回追加ここまで
――――――

 これで飛行機が奥に向かって移動するようになりました。

○2Dと3Dでの「Agents, Goals, and Behaviors」の利用方法の違い

 前項では、3Dゲームへの「Agents, Goals, and Behaviors」適用を行いました。実は2Dでも3Dでも実装の流れとしては、ほとんど同じです。

 どちらも大きな流れとしては「エージェント(GKAgent)インスタンスを作成して、そこにルール(GKRule)を追加する」というものになります。

 変更点としては、扱う位置情報が2次元から3次元になるだけなので、2Dゲームと大きな違いはなく実装できると思います。

●「Agents, Goals, and Behaviors」を利用した、障害物を回避するルールの追加

 最後に障害物を設置したときの動きも見ていきます。

 鬼ごっこアプリ同様に、GKObstacleを使って障害物を設置します。GameViewControllerのviewDidLoadを下記のように修正してください。

――――――
class GameViewController: UIViewController {
  // 省略

  override func viewDidLoad() {
    // 省略

    let targetAgent = GKAgent3D()
    targetAgent.position = float3(0, 0, -300)
    shipAgent.maxAcceleration = 1
    shipAgent.maxSpeed = 1
    shipAgent.delegate = self
    // 今回修正ここから
    let obstacle = GKSphereObstacle(radius: 100)
    obstacle.position = vector_float3(0, 0, -150)
    shipAgent.behavior = GKBehavior(goals: [
      GKGoal(toSeekAgent: targetAgent),
      GKGoal(toAvoid: [obstacle], maxPredictionTime: 100.0)
      ], andWeights: [NSNumber(value: 1), NSNumber(value: 50)])
    // 今回修正ここまで
    agentSystem.addComponent(shipAgent)
  }

  // 省略
}
――――――

 今回追加したGKSphereObstacleは3Dゲーム用の障害物オブジェクトです。この障害物を避けるルール【GKGoal(toAvoid: [obstacle], maxPredictionTime: 100.0)】を追加することで、飛行機が斜めに移動するようになりました。

 以上で、3Dゲームにおける「Agents, Goals, and Behaviors」の基本的な使い方は終了です。

●GameplayKitでゲームAI開発の工数を削減しよう

 今回のソースはkonkai.zipからダウンロードできます。

 これまで4回にわたって「Agents, Goals, and Behaviors」の使い方を見てきましたがいかがでしょうか? GameplayKitは分かりにくいところもありますが、うまく使えば開発工数を大いに削減できます。

 もし機会がありましたら、本連載を参考に試していただけると幸いです。

□□
●筆者紹介

杉本裕樹
田町のベンチャーで働くエンジニア。

仕事ではiPhoneアプリの開発やRailsを使ったWebサービス開発を行っている。最近のマイブームはUnityを使った3Dゲーム開発。
□□

最終更新:7/11(火) 8:10
@IT