【Swift Playgrounds】ARを作るPART.4「蝶を捕まえる」を読み解く

# この投稿は何か？
[前回](https://qiita.com/chino_tweet/items/221c268de2137b90eef0)からの続きです。


# コードを読む
このページにも`SharedCode`ファイルがありますが、以前のページと共通なので解説を省略します。
ここでは、メインファイルのみを扱います。
コード全体は、以下の通りです。

- 蝶とテキストの3Dモデルを作成して、シーンに追加する
- 蝶が飛んでいるように動かす
- カメラが蝶に近づいたとき、別の3Dモデルを表示する。

````Swift:メインファイル
// バナーと蝶を作成します。
let banner = Model.text("すばしっこい蝶を捕まえてください!", elevation: 10.cm)
let butterfly = Model.flyingBug

// シーンにモデルを追加して、蝶を縮小します。
scene.add([banner, butterfly])
butterfly.run(action: .scaleTo(0.1, duration: 2))

// 蝶を移動するシーケンスを作成します。継続時間を変更して、移動を速くしたり遅くしたりします。
let duration: Float = 1
let sequence: [BuiltinAction] = [
    .moveBy(x: 35.cm, y: 20.cm, z: -5.cm, duration: duration),
    .moveBy(x: -20.cm, y: 5.cm, z: -20.cm, duration: duration),
    .moveBy(x: -25.cm, y: -10.cm, z: 10.cm, duration: duration),
    .moveBy(x: 15.cm, y: 20.cm, z: 30.cm, duration: duration),
    .moveBy(x: -5.cm, y: -35.cm, z: -15.cm, duration: duration)
]

// シーンが開始するときに、蝶を舞わせます。
scene.setOnStartHandler {
    butterfly.animate(.flap, repeats: .loop)
    butterfly.repeat(.pingpong, sequence: sequence, completion: nil)
    butterfly.play(.melodyLoop, loops: true, completion: nil)
}

// カメラが蝶から2cm以内に近づいたとき、サプライズを起こします!
scene.camera.when(butterfly, isWithin: 2.cm) {
    let ear = Model.ear
    let eye = Model.eye
    let foot = Model.foot
    
    scene.place(ear, at: butterfly.location)
    scene.place(eye, at: butterfly.location)
    scene.place(foot, at: butterfly.location)
    
    ear.run(action: .moveBy(x: Float.random(in: -20.cm...20.cm), y: Float.random(in: -20.cm...20.cm), z: Float.random(in: -20.cm...20.cm), duration: 1))
    eye.run(action: .moveBy(x: Float.random(in: -20.cm...20.cm), y: Float.random(in: -20.cm...20.cm), z: Float.random(in: -20.cm...20.cm), duration: 1))
    foot.run(action: .moveBy(x: Float.random(in: -20.cm...20.cm), y: Float.random(in: -20.cm...20.cm), z: Float.random(in: -20.cm...20.cm), duration: 1))
}
````

コードを分解して、見ていきます。

### モデル作成とシーンへの追加
テキストの3Dモデルを定数`banner`に、蝶の3Dモデルを定数`butterfly`に割り当てています。
`add()`メソッドを使って、シーンに追加します。

````swift:バナーと蝶を作成して、シーンに追加する
let banner = Model.text("すばしっこい蝶を捕まえてください!", elevation: 10.cm)
let butterfly = Model.flyingBug

scene.add([banner, butterfly])
butterfly.run(action: .scaleTo(0.1, duration: 2)).   // 蝶を縮小する
````

シーンに追加した後に、蝶のアクションを指定しています。
アクションを指定した後に、シーンに追加した方が個人的には自然な気がします。

### コード2
蝶がゆらゆらと浮遊しているように見せています。
継続時間である`duration`を変更することで、移動を速くしたり遅くしたり調節できます。

````swift:蝶のシーケンスを作成
let duration: Float = 1
let sequence: [BuiltinAction] = [
    .moveBy(x:  35.cm, y:  20.cm, z:  -5.cm, duration: duration),
    .moveBy(x: -20.cm, y:   5.cm, z: -20.cm, duration: duration),
    .moveBy(x: -25.cm, y: -10.cm, z:  10.cm, duration: duration),
    .moveBy(x:  15.cm, y:  20.cm, z:  30.cm, duration: duration),
    .moveBy(x:  -5.cm, y: -35.cm, z: -15.cm, duration: duration)
]
````

ここでは、`moveBy(x:y:z:duration:)`メソッドが`BuiltinAction`の型メソッドであることがわかります。
この定数`sequence`は、蝶を動かすことが目的ではあるものの、実装上、__どの3Dモデルを動かすか__は現時点では未定です。

### シーケンスを蝶に渡す
「シーンを開始する」ボタンをタップしたら、蝶がAR空間を浮遊するようにします。
シーン開始後のハンドラは、`setOnStartHandler`メソッドに渡します。
蝶は移動するだけでなく、音を鳴らしたり、パタパタと羽を動かし続けるようにアニメーションさせています。

````swift:シーンが開始すると、蝶が舞う
scene.setOnStartHandler {
    butterfly.animate(.flap, repeats: .loop)
    butterfly.repeat(.pingpong, sequence: sequence, completion: nil)
    butterfly.play(.melodyLoop, loops: true, completion: nil)
}
````

ここで、作成済みのシークエンスを蝶に渡しています。

### カメラの距離イベント
カメラと蝶の距離が2cm以内になると、イベントが発生するようにしています。
耳と目と足の3Dモデルが出現して、蝶の周辺をランダムに動き回るようにしています。

````swift:カメラと蝶が2cm以内に近づいたとき
scene.camera.when(butterfly, isWithin: 2.cm) {
    let ear = Model.ear
    let eye = Model.eye
    let foot = Model.foot
    
    scene.place(ear, at: butterfly.location)
    scene.place(eye, at: butterfly.location)
    scene.place(foot, at: butterfly.location)
    
    ear.run(action: .moveBy(x: Float.random(in: -20.cm...20.cm), 
                            y: Float.random(in: -20.cm...20.cm),
                            z: Float.random(in: -20.cm...20.cm), 
                            duration: 1))
    eye.run(action: .moveBy(x: Float.random(in: -20.cm...20.cm), 
                            y: Float.random(in: -20.cm...20.cm), 
                            z: Float.random(in: -20.cm...20.cm),
                            duration: 1))
    foot.run(action: .moveBy(x: Float.random(in: -20.cm...20.cm), 
                             y: Float.random(in: -20.cm...20.cm), 
                             z: Float.random(in: -20.cm...20.cm), 
                             duration: 1))
}
````

ここでは、`Float.random(in: -20.cm...20.cm)`という記述に注目します。
`Float.random(in:)`メソッドは、パラメータに指定された範囲内で乱数を返します。
また、`-20.cm...20.cm`は、範囲の表現です。
したがって、最小`-20`cmから最大`20`cm以内にある小数点数を返すことになります。
`Int`でなく、`Float`の`random(in:)`メソッドを使っているところに、何かこだわりがあるのでしょうか...。

# コードを読んでわかったこと
このページで構築しているARコンテンツは、インタラクティブで何度も体験したくなる要素が詰め込まれていると感じました。
ランダムな要素を取り入れたり、タップしたり、実際に動いたりするところが楽しいと感じさせてくれる理由ではないでしょうか。

- 連続的にアクションさせるには、シークエンスを作ってから、モデルの`repeat(_:sequence:completion:)`メソッドに渡す。
- ちなみに、同時にいくつかのアクションを行うには、`run(group:)`メソッドにアクションコレクションを渡す。
- AR空間上に存在する3Dモデルの位置情報は、`location`プロパティから取得できる。