WWDC 2017 の SceneKit サンプル Fox 2 を調べる その20
今回は、ChaserComponent.swift を見てゆく。 この GKComponent はプレイヤーキャラ Max を追いかける設定が行われている。
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 を見てゆく。