【JavaScript】Photon + PlayCanvasを使ってモバイル・デスクトップで動く一人称視点のマルチプレイができる空間を作る【WebGL】



PhotonのJavaScript SDKとPlayCanvasを組み合わせて利用するためのリポジトリです。
大体30分ほどで、ブラウザー上で動く、リアルタイムで位置と回転が同期されているゲームを作ることができます。PlayCanvasとネットワークエンジン PhotonのJavaScript SDKを使用してリアルタイム通信のできる空間を作っていきます。
### 今回作るもの
前回の記事[【JavaScript】 PlayCanvasの公式サンプルを使ってモバイル・デスクトップで動く一人称視点の空間を作る【WebGL】
](https://qiita.com/yushimatenjin/items/4310401a26522ba4d502)の続きとして、リアルタイムで動ける3Dのゲームを作ります。

![](https://i.imgur.com/xAhvs8Q.gif)



※ こちらは一つのURLを3画面で開いたものを録画したものです。
今回の完成品となるプロジェクトはこちらです
https://playcanv.as/p/f6yr7YSW/

それでは早速作っていきます
### 準備するもの

- ブラウザー
- PlayCanvasアカウント  
https://playcanvas.com/
- **Photonアカウント**
https://www.photonengine.com/ja/photon

## 準備 アカウント作成 (PlayCanvasとPhotonアカウントの作成)

### PlayCanvasについて
PlayCanvas Editorを使用して開発をしていきますので、
今回のプロジェクトを実装する上で必要なPlayCanvasアカウントについてはこちらから作成ください。
https://playcanvas.com/

### Photonについて
> マルチプレイを簡単に実現！世界No.1の独立系ネットワーキングエンジンかつマルチプレイヤープラットフォーム— 迅速で信頼性が高く、拡張可能です。インディーかプロかを問わず、あらゆる用途に対応します。
> https://www.photonengine.com/ja/photon

Photonは、Unity多くで使われているようですが、PhotonのREALTIMEにはJavaScriptのSDKも存在しており、20CCU(同時接続)までのプロジェクトでは無料で使用できます。

## 準備 プロジェクトの準備

こちらの手順は[前回](https://qiita.com/yushimatenjin/items/4310401a26522ba4d502)のプロジェクトを作成してる方はスキップして頂いても構いません。

前回の記事
[【JavaScript】 PlayCanvasの公式サンプルを使ってモバイル・デスクトップで動く一人称視点の空間を作る【WebGL】
](https://qiita.com/yushimatenjin/items/4310401a26522ba4d502)の続きですが、こちらを作成されている方は、そのまま利用できます。
この記事で初めてPlayCanvasを触る方は、フォーク機能を使って前回までのプロジェクトをコピーしましょう。


### プロジェクトをフォークする

![](https://i.imgur.com/gbC0MW7.png)
こちらは前回作成したプロジェクトをの`ASSETS`を一部フォルダを整理したプロジェクトです。
このプロジェクトを`フォーク`して使います。

#### 1. フォークするプロジェクトへアクセス

フォークするためのプロジェクトにアクセスをします。
![](https://i.imgur.com/aEB2Joa.png)

https://playcanvas.com/project/696633/

#### 2. `Fork`をクリックしてプロジェクトを複製する

![](https://i.imgur.com/LCKyOMr.png)


![](https://i.imgur.com/eDS8ab5.png)


#### 3. フォークしたプロジェクトから`EDITOR`へアクセス
![](https://i.imgur.com/Ct7dbl6.png)


#### 4. シーンを選択

![](https://i.imgur.com/F91iSfA.png)


### PlayCanvas　エディタ
>PlayCanvas エディタはゲームを構成するシーンやエンティティを作成および編集するために使用する視覚的な編集ツールです。
ブラウザー内で実行できるのでどこでも利用可能。
PlayCanvas エンジンを使ってシーンをレンダリングします。WYSIWYG。
ボタン一つでゲームを新規タブでプレイ。
ライブ編集によりゲーム実行時にもイテレート可能。
> https://developer.playcanvas.com/ja/user-manual/designer/
### 起動

![](https://i.imgur.com/d2GwFrA.png)



![](https://i.imgur.com/L0x83n0.gif)


プロジェクトの作成はこれで完了しました。
起動すると、一人称視点で動き回れる事がわかります。
このプロジェクトからPhotonとPlayCanvasを組み合わせて作っていきましょう。


## Photonの準備 プロジェクト作成 ~SDKの準備

#### 1.管理画面から「新しくアプリを作成する」をクリック

Photonの管理画面にアクセスをします。


https://dashboard.photonengine.com/ja-JP/PublicCloud

![](https://i.imgur.com/VTjBXgh.png)


#### 2.Photon Realtimeを選択してプロジェクトを作成

![](https://i.imgur.com/N1o45le.png)



#### 3. アプリケーションIDのコピー
プロジェクトを作成することができました、こちらのアプリケーションIDを使用しますので保存をしておきましょう。
![](https://i.imgur.com/m4v6OvD.png)

#### 4. SDKをダウンロード

こちらからPhotonのJavaScript SDKをダウンロードします。


![](https://i.imgur.com/k5aDBKG.png)




使用するのは、v4.1.0.1のバージョンのSDKを使用します。


![](https://i.imgur.com/Z9QjQxt.png)

SDKはZIP形式でダウンロードされますので解凍します。


#### 5. SDKをPlayCanvasへドラッグアンドドロップ

解凍したPhotonのSDKをドラッグアンドドロップドロップで配置します

ファイルは`photon-javascript-sdk_v4-1-0-1/lib`ディレクトリにある。`Photon-Javascript_Emscripten_SDK.min.js`を使用します。


![](https://i.imgur.com/hXyCNCL.png)



#### 6. scriptsフォルダーへ移動

ディレクトリの構造によって動作は変わりませんが、管理しやすくするために、scriptsフォルダーにPhotonのSDKをドラッグアンドドロップで移動させましょう。


![](https://i.imgur.com/aRz8K22.png)



## 実装

PlayCanvasでPhotonのSDKを利用するためのスクリプトを書きます。
今回は、3つのファイルを作成します。


### スクリプトファイルを３つ作成する

3つのファイルをエディタ上から保存します。

1. `photon-playcanvas.js`
2. `photon-loadbalancing.js`
3. `photon-playcanvas-connect.js`

#### 1. photon-playcanvas.jsを作成

##### a. `ASSETS`で右クリック→ New Asset → Script

![](https://i.imgur.com/vCVU7Px.png)

##### b. ファイル名`photon-playcanvas.js`を入力
![](https://i.imgur.com/kXNQ4XU.png)

#### 2. 同じ手順で`photon-loadbalancing.js`,`photon-playcanvas-connect.js`を作成
先程と同様の手順で`photon-playcanvas.js`と`photon-playcanvas-connect.js`を作成します。

この手順を終えたときにこの3つのファイルが新規に追加されていれば大丈夫です。

![](https://i.imgur.com/V9t3zzL.png)

### スクリプトファイルをコードエディターで編集する
作成した3つのスクリプトファイルをコードエディタから編集をします。

それぞれのコードについては、GitHub [こちら](https://github.com/yushimatenjin/playcanvas-photon-realtime)のリポジトリにも公開しております。

コードエディタで編集する方法は、`ASSETS`の中にあるスクリプトファイルをダブルクリックすることでコードエディタを開くことができます。コードエディタを開くことができたらそれぞれのファイルにソースコードをコピーしましょう。

![](https://i.imgur.com/xPYt9je.png)

### コードエディタについて
![](https://i.imgur.com/m10pi0z.png)

コードエディタについて、ショートカットキー詳しく知りたい方は公式ドキュメントが参考になります。

> スクリプトアセットはPlayCanvasのコードエディタを使用して編集されます。エディタでファイルを開くにはスクリプトアセットをダブルクリックします。コード編集の権限を持つ全てのユーザはエディタでリアルタイムに共同編集することができます。コードエディタ画面の右下に他のユーザーのアバターが表示されます。
https://developer.playcanvas.com/ja/user-manual/scripting/code-editor/

それぞれのスクリプトをファイルにコピーしていきます。


[こちら](https://github.com/yushimatenjin/playcanvas-photon-realtime/blob/master/photon-playcanvas.js)のコードになります。

#### 1. photon-playcanvas.js

PlayCanvasのゲームが起動されたときにPhotonを利用するためのスクリプトになります。


```photon-playcanvas.js
/*jshint esversion: 6, asi: true, laxbreak: true*/

class PhotonPlayCanvas extends pc.ScriptType {
    initialize () {
      const options = {
        app: this.app,
        appId: this.appId,
        appVersion: this.appVersion,
        wss: this.wss ? 1 : 0
      }
  
      // Photonのセットアップ
      this.app.photon = new LoadBalancing(options)
  
      // Connect to Photon Server
      this.app.photon.connectToRegionMaster(this.region)
  
      this.app.on('lobby:join', () => {
        if (this.app.photon.roomInfos.length === 0) {
          try {
            this.createRoom(this.roomName)
          } catch (e) {
            this.joinRoom(this.roomName)
          }
        } else {
          this.joinRoom(this.roomName)
        }
      })
    }
  
    createRoom (roomName) {
      this.app.photon.createRoom(roomName)
    }
  
    joinRoom (roomName) {
      this.app.photon.joinRoom(roomName)
    }
  }
  
  pc.registerScript(PhotonPlayCanvas)
  // LoadBalancing options
  PhotonPlayCanvas.attributes.add('appId', { type: 'string' })
  PhotonPlayCanvas.attributes.add('appVersion', {
    type: 'string',
    default: '1.0'
  })
  PhotonPlayCanvas.attributes.add('wss', { type: 'boolean', default: true })
  
  // Photon realtime options
  PhotonPlayCanvas.attributes.add('region', {
    type: 'string',
    default: 'jp',
    description:
      'Photon Cloud has servers in several regions, distributed across multiple hosting centers over the world.You can choose optimized region for you.',
    enum: [
      { 'Select Region': 'default' },
      { 'Asia, Singapore': 'asia' },
      { 'Australia, Melbourne': 'au' },
      { 'Chinese Mainland (See Instructions)	Shanghai': 'cn' },
      { 'Canada, East Montreal': 'cae' },
      { 'Europe, Amsterdam': 'eu' },
      { 'India, Chennai': 'in' },
      { 'Japan, Tokyo': 'jp' },
      { 'South America, Sao Paulo': 'sa' },
      { 'South Korea, Seoul': 'kr' },
      { 'USA, East Washington': 'us' },
      { 'USA, West San José': 'usw' }
    ]
  })
  
  // Room options
  PhotonPlayCanvas.attributes.add('roomName', { type: 'string', default: 'room' })
  
```


#### 2. photon-loadbalancing.jsを作成
Photon SDK内の`LoadBalancingClient`を利用して、Photonを使用するためのスクリプトになります。


```photonloadbalancing.js
class LoadBalancing extends Photon.LoadBalancing.LoadBalancingClient {
  constructor (props) {
    // Photon Settings
    const wss = props.wss
    const appId = props.appId
    const appVersion = props.appVersion
    super(wss, appId, appVersion)
    // pc.Application
    this.app = props.app
    this.setLogLevel(4)
  }

  onRoomList (e) {
    this.app.fire('lobby:join')
  }

  onJoinRoom (e) {
    this.app.fire('room:join')
  }

  onEvent (code, content, actorNr) {
    const payload = { code, content, actorNr }
    if (code === 0) {
      this.app.fire('sync:position', payload)
      return
    }
    if (code === 1) {
      this.app.fire('sync:rotation', payload)
    }
  }

  onActorJoin (e) {
    const payload = Object.assign({}, this.myRoomActors())
    this.app.fire('sync:players', payload)
  }

  onActorLeave (e) {
    const payload = Object.assign({}, this.myRoomActors())
    this.app.fire('sync:players', payload)
  }
}

```

#### 3. photon-playcanvas-connect.jsを作成
Photonからイベントが発火された際にPlayCanvasに変更を加えるスクリプトになります。

```photon-playcanvas-connect.js

/*jshint esversion: 6, asi: true, laxbreak: true*/
const EVENT_LIST = {
    POSITION: 0,
    ROTATION: 1,
    COMMAND: 2
  }
  class PhotonPlaycanvasConnect extends pc.ScriptType {
    raiseEvent (event, data) {
      // Photonを使用
      this.app.photon.raiseEvent(event, data)
    }
    // スクリプト読み込まれた際に一度実行される
    initialize () {
      this.isSync = false
      this.app.on('room:join', () => {
        this.isSync = true
        if (pc.platform.mobile) {
          //モバイル端末だった場合の処理
  
          this.app.touch.on(pc.EVENT_TOUCHMOVE, () => {
            // get current Player position & rotation
            const position = this.Player.getLocalPosition()
            const rotation = this.Player.findByName('Camera').getLocalRotation()
  
            this.raiseEvent(EVENT_LIST.ROTATION, Object.assign({}, rotation))
            this.raiseEvent(EVENT_LIST.POSITION, Object.assign({}, position))
          })
        } else {
          // KEYDOWNイベントが発火されたら同期をする
          this.app.keyboard.on(pc.EVENT_KEYDOWN, () => {
            // get current Player position & rotation
            const position = this.Player.getLocalPosition()
  
            this.raiseEvent(EVENT_LIST.POSITION, Object.assign({}, position))
          })
          // MOUSEMOVEイベントが発火されたら同期をする
          this.app.mouse.on(pc.EVENT_MOUSEMOVE, () => {
            const rotation = this.Player.findByName('Camera').getLocalRotation()
            this.raiseEvent(EVENT_LIST.ROTATION, Object.assign({}, rotation))
          })
        }
      })
  
      this.app.on('sync:players', actors => {
        const prefix = 'player_'
        const tagName = 'player'
        const actorNums = Object.values(actors).map(actor => actor.actorNr)
        // Playerを同期する
        for (let actor of Object.values(actors)) {
          const { isLocal, actorNr, name } = actor
          if (!isLocal) {
            const otherPlayer = this.PlayerTemplate.clone()
            otherPlayer.setName(`${prefix}${actorNr}`)
            otherPlayer.tags.add(tagName)
            otherPlayer.enabled = true
            this.app.root.addChild(otherPlayer)
            otherPlayer.setLocalPosition(0, 0, 0)
          }
        }
        // Playerのタグタグから現在いないPlayerを消す
        {
          const players = this.app.root.findByTag(tagName)
          for (let player of players) {
            const actorNr = player.name.replace(prefix, '')
            if (!actorNums.includes(Number(actorNr))) {
              player.destroy()
            }
          }
        }
      })
  
      this.app.on('sync:rotation', ({ content, actorNr }) => {
        const { y, w } = content
        const entity = this.app.root.findByName(`player_${actorNr}`)
        if (!entity) return
  
        entity.setLocalRotation(0, y, 0, w)
      })
  
      this.app.on('sync:position', ({ content, actorNr }) => {
        const { x, y, z } = content
        const entity = this.app.root.findByName(`player_${actorNr}`)
        if (!entity) return
  
        entity.setPosition(x, y + this.yOffset, z)
      })
    }
  }
  
  pc.registerScript(PhotonPlaycanvasConnect)
  
  // Room Option
  PhotonPlaycanvasConnect.attributes.add('roomName', {
    type: 'string',
    default: 'room'
  })
  
  // Sync option
  PhotonPlaycanvasConnect.attributes.add('Player', { type: 'entity' })
  PhotonPlaycanvasConnect.attributes.add('PlayerTemplate', { type: 'entity' })
  PhotonPlaycanvasConnect.attributes.add('yOffset', {
    type: 'number',
    default: -0.7
  })
  
  PhotonPlaycanvasConnect.attributes.add('reverse', {
    title: "逆向き",
    type: 'boolean',
    default: true
  })
  
  
```


## 設置
スクリプトをPlayCanvas Editorを使って適用していきます。

### スクリプトの読み込み順を変える
PlayCanvasではSCRIPTを読み込む順番をエディタから制御します。
今回PhotonのSDKを使用しているのですが、SDKが読み込まれる前に、PhotonのAPIを使用するとエラーが出てしまいますので、スクリプトの読み込み順序を変更していきます。

#### 1. 設定ボタンをクリック
![](https://i.imgur.com/ZD7CqLk.png)

#### 2. ASSET LOAD TASKを変更する
スクリプトの読み込み順序を、PhotonのSDK → photon-loadbalancing　→　その他の順に変更します。
##### 変更前
![](https://i.imgur.com/wYAeqV9.png)

##### 変更後

![](https://i.imgur.com/rv5yl2X.png)

### スクリプトの追加

#### 1. Rootにスクリプトコンポーネントを追加
![](https://i.imgur.com/EPj3k53.png)

#### 2. ADD SCRIPTSから`photon-playcanvas`, `photon-playcanvas-connect`を追加

##### a. パースボタンをクリック
追加ができたらパースボタンをクリックします。
スクリプトのパースが行われると、`roomName`や`Player`などの属性を追加できる要素が追加されます。

![](https://i.imgur.com/iqNdy91.png)


`!`マークなどが出た際には、パースボタンを押してから一度、パースボタンの横からスクリプトを削除してから、もう一度、`ADD SCRIPT`を押してください。



#### 3. `Photon`のアプリケーションIDを追加する

##### a. アプリケーションIDを取得
こちらのページからPhotonのアプリケーションIDを取得します。
![](https://i.imgur.com/BhCfkZB.png)

##### b. 属性にアプリケーションIDをペースト

![](https://i.imgur.com/IWMKAwT.png)

https://dashboard.photonengine.com/ja-JP/PublicCloud

これでPhotonとPlayCanvasのつなぎこみの設定は終わりました。
次にプレイヤーを追加していきます。

### その他の属性の設定

![](https://i.imgur.com/xAhvs8Q.gif)


#### 1. `Player`を選択して追加

##### a. `PhotonPlaycanvasConnect`スクリプトの`Player`属性をクリック

![](https://i.imgur.com/QW13ghn.png)

##### b. `ヒエラルキー`から`Player`エンティティを選択
![](https://i.imgur.com/ZgNbcPL.png)

### スクリプト属性について



これで、スクリプトの`Player`属性に`Player`エンティティが追加されました。このような形でPlayCanvasでは、ソースコードを変更せず値を変更したい場合などについてはスクリプト属性を使用して値を設定します。

> スクリプト属性
> スクリプトのアトリビュート機能は、スクリプト内で使用する変数をPlayCanvasエディタ内で編集することができるようにする便利な機能です。この機能を使うことで、一度コードを書いた後にエンティティごと作られるインスタンスにそれぞれ違うパラメータを設定する調整ができるようになります。これにより、アーティスト、デザイナーやその他のプログラマーではないチームメンバーがコードを書かずに値を変更できるにプロパティを露出させることができます。
> https://developer.playcanvas.com/ja/user-manual//script-attributes/


### 自分以外を表示するためのEntityを追加

今回はFPS視点なので、自分のキャラクターは追加をしないで、他の人が入ってきたときに`きつね`のアバターで参加をしてもらうことにします。今ヒエラルキーを上にある`Fox`を使用して、他の人のアバターを設定しましょう。



#### 1. `ヒエラルキーから`エンティティを追加する

ヒエラルキーの`+`ボタンをクリックし、`Entity`を追加します。


#### 2. エンティティの名前を`PlayerTemplate`という名前に変更します。

![](https://i.imgur.com/yaKddtj.png)

#### 3. ヒエラルキー上の`Fox`を`PlayerTemplate`ヒエラルキーの配下に配置
ヒエラルキー上から、`Fox`エンティティを`PlayerTemplate`の配下にドラッグアンドドロップをして配置します。
![](https://i.imgur.com/7Uf1n7s.png)

#### 4. `Root`のスクリプトコンポーネントに適用する

![](https://i.imgur.com/0h0JSvl.png)


#### 5. `PlayerTemplate`スクリプトを非表示にする
この`PlayerTemplate`としたエンティティは他のプレイヤーが入ってきた際にスクリプトから制御をして表示を切り替えますので、ページを起動した際には、非表示にしておきます。

![](https://i.imgur.com/GpAbTRm.png)

ここまでできれば終わりです。あとは起動するだけでマルチプレイのゲームができているかと思います。


### 起動
これで起動をしてみます。

![](https://i.imgur.com/XgcP42e.gif)

これでPlayCanvasで動くマルチプレイのできるゲームを作成することができました。

ルームの機能がありますので、ルーム名を変えたりすることで参加するユーザーを区切ることもできます。
本日使用したPhotonのドキュメントと、PlayCanvasのドキュメントはそれぞれこちらになります。

Photon LoadBalancingClient
https://doc-api.photonengine.com/en/javascript/current/Photon.LoadBalancing.LoadBalancingClient.html

PlayCanvas ユーザーマニュアル
https://developer.playcanvas.com/ja/user-manual/


## 補足

#### 向きを変更する
逆向きになっていますのでヒエラルキーをから`Fox`を選択して`y`の`rotate`を`180`度に設定してあげるといい感じになると思います。


## ゲームを公開する

PlayCanvasでは作成したゲームをウェブ上ですぐに公開できます。


#### 1. 左のMEBUからPUBLISH/DOWNLOADをクリック
![publish.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257048/9ac652fa-751c-50f0-e975-da30b3933d6f.png)

#### 2. PUBLISH TO PLAYCANVASから公開
![oc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257048/7c09c09a-96ef-9e51-83da-6575ef94ec8a.png)


#### 3. BUILDSを確認
PUBLISHが成功するとBUILDSに共有できるURLが生成されます。
こちらを共有することで、第三者に完成したプロジェクトを公開できます。

![URL.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257048/00ef1ec1-1e29-1743-4639-1a77d2cff7c9.png)




今回の完成品となるプロジェクトはこちらになります。　

https://playcanv.as/p/f6yr7YSW/



今回のプロジェクトで質問や意見がありましたら。GitHubのIssueか@mxcn3まで連絡をお願いします。



<font color="OrangeRed">
PlayCanvas開発で参考になりそうな記事の一覧です。
</font>
<font color="OrangeRed">

- [PlayCanvas入門- モデルの作成～ゲームに入れ込むまで](https://qiita.com/yushimatenjin/items/2a2c35fe7b1d08f9e361)
- [JavaScriptでスロットを実装する。【PlayCanvas】](https://qiita.com/yushimatenjin/items/e84c42da74df1eb07386)
- [3Dモデルのビューワーを3分で作る【初めてのPlayCanvas】](https://qiita.com/yushimatenjin/items/4b09c9e2076a0b2a6933)
</font>
- [PlayCanvasのコードエディターでes6に対応する](https://qiita.com/yushimatenjin/items/a61a21c64c1c1a550dd4)
- [Gulpのプラグインを書いたらPlayCanvasでの開発がめちゃくちゃ便利になった](https://qiita.com/yushimatenjin/items/5f0f178e8a4ba4a5ee57)
- [PlayCanvas Editorに外部スクリプトを読み込む新機能が追加されたので開発方法を考える。- Reduxを組み込む](https://qiita.com/yushimatenjin/items/7a64220cceac66843d7d)
- [React Native + PlayCanvasを使ってスマートフォンゲームを爆速で生み出す](https://qiita.com/yushimatenjin/items/7c7ad5d35473c11f32f2)
- [PlayCanvasのエディター上でHTML, CSSを組み込む方法](https://qiita.com/yushimatenjin/items/814b4a32db53397219df)


</font>


### PlayCanvasのユーザー会のSlackを作りました！

 少しでも興味がありましたら、ユーザー同士で解決・PlayCanvasを推進するためのSlackを作りましたので、もしよろしければご参加ください！

- [日本PlayCanvasユーザー会 - Slack](https://join.slack.com/t/playcanvasjphq/shared_invite/zt-9aihkaep-TNA04tqgvYDFhBJABLLckw)