次は本項の目的である、AIへの「避ける」動作の追加を行います。最初にGameplayKitを使わない場合の実装方法を考えてみましょう。
例として下のような配置のときの鬼の動きを検討します。
鬼を動かす流れとしては、「鬼からプレイヤーへの最短経路を計算」→「それに沿って鬼を動かす」という流れになります。「それに沿って鬼を動かす」部分についてはSKActionで実装できるので、「鬼からプレイヤーへの最短経路を計算」について詳しく見ていきます。
「鬼からプレイヤーへの最短経路を計算」の方法としては以下のような手順が考えられます。
まずは鬼の通るルート候補を作ります。そのために障害物に沿って以下のような点を配置します。ここで配置した点が鬼の通過する座標になります。
次は、先ほど作った点・プレイヤー・鬼を繋いで線を作ります。この線が鬼からプレイヤーへの最短経路候補になります。
この時障害物と重なる線は作らないようにします。
最後に作成した最短経路候補の中から最短経路を探します。一番単純な方法としては、全てのルートの距離を計算して最短距離を導く方法が考えられます。
普通に実装すると無限ループするため、「一度通った道は通らない」などのルールを追加する必要があります。
以上が障害物があるときの鬼の最短ルートの取得方法です。実装もやや難解な上、パフォーマンスチューニングの手間も掛かりそうです。
しかしGameplayKitの「Agents, Goals, and Behaviors」を使えばこれらの処理を簡単に実現できます。次項ではGameplayKitを使った場合の実装方法を解説します。
まずは「Agents, Goals, and Behaviors」で使うための障害物情報を作成します。GameScene.swiftのcreateObstaclesメソッドを以下のように置き換えてください。
class GameScene: SKScene { let player = SKShapeNode(circleOfRadius: 10) var enemies = [SKShapeNode]() var timer: Timer? var prevTime: TimeInterval = 0 var startTime: TimeInterval = 0 var isGameFinished = false let playerAgent = GKAgent2D() let agentSystem = GKComponentSystem(componentClass: GKAgent2D.self) var enemyAgents = [GKAgent2D]() var obstacles = [GKCircleObstacle]() // 今回追加 // 省略 // ここから今回置き換え func createObstacles() { guard let viewFrame = view?.frame else { return } while obstacles.count < 5 { let point = CGPoint( x: CGFloat(arc4random_uniform(UInt32(viewFrame.width))) - viewFrame.width / 2, y: CGFloat(arc4random_uniform(UInt32(viewFrame.height))) - viewFrame.height / 2) let radius = Float(arc4random_uniform(50) + 50) // 障害物かPlayerが衝突していたら設置しない let isObstacleOverlapped = obstacles.contains { let dx = (Float(point.x) - $0.position.x) let dy = (Float(point.y) - $0.position.y) if sqrt(dx*dx + dy*dy) < $0.radius + radius { return true } return false } let dx = point.x - player.position.x let dy = point.y - player.position.y let isPlayerOverlapped = sqrt(dx*dx + dy*dy) < CGFloat(radius) + player.frame.width if isObstacleOverlapped || isPlayerOverlapped { continue } let obstacleNode = SKShapeNode(circleOfRadius: CGFloat(radius)) obstacleNode.fillColor = UIColor(red: 0.7, green: 0.7, blue: 0.7, alpha: 1.0) obstacleNode.position = point obstacleNode.physicsBody = SKPhysicsBody(circleOfRadius: CGFloat(radius)) obstacleNode.physicsBody?.pinned = true addChild(obstacleNode) let obstacle = GKCircleObstacle(radius: radius) obstacle.position = float2(x: Float(point.x), y: Float(point.y)) obstacles.append(obstacle) } } // ここまで今回置き換え // 省略 }
障害物が配置できたので、AIに障害物を避けるルールを追加します。GameSceneのcreateEnemyメソッドを以下のように修正します。
class GameScene: SKScene { // 省略 func createEnemy() { let enemy = SKShapeNode(circleOfRadius: 10) enemy.position.x = size.width / 2 enemy.fillColor = UIColor(red: 0.94, green: 0.14, blue: 0.08, alpha: 1.0) enemy.physicsBody = SKPhysicsBody(circleOfRadius: enemy.frame.width / 2) addChild(enemy) enemies.append(enemy) let anemyAgent = GKAgent2D() anemyAgent.maxAcceleration = 200 // 今回修正、加速度を上げることで障害物を避けやすくする anemyAgent.maxSpeed = 70 anemyAgent.position = vector_float2(x: Float(enemy.position.x), y: Float(enemy.position.y)) anemyAgent.delegate = self anemyAgent.behavior = GKBehavior(goals: [ GKGoal(toSeekAgent: playerAgent), GKGoal(toAvoid: obstacles, maxPredictionTime: 10), // 今回追加 ], andWeights: [NSNumber(value: 1), NSNumber(value: 50)]) // 今回修正 agentSystem.addComponent(anemyAgent) enemyAgents.append(anemyAgent) } // 省略 }
これで鬼が障害物を回避して移動するようになりました。
障害物を避ける処理はcreateEnemyメソッドの下記の部分です。
anemyAgent.behavior = GKBehavior(goals: [ GKGoal(toSeekAgent: playerAgent), GKGoal(toAvoid: obstacles, maxPredictionTime: 10), ], andWeights: [NSNumber(value: 1), NSNumber(value: 50)])
anemyAgentが鬼の動きを管理するクラスで、それにGKBehaviorという振る舞いを表すクラスを渡しています。そうすることで、鬼(anemyAgent)はGKBehaviorに沿った動きをするようになります。
GKBehaviorがどのような振る舞いをするかは、「GKGoal」というクラスで設定します。今回はプレイヤーを追いかける「GKGoal(toSeekAgent: playerAgent)」というルールと、障害物を避ける「GKGoal(toAvoid: obstacles, maxPredictionTime: 10)」を設定したことで、障害物を避けつつプレイヤーを追いかけるようになりました。
Copyright © ITmedia, Inc. All Rights Reserved.