A-FrameでVR迷路を移動する

A-Frameの勉強として、VR迷路を作ってみました。
ソースコードは[github](https://github.com/jyuko/aframe-maze-sample)で公開しています。

## 作るもの
A-Frameを使ったVR迷路

"スマートフォン(iOS,Android)＋Cardboard"だけで操作できるよう`look-controls`で視点を変えることで、camera(カメラ)の向いている方向に移動させます。


## 迷路の配置
碁盤目状の9×9マスに壁と通路を作るイメージで、`<a-plane>`の上に`<a-box>`を並べているだけです。
`class="collidable"`は、当たり判定のために付与しているクラスです。詳しくは、後で説明します。

```index.html
<a-entity id="maze" position="-4 1 -10">
    <a-box class="collidable" position="0 0 0" width="1" height="2" depth="1" color="#4CC3D9"></a-box>
    <a-box class="collidable" position="0 0 1" width="1" height="2" depth="1" color="#4CC3D9"></a-box>
    <a-box class="collidable" position="0 0 2" width="1" height="2" depth="1" color="#4CC3D9"></a-box>
    <a-box class="collidable" position="0 0 3" width="1" height="2" depth="1" color="#4CC3D9"></a-box>
    ...
    <a-box class="collidable" position="8 0 7" width="1" height="2" depth="1" color="#4CC3D9"></a-box>
    <a-box class="collidable" position="8 0 8" width="1" height="2" depth="1" color="#4CC3D9"></a-box>
</a-entity>
<a-plane  rotation="-90 0 0" width="100" height="100" color="#7BC8A4" static></a-plane>
```

上から見ると、こんな感じです。
![localhost-8383-aframe-sample-index.html(iPad) (2).png](https://qiita-image-store.s3.amazonaws.com/0/153460/ee9cdee0-cabe-d650-c2b7-bc624b0b97db.png)


## プレイヤーの移動
camera(カメラ)の向きからベクトルを計算し、移動速度(speed)を掛けることで移動量とします。
上下は考慮しないので、y軸の回転角から三角関数(あまり自信はない)で求めます。

```index.js
var speed = 0.01;
var isIntersect = false;

function movePlayer() {
    var camera = document.getElementById('camera');

    if (camera && !isIntersect) {
        var position = camera.getAttribute('position');
        var rotation = camera.getAttribute('rotation');

        position.x += -Math.cos((rotation.y - 90) * Math.PI / 180) * speed;
        position.z += Math.sin((rotation.y - 90) * Math.PI / 180) * speed;
        camera.setAttribute('position', position);
    }
}
```

A-FrameはHTMLで記述されているため、`getAttribute`、`setAttribute`で移動・回転ができます。
`isIntersect`は壁との当たり判定の結果を格納する変数です。こちらも後で説明します。

`movePlayer()`は、くり返し実行する必要があるので、renderメソッドを作成してループさせます。

```index.js
var t = 0;

function render() {
    t += 0.01;
    requestAnimationFrame(render);
    movePlayer();
}
render();
```


## プレイヤーと壁の当たり判定
camera(カメラ)に[raycaster](https://aframe.io/docs/0.3.0/components/raycaster.html)を追加して、視線(＝進行方向)と交差するオブジェクトの有無を判定します。

```index.html
<a-entity id="camera" camera look-controls raycaster="objects: .collidable; far: 0.5;" collider-check rotation="0 0 0" position="0 0.3 0"></a-entity>
```

まず、`raycaster`のプロパティについて説明します。

* `objects: .collidable;`  
当たり判定の対象を指定するプロパティです。  
`class="collidable"`が付与されたオブジェクトを対象にしており、迷路の壁にあたる`<a-box>`は当たり判定の対象、床にあたる`<a-plane>`は対象外となります。
* `far: 0.5;`  
当たり判定を行う距離です。  
デフォルト値が"Infinity"となっていて、変更しないと視線に壁があるだけで衝突と判定されてしまいます。

次に、当たり判定用のコンポーネントとして、`collider-check`を設定します。


```index.js
AFRAME.registerComponent('collider-check', {
    dependencies: ['raycaster'],
    init: function () {
        this.el.addEventListener('raycaster-intersection', function () {
            isIntersect = true;
        });
        this.el.addEventListener('raycaster-intersection-cleared', function () {
            isIntersect = false;
        });
    }
});
```

`raycaster`のプロパティで設定した距離内に交差オブジェクトがある場合、`raycaster-intersection`イベントが発生します。この間、`isIntersect`が`true`になるようにします。
同様に、交差オブジェクトがない場合、`raycaster-intersection-cleared`イベントが発生しますので、`isIntersect`は`false`にします。

先程、プレイヤーの移動を行う条件式に`!isIntersect`を入れていたので、"壁に近付くと止まる"、"前方に壁がなければ前進する"という挙動になります。

これで、コーディングは完了です。


## ビルド&デプロイ
[gulp](http://gulpjs.com/)を使ってビルドしました。
記事の内容から逸れるので、詳細は割愛します。

```sh
$ gulp default
```
動作確認のためにWebサーバを用意するのも面倒なので、Amazon S3に必要なファイルだけアップしました。

* dist/app.min.js
* index.html


## 迷路に入ってみる
スマートフォン実機(iPhone SE)で、index.htmlにアクセスします。
![IMG_1326 (1).png](https://qiita-image-store.s3.amazonaws.com/0/153460/5c87960b-a236-1f82-49d6-eea9dc3ef439.png)

向きを変えることで、ちゃんと迷路を抜けられました。
ただ、アドレスバーが邪魔ですね。

気を取り直して、VRモードに切り替えます。
視差効果で左右の表示が異なっているのがわかります。
![IMG_1327.png](https://qiita-image-store.s3.amazonaws.com/0/153460/9fc4da83-e25e-e24a-0d88-3b1b902ad968.png)

最後に、以前に自作した[Cardboard初号機](https://vr.google.com/cardboard/get-cardboard/)にセットして見てみます。
![IMG_1328.JPG](https://qiita-image-store.s3.amazonaws.com/0/153460/af9f38e7-5726-f57f-72a4-b262adddf147.jpeg)

**立体的に見えた！**
...けど、没入感はあまりないかな。


## まとめ
A-Frameを使ってVR体験できる迷路を作りました。

ほとんどJavaScriptを書かずに実装できたので、three.jsと比べて、スッキリと見やすく記述できそうです。
移動や当たり判定は、ゲームやナビゲーション等にも応用できそうですし、何かの役に立てば幸いです。

余力があれば、スタート・ゴール時の処理を追加してタイム計測したり、ステージを切り替えられるようにすると、ミニゲームっぽくなるかなと思います。