GamePlayKitのAgents, Goals & Behavioursを触ってみた

はじめに

Agents, Goals & BehavioursはGamePlayKitに搭載されている機能の1つでAIで、ターゲットとなるゴールの位置に向かってAgentsを移動させることのできる機能です。

以下の画像はAgents, Goals & Behavioursで実装されていたサンプルの動きをです。この完成形を目指して遊んでみました。

http://flexmonkey.blogspot.com/2015/11/a-look-at-agents-goals-behaviours-in.html

Agents, Goals & Behavioursを実装してみる

できるだけミニマムな形で実装をしてみました。

playerはとしてタップした位置に移動させ、agentDidUpdateでenemyの位置を変更させることで追いかける処理を作っています。

class GameScene: SKScene {

    let player: SKShapeNode = {
        let shape = SKShapeNode(circleOfRadius: 20)
        shape.strokeColor = .gray
        return shape
    }()
    let enemy: SKShapeNode = {
        let shape = SKShapeNode(circleOfRadius: 10)
        shape.position.y = 100
        shape.strokeColor = .white
        return shape
    }()
    
    let agentSystem = GKComponentSystem(componentClass: GKAgent2D.self)
    let playerAgent = GKAgent2D()
    var prevTime: TimeInterval = 0
    
    lazy var enemyAgent: GKAgent2D = {
        let agent = GKAgent2D()
        agent.maxAcceleration = 300
        agent.maxSpeed = 500
        agent.position = vector_float2(x: Float(enemy.position.x), y: Float(enemy.position.y))
        agent.delegate = self

        //敵の行動を作成、playerAgentをゴールに設定
        agent.behavior = GKBehavior(goals: [GKGoal(toSeekAgent: playerAgent)])
        
        return agent
    }()
    
    override func didMove(to view: SKView) {
        super.didMove(to: view)
        self.addChild(player)
        self.addChild(enemy)
        
        agentSystem.addComponent(enemyAgent)
    }
    
    override func update(_ currentTime: TimeInterval) {
        let delta = prevTime == 0 ? 0 : currentTime - prevTime
        prevTime = currentTime
        agentSystem.update(deltaTime: delta)
        
        playerAgent.position = vector_float2(x: Float(player.position.x), y: Float(player.position.y))
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        touches.forEach {
            let touchPos = $0.location(in: self)
            player.position = touchPos
        }
    }
}

extension GameScene: GKAgentDelegate {
    func agentDidUpdate(_ agent: GKAgent) {
        if let agent = agent as? GKAgent2D {
            enemy.position = CGPoint(x: CGFloat(agent.position.x), y: CGFloat(agent.position.y))
        }
    }
}

背景黒いのでちょっとみづらいですが追いかけてます

上記のコードの中でわかりにくいのはupdateで時間を計算してagentSystemのupdateに渡しているところだと思います。

以下のドキュメントに書いてあるように最後の更新された以降のデルタタイムでコンポーネントの更新を行うようです、 しっかりと理解できているわけではないですが、currentTimeをそのまま渡すとobjectが明後日の方向に吹き飛んでしまうので時間の計算は必須です。

/**
* Updates each component with the given delta time since the last update. Each component thus performs its time
* based logic with a single message.
*/

追いかける敵を増やしてみる

class GameScene: SKScene {

    let player: SKShapeNode = {
        let shape = SKShapeNode(circleOfRadius: 20)
        shape.fillColor = .lightGray
        shape.strokeColor = .gray
        return shape
    }()
    var enemies:[SKShapeNode] = []
    
    let agentSystem = GKComponentSystem(componentClass: GKAgent2D.self)
    let playerAgent = GKAgent2D()
    var prevTime: TimeInterval = 0
    
    var enemyAgents: [GKAgent2D] = []
    
    override func didMove(to view: SKView) {
        super.didMove(to: view)
        self.addChild(player)
        
        createEnemy(count: 800)
    }
    
    private func createEnemy(count: Int){
        for _ in 0..<count {
            let enemy = SKShapeNode(circleOfRadius: 10)
            enemy.position = CGPoint(x: CGFloat.random(in: -200...200), y: CGFloat.random(in: -200...200))
            enemy.strokeColor = .white
            self.addChild(enemy)
            
            enemies.append(enemy)
            
            let agent = GKAgent2D()
            agent.maxAcceleration = Float.random(in: 10...300)
            agent.maxSpeed = Float.random(in: 200...500)
            agent.position = vector_float2(x: Float(enemy.position.x), y: Float(enemy.position.y))
            agent.delegate = self
            agent.behavior = GKBehavior(goals: [GKGoal(toSeekAgent: playerAgent)])
            agentSystem.addComponent(agent)
            enemyAgents.append(agent)
        }
    }
    
    override func update(_ currentTime: TimeInterval) {
        let delta = prevTime == 0 ? 0 : currentTime - prevTime
        prevTime = currentTime
        agentSystem.update(deltaTime: delta)
        
        playerAgent.position = vector_float2(x: Float(player.position.x), y: Float(player.position.y))
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        touches.forEach {
            let touchPos = $0.location(in: self)
            player.position = touchPos
        }
    }
}

extension GameScene: GKAgentDelegate {
    func agentDidUpdate(_ agent: GKAgent) {
        if let agent = agent as? GKAgent2D,
            let index = enemyAgents.firstIndex(where: { $0 == agent }) {
            let enemy = enemies[index]
            enemy.position = CGPoint(x: CGFloat(agent.position.x), y: CGFloat(agent.position.y))
        }
    }
}

参考文献

A Look at Agents, Goals & Behaviours in GameplayKit

Agents, Goals, and Behaviors

GKComponentSystem

GKAgent2D を使ってみた

iOS GameplayKitの「Agents, Goals, and Behaviors」で作る、鬼ごっごの鬼AI (3/3)

GameplayKitAgents