GKObstacleで障害物を作る

はじめに

Pathfindingの記事ではObstacleを使って目的のゴールまでの障害を作成しましたが、Agents, Goals & BehavioursでもObstacleを作成しゴールにたどり着くまでの道のりに障害物を配置することが可能です。

GamePlayKitの障害物について

GamePlayKitにはGKObstacleというクラスが存在します。

私たち開発者が直接GKObstacleクラスを使うのではなく、GKObstacleを継承したGKCircleObstacle,GKShapeObstacle,GKPolygonObstacleのどれのインスタンスを作成し使用することになります。

今回はSpriteKitを使用して2次元的な障害物を作りたいので、GKPolygonObstacleを使ってみました。

GKPolygonObstacle

GKPolygonObstacleのインスタンスを作るのは簡単で以下のように障害物にしたいSKNodeの配列を渡してあげるだけでOKです。

let obstacle: SKShapeNode = {
    let shape = SKShapeNode(circleOfRadius: 40)
    shape.position.y = 100
    shape.fillColor = .white
    return shape
}()
let polygonObstacle = SKNode.obstacles(fromNodeBounds: [obstacle])

障害物を計算に適用させる

作成した障害物ですが、Agents, Goals & Behavioursの場合GKAgentの動きを計算する要素に関してはGKBehaviorにGKGoalとして渡してあげれば良いので、以下のように目的地を障害物を一緒に渡してあげれば自動的に計算してくれます。

GKBehavior(goals: [GKGoal(toSeekAgent: playerAgent),
                   GKGoal(toAvoid: polygonObstacle, maxPredictionTime: 2)])

maxPredictionTimeは、GKAgentが何秒前に障害物との衝突予測を行うかという時間です。上記のコードでは2秒を設定しているので、障害物に衝突する2秒前から回避の計算が移動座標に組み込まれる形になっています。

コード

class GameScene: SKScene {

    let player: SKShapeNode = {
        let shape = SKShapeNode(circleOfRadius: 20)
        shape.fillColor = .purple
        return shape
    }()
    let enemy: SKShapeNode = {
        let shape = SKShapeNode(circleOfRadius: 10)
        shape.position.y = 300
        shape.fillColor = .white
        return shape
    }()
    let obstacle: SKShapeNode = {
        let shape = SKShapeNode(circleOfRadius: 40)
        shape.position.y = 100
        shape.fillColor = .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 = 1000
        agent.maxSpeed = 100
        agent.position = vector_float2(x: Float(enemy.position.x), y: Float(enemy.position.y))
        agent.delegate = self
        
        let polygonObstacle = SKNode.obstacles(fromNodeBounds: [obstacle])
        agent.behavior = GKBehavior(goals: [GKGoal(toSeekAgent: playerAgent),
                                            GKGoal(toAvoid: polygonObstacle, maxPredictionTime: 2)])
        return agent
    }()
    
    override func didMove(to view: SKView) {
        super.didMove(to: view)
        self.addChild(player)
        self.addChild(enemy)
        self.addChild(obstacle)
        
        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))
        }
    }
}

オブジェクトを増やしてみた

タップした位置についてくるオブジェクトがかわいい!

速度や加速度をいじると動き方が変わるのでいい感じに遊べます

class GameScene: SKScene {

    let player: SKShapeNode = {
        let shape = SKShapeNode(circleOfRadius: 20)
        shape.fillColor = .purple
        return shape
    }()
    var enemys: [SKShapeNode] = []
    var enemyAgents:[GKAgent] = []
    let obstacle: [SKShapeNode] = {
        var shapes: [SKShapeNode] = []
        let shapePos = [CGPoint(x: 100, y: 100),
                        CGPoint(x: 100, y: -100),
                        CGPoint(x: -100, y: 100),
                        CGPoint(x: -100, y: -100),
                        CGPoint(x: 100, y: 300),
                        CGPoint(x: -100, y: 300),
                        CGPoint(x: 100, y: -300),
                        CGPoint(x: -100, y: -300),
        ]
        for i in 0..<shapePos.count {
            let shape = SKShapeNode(circleOfRadius: 20)
            shape.fillColor = .white
            shape.position = shapePos[i]
            shapes.append(shape)
        }
        return shapes
    }()
    
    let agentSystem = GKComponentSystem(componentClass: GKAgent2D.self)
    let playerAgent = GKAgent2D()
    var prevTime: TimeInterval = 0
    
    override func didMove(to view: SKView) {
        super.didMove(to: view)
        createEnemy(count: 300)
        
        self.addChild(player)
        enemys.forEach{self.addChild($0)}
        obstacle.forEach{self.addChild($0)}
        
    }
    
    private func createEnemy(count: Int){
        for _ in 0..<count {
            let enemyAgent: GKAgent2D = {
                let enemy: SKShapeNode = {
                    let shape = SKShapeNode(circleOfRadius: 3)
                    shape.position = CGPoint(x: CGFloat.random(in: -200...200), y: CGFloat.random(in: -200...200))
                    shape.strokeColor = .lightGray
                    shape.lineWidth = 3
                    return shape
                }()
                
                let agent = GKAgent2D()
                agent.maxAcceleration = 1000
                agent.maxSpeed = Float.random(in: 100...200)
                agent.position = vector_float2(x: Float(enemy.position.x), y: Float(enemy.position.y))
                agent.delegate = self
                
                let polygonObstacle = SKNode.obstacles(fromNodeBounds: obstacle)
                agent.behavior = GKBehavior(goals: [GKGoal(toSeekAgent: playerAgent),
                                                    GKGoal(toAvoid: polygonObstacle, maxPredictionTime: 2)])
                enemys.append(enemy)
                return agent
            }()
            enemyAgents.append(enemyAgent)
            
            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,
            let index = enemyAgents.firstIndex(where: { $0 == agent }) {
            let enemy = enemys[index]
            enemy.position = CGPoint(x: CGFloat(agent.position.x), y: CGFloat(agent.position.y))
        }
    }
}
Swift

前の記事

SceneKitミニマムサンプル
Swift

次の記事

XCDYouTubeKitを使ってみる