Apple Engine

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

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

今回は、ChaserComponent.swift を見てゆく。 この GKComponent はプレイヤーキャラ Max を追いかける設定が行われている。

f:id:x67x6fx74x6f:20180417170221p:plain

 

ChaserComponent の中身

import

中で移動座標の処理をしているため simd をインポートしている。

import GameplayKit
import simd

 

追いかける敵キャラの状態

追いかける敵キャラの状態を以下の enum で設定している。

  • 周りをぶらつく状態
  • Max を追いかけている状態
  • 倒されてしまった状態
enum ChaserState: Int {
    case wander
    case chase
    case dead
}

 

ChaserComponent クラスの中身

BaseComponent を継承した ChaserComponent クラス。 以下の中身を見てゆく。

class ChaserComponent: BaseComponent {
    ...
}

 

GKInspectable

scene.scn の enemy1 の Components でも設定変更可能な変数。
scene.scn で設定している値が優先されるが、デフォルト値は同じようになっている。

@GKInspectable var hitDistance: Float = 0.5
@GKInspectable var chaseDistance: Float = 3.0
@GKInspectable var chaseSpeed: Float = 9.0
@GKInspectable var wanderSpeed: Float = 1.0
@GKInspectable var mass: Float = 0.3
@GKInspectable var maxAcceleration: Float = 8.0

 

player 変数

PlayerComponent がセットされると、ChaserComponent の GKAgent の各設定がされる。
以下設定。

  • この agent の mass と maxAcceleration に、ここで設定している変数 mass と maxAcceleration を渡す。文字通り質量と最大加速度。
  • chaseGoal に Max が設定されている PlayerComponent の agent を使い追いかけるゴールを設定
  • wanderGoal に周りを探索するゴールを設定
  • center の配列にポイントを設定し、GKPath でそれを使用してパスの作成。そのパスを元に toStayOn でその範囲内で移動するゴールの作成。
  • 設定したゴールの GKBehavior を作成し、この agent に設定する
  • startWandering() を動かし、周りを探索するような移動をする

 

var player: PlayerComponent? {
    didSet {
        self.agent.mass = self.mass
        self.agent.maxAcceleration = self.maxAcceleration

        chaseGoal = GKGoal(toSeekAgent: (player?.agent)!)
        wanderGoal = GKGoal(toWander: self.wanderSpeed)

        var center: [float2] = []
        center.append(float2(x: -1, y: 9))
        center.append(float2(x: 1, y: 9))
        center.append(float2(x: 1, y: 11))
        center.append(float2(x: -1, y: 11))

        let p = GKPath(points: center, radius: 0.5, cyclical: true)
        centerGoal = GKGoal(toStayOn: p, maxPredictionTime: 1)
        behavior = GKBehavior(goals: [chaseGoal!, wanderGoal!, centerGoal!])
        agent.behavior = behavior
        startWandering()
    }
}

 

メンバ変数

以下。文字通りの変数。

  • state は ChaserState を取り、デフォルト値は 0 で周りを探索する。
  • speed はこのコンポーネントに紐づけられるキャラのスピード
  • ステートに合わせた chaseGoal、wanderGoal のゴールと、移動範囲を設定する centerGoal
  • behavior は関数をまたいで使えるようにした GKBehavior
private var state = ChaserState(rawValue: 0)!
private var speed: Float = 9.0

private var chaseGoal: GKGoal?
private var wanderGoal: GKGoal?
private var centerGoal: GKGoal?

private var behavior: GKBehavior?

 

isDead()

BaseComponent の isDead() をオーバーライドして state が dead なら true 返す変数に書き換えている。

override func isDead() -> Bool {
    return state == .dead
}

 

startWandering()

Max 近くにいない場合に、追いかけず周りを移動する処理。

agent の最大移動スピードを周りを移動する wanderSpeed に変更。
behavior のウエイトの変更し、chaseGoal を 0、wanderGoal 1 にして優先度をあげ、state を "wander" に変更する。

func startWandering() {
    guard let behavior = behavior else { return }

    self.agent.maxSpeed = self.wanderSpeed
    behavior.setWeight(1, for: self.wanderGoal!)
    behavior.setWeight(0, for: self.chaseGoal!)
    behavior.setWeight(0.6, for: self.centerGoal!)
    state = .wander
}

 

startChasing()

Max を追いかける処理。

agent の最大移動スピードを Max を追いかける speed に変更。
behavior のウエイトの変更し、wanderGoal を 0、chaseGoal 1 にして優先度をあげ、state を "chase" に変更する。

func startChasing() {
    guard let behavior = behavior else { return }

    self.agent.maxSpeed = self.speed
    behavior.setWeight(0, for: self.wanderGoal!)
    behavior.setWeight(1, for: self.chaseGoal!)
    behavior.setWeight(0.1, for: centerGoal!)
    state = .chase
}

 

override func update(deltaTime seconds: TimeInterval)

BaseComponent の update 関数をオーバーライドして処理を変えている。

  • state が dead であれば処理を update を抜ける
  • character に playerComponent から character を取得
  • player の entity から Max の SCNNode を取得
  • 自身のコンポーネントに紐づいている enemy1 の SCNNode を取得
  • simd_distance で Max と enemy1 のノードから比較して位置を distance に渡す
  • switch 文で state が wander の場合で distance より chaseDistance 小さい場合は startChasing()。state が chase でdistance より chaseDistance 大きい場合は startWandering()。dead の場合はスルーする処理を実行
  • speed に chaseSpeed と distance のどちらか小さい値を渡す。
  • このクラスで設定されている handleEnemyResponse を character と enemy1 のノードを渡し関数を呼ぶ
  • スーパークラスの update を呼ぶ
override func update(deltaTime seconds: TimeInterval) {
    if state == .dead {
        return
    }

    guard let character = player?.character else { return }
    guard let playerComponent = (player?.entity?.component(ofType: GKSCNNodeComponent.self)) else { return }
    guard let nodeComponent = entity?.component(ofType: GKSCNNodeComponent.self) else { return }

    let enemyNode = nodeComponent.node
    let playerNode = playerComponent.node
    let distance = simd_distance(enemyNode.simdWorldPosition, playerNode.simdWorldPosition)

    switch state {
        case .wander:
            if distance < chaseDistance {
                startChasing()
            }
        case .chase:
            if distance > chaseDistance {
                startWandering()
            }
        case .dead:
            break
    }

    speed = min(chaseSpeed, distance)

    handleEnemyResponse(character, enemy: enemyNode)

    super.update(deltaTime: seconds)
}

 

private func handleEnemyResponse(_ character: Character, enemy: SCNNode)

敵キャラクターの処理。

敵キャラと Max 場所から方向を導き出し、hitDistance より近い場所に Max がいて、Max が isAttacking の状態を true にしている場合は Max の攻撃がヒットしていることとなる。

state を dead に設定し character(Character クラス)の didHitEnemy() を実行し Max のアニメーションを行う。
BaseComponent の performEnemyDieWithExplosion 敵キャラのアニメーション処理を行う。

isAttacking が false の場合、Max が接触したこととなり、character(Character クラス)で wasTouchedByEnemy() が実行され、敵に接触した際のアニメーションが実行される。

private func handleEnemyResponse(_ character: Character, enemy: SCNNode) {
    let direction = enemy.simdWorldPosition - character.node.simdWorldPosition

    if simd_length(direction) < hitDistance {
        if character.isAttacking {
            
            state = .dead
            
            character.didHitEnemy()

            performEnemyDieWithExplosion(enemy, direction: direction)
        } else {
            character.wasTouchedByEnemy()
        }
    }
}

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