PlayCanvasでSPAっぽいポータルサイトをつくってみた

この記事は [PlayCanvas Advent Calendar 2017](https://qiita.com/advent-calendar/2017/playcanvas) の3日目の記事です。
# 作ったもの
自分のポータルサイトをPlayCanvasだけで作ってみました。
[utautattaro.com](https://utautattaro.com)
モバイル、デスクトップ　両方動きます。
現状の機能としては、各種SNSに飛ぶリンクが仕込んであったり、ポートフォリオをまとめてあったり、ちょっとした自己紹介が載せてある程度で、今後さらに追加修正予定です。
![1lzFoU0.gif](https://i.imgur.com/1lzFoU0.gif)

# なぜ作ったか
もともと自分のポータルサイトはPlayCanvasで1年前くらいに作っていたものをずっと使っていたのですが、飽きてしまったので作り直しました。
[昔作った時のブログ](https://blog.utautattaro.com/2016/09/23/portal/)
また、ebayのPlayCanvas製のサイトを見たときに、canvas一枚でWebページとして構成させても十分行けるなあ　と感じたのも大きなきっかけです。
[ネットショッピングの大手 eBay のドイツにおける市場調査レポートが PlayCanvas 製ですごい！](https://webgl.souhonzan.org/entry/?v=1106)

# 作り方
今回はすべてPlayCanvas上だけで完結するような作りにしています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/148084/e229f5a0-f9c5-c14e-1da2-96c903ee5f1c.png)
エンティティはシンプルに大きく分けて3つの役割を持つエンティティに分類しました
1. リンクエンティティ(LE)　    ・・・　選択すると別ページに遷移する
2. フォルダエンティティ(FE)    ・・・　選択すると指定したフォルダを開く
3. アクションエンティティ(AE)  ・・・　選択すると特定のアクションをする

ポータルサイトに存在するエンティティは以下のようになっています

- [AE]about me
- [[LE]blog](https://blog.utautattaro.com)
- [FE]portfolios
    - [[LE]slides](https://slides.utautattaro.com)
    - [[LE]play now](https://playcanvas.utautattaro.com)
    - [[LE]voxelcanvas](https://voxelcanvas.me)
- [FE]social
    - [[LE]github](https://github.com/utautattaro)
    - [[LE]twitter](https://twitter.com/utautattaro)
    - [[LE]facebook](https://fb.me/utautattaro)
    - [[LE]playcanvas](https://playcanvas.com/ryotaro)
  
## エンティティ選択の処理
PlayCanvasでエンティティをクリック/タップした時を取得する処理はRayを使う方法やFlame bufferを使う方法などいろいろありますが、自分はFlame bufferを使って取得してます。この辺は好みで。
[Entity Picking](https://developer.playcanvas.com/en/tutorials/entity-picking/)

このままだと床やUI等のentityも反応してしまうので、選択したいエンティティには追加でtagを追加してあげて、Pickする側でtagのないエンティティをはじくようにしてあげます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/148084/772a6877-45ae-ebff-98ad-5689cb1a5fda.png)

```javascript
if(entity.tags.has('button'))
```

エンティティのアクションは、まずすべてのエンティティが共通して持つスクリプトが選択イベントを受け取り、自身が何の役割を持ったエンティティなのかによって次に対応したスクリプトが呼ばれるような仕組みにしました。

```javascript:allEntity.js
allEntity.prototype.fire = function(){ //エンティティが選択されたときのコールバック
    if(this.entity.script._scripts.length > 1){
        //自身に紐づく2つめのスクリプトのfireaction()メソッドをコール
        this.entity.script._scripts[1].fireaction();
    }
};
```

## リンクエンティティの実装
リンクエンティティは最初から指定されたURLに飛ばすだけなのでシンプルです。

```javascript:linkEntity.js
linkEntity.prototype.fireaction = function(){
    window.location.href = "hogehoge.com";
};
```

## フォルダエンティティの実装
フォルダエンティティの場合は事前に、フォルダ先となるエンティティを用意してdisableにしておき、選択されたときにenableにすることで実装しています。
また、カメラの視点も変える必要があるので、それぞれ代入してあげます。
リンクエンティティと違い、フォルダエンティティは開け閉めすることができるので、enabledで確認して閉める処理も実装しています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/148084/a318b2c7-a163-b3df-1db0-a40c772d07ec.png)

```javascript:folderEntity.js
folderEntity.prototype.fireaction = function(){
    if(!this.folderentity.enabled){
        this.folderentity.enabled = true;
        var newtop = new pc.Vec3();
        var newbottom = new pc.Vec3();
        newtop.add2(this.folderentity.getLocalPosition(),new pc.Vec3(2,2,2));
        newbottom.add2(this.folderentity._children[this.folderentity._children.length - 1].getPosition(),new pc.Vec3(2,2,2));
        camera.script.scrollwheel.settopbottom(newtop,newbottom,camera.getLocalPosition());        
    }else{
        this.folderentity.enabled = false;
        var newcamera = new pc.Vec3();
        newcamera.add2(this.entity.parent.getPosition(),new pc.Vec3(2,2,2));
        camera.script.scrollwheel.reflash(camera.getLocalPosition(),newcamera);
    }
};
```

## アクションエンティティの実装
特殊な動きを追加で実装したくなった時ようにとりあえず定義してみましたが、現状about meでカメラが移動するときくらいしか使っていません。
基本は固有のアクションごとに任意に実装していく予定ですが、about meでは、フォルダエンティティとほぼ同様で、手前に配置されたテキストエレメントを表示するようにカメラを引くような処理しかしていません。
Orthographicだと距離感がないので、この時だけcameraをPerspectivにするような処理も追加しています。

```javascript:actionEntity.js
actionEntity.prototype.fireaction = function(){
    if(!this.aboutentity.enabled){
        this.aboutentity.enabled = true;
        var newtop = new pc.Vec3();
        var newbottom = new pc.Vec3();
        newtop.add2(this.aboutentity.getLocalPosition(),new pc.Vec3(2,2,2));
        newbottom.add2(this.aboutentity._children[this.aboutentity._children.length - 1].getLocalPosition(),new pc.Vec3(2,2,2));
        camera.camera.projection = pc.PROJECTION_PERSPECTIVE;
        camera.script.scrollwheel.settopbottom(newtop,newbottom,camera.getLocalPosition());
    }else{
        this.aboutentity.enabled = false;
        var newcamera = new pc.Vec3();
        newcamera.add2(this.entity.parent.getPosition(),new pc.Vec3(2,2,2));
        camera.script.scrollwheel.reflash(camera.getLocalPosition(),newcamera);
        camera.camera.projection = pc.PROJECTION_ORTHOGRAPHIC;
    }
};
```
エンティティの実装は以上です。

##カメラの処理
カメラはすべてtop/bottomを指定して、lerpで移動するような処理にしました。
デフォルト，フォルダ選択，などなどのアクションに応じてtop/bottomが入れ替えるような処理にしています。

```javascript:camera.js
this.entity.setLocalPosition(campos.lerp(this.top, this.bottom,this.entity.cameraPosition));
```

mouseのインプットや、touchのインプットに合わせてcamera.cameraPositionの値を0 - 1の範囲で加算/減算してあげることでcanvas内でのスクロールを実現しています。

# 終わりに
ざっとですが、大まかな構成としては以上です。
なお、プロジェクトはパブリックで公開してあるのでぜひそちらも見てみてください。
https://playcanvas.com/project/424527/overview/mywebsite_portal