そういえば、JavaScriptでリアルタイム通信のゲームってどうやって作るのってなったとき。


# 新しい記事がこちらになります
【JavaScript】Photon + PlayCanvasを使ってモバイル・デスクトップで動く一人称視点のマルチプレイができる空間を作る【WebGL】
https://qiita.com/yushimatenjin/items/74b69d682fcaeb124308


# PlayCanvasアドベントカレンダー11日目！

投稿日が遅れてしまいましたが、この記事はPlayCanvasアドベントカレンダー 12月11日の記事となります。
PhotonのJS SDKとゲームエンジンを組み合わせて色々できそうだなと思っていたのでアドベントカレンダーを機にもう一度触ってみます。

<S>HTML5 VTuber LIVEシステムとか作りたい</S>


# JavaScriptでリアルタイム通信するゲームを作る
JavaScriptはよく触ってるけど、そういえばリアルタイムの通信/対戦ゲームってどうやって作るのってなった時に参考になるかもしれないです。
PlayCanvasでマルチ対戦のゲームを作るために今回はPhotonというネットワークエンジンを使用して作成します。役割としては描画の部分は`PlayCanvas`、リアルタイム通信の部分については`Photon`を使用して作っていきます。



今回デモとして作成したこちらのゲームは[WASD]で移動ができるシンプルなゲームとなっております。


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">PhotonとPlayCanvas<br>[W A S D]で移動(スマホはタッチで飛ぶだけ)<a href="https://twitter.com/hashtag/playcanvas?src=hash&amp;ref_src=twsrc%5Etfw">#playcanvas</a> <a href="https://twitter.com/hashtag/%EF%BC%92%E7%AA%93?src=hash&amp;ref_src=twsrc%5Etfw">#２窓</a><a href="https://t.co/mPDM21Lfme">https://t.co/mPDM21Lfme</a></p>&mdash; はが (@Mxcn3) <a href="https://twitter.com/Mxcn3/status/1205359807101718528?ref_src=twsrc%5Etfw">December 13, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


## PlayCanvasについて

PlayCanvasはJavaScriptで記述されたWebGLのエンジンで、最近GitHubのスター数が5000を超えました。
OSSのEngineと、SaaS型のエディターを兼ね備えているゲームエンジンです。engineについては、Three.js, Babylon.jsなどに似た使い方を使用することができます。エディターはビジュアルエディターとコードエディターがあり、エディターはクラウド上でプロジェクトの作成から公開までが出来るもので、`FBX`や`OBJ`といった3Dモデルの素材については、ブラウザへドラッグアンドドロップをすることでGUIから操作・配置ができるエディターとなっております。

## Photonについて

Photonについては、Unity多く使われているようですが、JavaScriptのSDKもありますので、Photon JavaScript SDKを使用してマルチ対戦のゲームを作っていきます。

Photonは20CCU（同時接続）までであれば、無料で使用できます。  


[https://www.photonengine.com/ja/photon](https://www.photonengine.com/ja/photon)



## PlayCanvasとPhotonを組み合わせる

![demo123.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257048/d5c4191e-c975-a413-f9f7-648a8e4ec7d6.png)

PlayCanvasとPhotonを組み合わせるにはこちらのリポジトリを参考になります。

[https://github.com/utautattaro/Photon-for-PlayCanvas](https://github.com/utautattaro/Photon-for-PlayCanvas)

## Photon x PlayCanvasでできること

3Dや2Dのリアルタイムゲームを作ることができます。こちらのタンクのゲームは4人がリアルタイム1つのルームに入り対戦できるゲームです。

<div align="center">
<img src="https://user-images.githubusercontent.com/39250588/70678115-08aeeb80-1c89-11ea-95f1-fac6da8d0cbe.PNG" width="400" align="center" />
</div>


## プロジェクトをフォークして作成する
上記のリポジトリを参考に、自分でプロジェクトを使用して作ることもできますが、PlayCanvasには便利なForkという機能があるのでそちらを利用して作ってみます。

#### 1. Forkする

<div align="center">
<img src="https://i.imgur.com/vmegJ8o.png" width="400" align="center" style="align='center'"/>
</div>


こちらのデモ用の[PlayCanvasのプロジェクト](https://playcanvas.com/project/654895)はPublicなプロジェクトとなっておりますので、PlayCanvasにログインしアクセスしていただくと、Forkできます。


#### 2. PlayCanvas EditorのAppIdを設定する

今回簡単なプロジェクトを作る上では、AppIdを設定することができればマルチプレイ対戦ができるようになっています。AppIdの設定にはPhotonのIDを作る必要があるので、設定します。

##### a. Photonに登録する

1. [Photon 公式サイト](https://www.photonengine.com/ja/photon)にアクセスして登録をします。

<div align="center">
<img src="https://i.imgur.com/sajAMrx.png" width="400" align="center" />
</div>



##### b. PhotonからAppId取得する

1. Photonのダッシュボードから新規アプリを作成し、AppIdを取得します。

<div align="center">
<img src="https://i.imgur.com/Xj9xOSZ.png" width="400" align="center" />
</div>


##### c. AppIdをPlayCanvas上に設定する

1. Forkしたプロジェクトに入る  

<div align="center">
<img src="https://i.imgur.com/7JjIX8o.png" width="400" align="center" />
</div>


2. コピーしたAppIdを貼り付ける  

<div align="center">
<img src="https://i.imgur.com/cHjx2CQ.png" width="400" align="center" />
</div>

##### PhotonのJavaScriptSDKをダウンロード
PlayCanvasでPhotonを使用するためにJavaScriptのSDKをダウンロードします。

1. PhotonのSDKの最新版をダウンロード

[https://www.photonengine.com/ja/sdks](https://www.photonengine.com/ja/sdks)　　

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


① SDKを解凍し\photon-javascript-sdk_v4-1-0-1\libの中にある`Photon-Javascript_SDK`をPlayCanvasにアップロード

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

② SETTINGSをクリック

③ Photon-Javascript_SDKのSCRIPT LOADIGN ORDERを一番上に！

しかしこのままだとPlayCanvasで使用する際にPhotonのSDKを読み込む前にPhotonのAPIを使用するプログラムが書いてあるため、SETTIGNSからSCRIPTのLOADING ORDERを変更します。


#### ゲームを起動する

PlayCanvasではエディター上には開発時に使用する`Launch`とリリースする際に使用する`Publish`の2つの機能があります。

##### a. Launchで確認する

開発時にはこちらのLaunchボタンを押して確認します。

<div align="center">
<img src="https://i.imgur.com/poZB3Bu.png" width="400" align="center" />
</div>


## スクリプトについて


### init.js
起動Photonの起動を行っています。`index.html`, `style.css`を読み込みUIのセットアップも行います。


### app.js
app.jsにてPhotonとPlayCanvasの同期をしています。Photonには様々なライフサイクルで発行されるイベントがありますが、今回使用したものについて説明します。

```javascript
/*jshint esversion: 6, asi: true, laxbreak: true*/
const App = pc.createScript('app');

App.prototype.initialize = function() {
    this.photon = this.app.root.children[0].photon;
    this.photon.setLogLevel(999);
    this.photon.onJoinRoom = () => {
        Object.values(this.photon.actors).map(event => {
            const { isLocal, actorNr } = event;
            if (isLocal) {
            } else {
                const entity = new pc.Entity();
                entity.addComponent("model", {
                    type: "box"
                });
                entity.setPosition(0, 1, 0);
                entity.tags.add(`${actorNr}`);
                this.app.root.addChild(entity);
            }
        });
    };

    this.photon.onActorJoin = event => {
        const { isLocal, actorNr } = event;
        if (isLocal) {
        } else {
            const entity = new pc.Entity();
            entity.addComponent("model", {
                type: "box"
            });
            entity.setPosition(0, 1, 0);
            entity.tags.add(`${actorNr}`);
            this.app.root.addChild(entity);
        }
    };

    this.photon.onEvent = (code, content, actorNr) => {
        const entities = this.app.root.findByTag(`${actorNr}`);
        const [entity] = entities;

        switch (code) {
            case 1: {
                const { x, y, z } = content;
                entity.setLocalPosition(x, y, z);
                break;
            }
            case 2: {
                const { x, y, z, w } = content;
                entity.setLocalRotation(x, y, z, w);
                break;
            }
            default: {
                break;
            }
        }
    };
};
```



##### photon.onJoinRoom
onJoinRoomはルームに入った際の処理になります。今回のような作りでは呼ばれるタイミング
    - ルームを新しく作ったとき
    - 既存のルームに入る時


1. ルームに入った時
2. 他のプレイヤーを取得
3. 新しいエンティティ("Model:Box")としてゲーム画面に配置


```javascript
// 1.ルームに入った時
this.photon.onJoinRoom = () => {
    //2. 他のプレイヤーを取得
    Object.values(this.photon.actors).map(event => {
        const { isLocal, actorNr } = event;
        if (isLocal) {
        } else {
            // 新しいエンティティを作成
            const entity = new pc.Entity();
            entity.addComponent("model", {
                type: "box"
            });
            // 新しく生成したエンティティのポジションの設定とタグを付与している
            entity.setPosition(0, 1, 0); // x, y, z
            entity.tags.add(`${actorNr}`);
            // PlayCanvasの画面上に配置する
            //3. 新しいエンティティ("Model:Box")としてゲーム画面に配置
            this.app.root.addChild(entity);
        }
    });
};
```

##### photon.onActorJoin
onActorJoinは自分の入っているルームに他のプレイヤーが参加してきたときの処理になります。

1. 他のプレイヤーが入ってきた時
2. 新しいエンティティを作成
3. 新しいエンティティ("Model:Box")としてゲーム画面に配置

```javascript
// 1.他のプレイヤーが入ってきた時
this.photon.onActorJoin = event => {
    // isLocal: 自分であるかの判定
    // actorNr: 入った順に1から振られる、ユニークな番号
    const { isLocal, actorNr } = event;
    // 自分自身であるかの判定
    if (isLocal) {
    } else {
        //  2. 新しいエンティティを作成
        const entity = new pc.Entity();
        entity.addComponent("model", {
            type: "box"
        });
        // ポジションとタグを設定
        entity.setPosition(0, 1, 0);
        entity.tags.add(`${actorNr}`);
        // 3. 新しいエンティティ("Model:Box")としてゲーム画面に配置
        this.app.root.addChild(entity);
    }
};

```



##### photon.onEvent
onEventは、今回は`player.js`により、データが送信された場合に呼び出されます。

1. `actorNr`のタグを元に`エンティティ`を検索
2. `Code`によって`Position`か`Rotation`かの判定を行う


```javascript
this.photon.onEvent = (code, content, actorNr) => {
    // 1. `actorNr`のタグを元に`エンティティ`を検索
    const entities = this.app.root.findByTag(`${actorNr}`);
    const [entity] = entities;
    
    //2. `Code`によって`Position`か`Rotation`かの判定を行う
    switch (code) {
        case 1: {
            const { x, y, z } = content;
            entity.setLocalPosition(x, y, z);
            break;
        }
        case 2: {
            const { x, y, z, w } = content;
            entity.setLocalRotation(x, y, z, w);
            break;
        }
        default: {
            break;
        }
    }
};
```



### player.js

Player.jsはキーボードの操作が押された際にPhotonサーバーにデータを送るものになります。

```javascript
/*jshint esversion: 6, asi: true, laxbreak: true*/
const Player = pc.createScript("player");

// PlayCanvas Editor上で使用するためにAttributesを作成
// Attributesの説明
// https://developer.playcanvas.com/ja/user-manual/scripting/script-attributes/

Player.attributes.add("moveSpeed", { type: "number", default: 0.1 });
Player.attributes.add("rotateSpeed", { type: "number", default: 2 });

const move = (direction, entity, self) => {
    const { photon } = self;
    switch (direction) {
        case "up": {
            entity.translateLocal(0, 0, -self.moveSpeed);
            break;
        }
        case "down": {
            entity.translateLocal(0, 0, self.moveSpeed);
            break;
        }
        default: {
            break;
        }
    }
    // send position
    // コード番号 1で現在のEntityのポジションを送信
    photon.raiseEvent(1, entity.getLocalPosition());
};

const rotate = (direction, entity, self) => {
    const { photon } = self;

    switch (direction) {
        case "left": {
            entity.rotate(0, -self.rotateSpeed, 0);
            break;
        }
        case "right": {
            entity.rotate(0, self.rotateSpeed, 0);
            break;
        }
        default: {
            break;
        }
    }
    // send rotation
    // コード番号 2で現在のEntityのポジションを送信
    photon.raiseEvent(2, entity.getLocalRotation());
};

Player.prototype.initialize = function () {
    this.photon = this.app.root.children[0].photon;
    if (this.app.touch) {
        this.app.touch.on(
            pc.EVENT_TOUCHSTART,
            () => {
                const { photon } = this
                const { x, y, z } = this.entity.getPosition();
                this.entity.rigidbody.teleport(x, y + 0.5, z);
                photon.raiseEvent(1, this.entity.getLocalPosition());

                photon.raiseEvent(2, this.entity.getLocalRotation());


            },
            null
        );
    }
};


// update code called every frame
Player.prototype.update = function (dt) {
    const { keyboard } = this.app;
    // 移動のキーが押されていたら
    if (keyboard.isPressed(pc.KEY_W) || keyboard.isPressed(pc.KEY_UP)) {
        move("up", this.entity, this);
    } else if (keyboard.isPressed(pc.KEY_S) || keyboard.isPressed(pc.KEY_DOWN)) {
        move("down", this.entity, this);
    }
    
    // 回転のキーが押されたら
    if (keyboard.isPressed(pc.KEY_A) || keyboard.isPressed(pc.KEY_LEFT)) {
        rotate("right", this.entity, this);
    } else if (keyboard.isPressed(pc.KEY_D) || keyboard.isPressed(pc.KEY_RIGHT)) {
        rotate("left", this.entity, this);
    }
};

```

##### photon.raiseEvent

[RPCs and RaiseEvent
](https://doc.photonengine.com/ja-jp/pun/v2/gameplay/rpcsandraiseevent)


raiseEventを使用して任意のタイミングでデータの送信を行います。今回は移動が行われた時と回転が行われた際に現在の場所を送信しています。

```javascript    
raiseEvent(eventCode, data, options)
```

型はそれぞれ

```
 eventCode:number
 object: object
 options: object
```



##### 移動時
```javascript
    ....
    // send position
    // コード番号 1で現在のEntityのポジションを送信
    photon.raiseEvent(1, entity.getLocalPosition());
};

```

##### 回転時
```javascript
    ...
    // send rotation
    // コード番号 2で現在のEntityのポジションを送信
    photon.raiseEvent(2, entity.getLocalRotation());
};
```




###### 参考
[raiseEvent - Photon Document](https://doc-api.photonengine.com/en/javascript/current/Photon.LoadBalancing.LoadBalancingClient.html)


##### initialize
PlayCanvasのスクリプトのライフサイクルには、スクリプト一度だけ呼び出される`initialize`、毎フレーム呼び出される`Update`があります。

1. Photonのプロパティを代入
2. タッチされた際に発火するイベントの定義

```javascript
Player.prototype.initialize = function () {
    // Rootエンティティからphotonプロパティを取得
    // 1. Photonのプロパティを代入
    this.photon = this.app.root.children[0].photon;
    
    // タッチ操作が可能だったら
    if (this.app.touch) {
    
    // タッチ操作が行われたら
    // PlayCanvas: タッチについて
    // https://support.playcanvas.jp/hc/ja/articles/227190908
    
    // 2. タッチされた際に発火するイベントの定義
    // PlayCanvas: イベント種類について
    // https://developer.playcanvas.com/ja/user-manual/scripting/communication/
        this.app.touch.on(
            pc.EVENT_TOUCHSTART,
            () => {
                const { photon } = this
                const { x, y, z } = this.entity.getPosition();
                this.entity.rigidbody.teleport(x, y + 0.5, z);
                // コード1で現在のポジションを送信
                photon.raiseEvent(1, this.entity.getLocalPosition());
                // コード2で現在の回転を送信
                photon.raiseEvent(2, this.entity.getLocalRotation());


            },
            null
        );
    }
};
```


##### update

`update`はフレームごとに呼ばれます

1. 移動のキーが押されていたら
2. 回転のキーが押されたら
3. 
```javascript
Player.prototype.update = function (dt) {
    const { keyboard } = this.app;
    // 1. 移動のキーが押されていたら
    if (keyboard.isPressed(pc.KEY_W) || keyboard.isPressed(pc.KEY_UP)) {
        // 移動
        move("up", this.entity, this);
    } else if (keyboard.isPressed(pc.KEY_S) || keyboard.isPressed(pc.KEY_DOWN)) {
        // 移動
        move("down", this.entity, this);
    }
    
    // 2. 回転のキーが押されたら
    if (keyboard.isPressed(pc.KEY_A) || keyboard.isPressed(pc.KEY_LEFT)) {
        // 回転
        rotate("right", this.entity, this);
    } else if (keyboard.isPressed(pc.KEY_D) || keyboard.isPressed(pc.KEY_RIGHT)) {
        // 回転
        rotate("left", this.entity, this);
    }
};
```

##### 起動

ゲーム起動すると`index.html`と`style.css`に定義されているUIと、ゲームが表示されます。

![](https://i.imgur.com/iExKGcQ.jpg)



##### b. PublishをしてURLを共有する

`Launch`はログインしているユーザーしか閲覧できないのでこちらから公開用のURLを発行することで、インターネット上で共有できます。

<div align="center">
<img src="https://i.imgur.com/SgCK93v.png" width="400" align="center" />
</div>

# おわりに

リアルタイムの通信をするゲームもかなり簡単に作れるそうですね。
もう少し興味がある方は、PhotonSDKのAPIは`player.js`で記述されているのでそちらをいじると色々できます。

→　player.jsをいじった記事を書きました
[そういえば、JavaScriptでリアルタイム通信のゲームってどうやって作るのってなったとき。その２](https://qiita.com/yushimatenjin/items/eb4311c3640a20bbdfb9)

[https://qiita.com/yushimatenjin/items/eb4311c3640a20bbdfb9](https://qiita.com/yushimatenjin/items/eb4311c3640a20bbdfb9)

#### 以下PlayCanvas開発で参考になりそうな記事の一覧です。

入門
- [PlayCanvas入門- モデルの作成～ゲームに入れ込むまで](https://qiita.com/yushimatenjin/items/2a2c35fe7b1d08f9e361)
- [JavaScriptでスロットを実装する。【PlayCanvas】](https://qiita.com/yushimatenjin/items/e84c42da74df1eb07386)
- [3Dモデルのビューワーを3分で作る【初めてのPlayCanvas】](https://qiita.com/yushimatenjin/items/4b09c9e2076a0b2a6933)
- [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)

その他の記事はこちらになります。
- [AR年賀状を作る](PlayCanvasで簡単に簡単なWebARコンテンツを作る！)
- [React Native + PlayCanvasを使ってスマートフォンゲームを爆速で生み出す](https://qiita.com/yushimatenjin/items/7c7ad5d35473c11f32f2)
- [PlayCanvasのエディター上でHTML, CSSを組み込む方法](https://qiita.com/yushimatenjin/items/814b4a32db53397219df)
- [【iOS13】新しくなったWebVRの使い方 ](https://qiita.com/yushimatenjin/items/344d86b3d193e0a88567)
### PlayCanvasのユーザー会のSlackを作りました！
少しでも興味がありましたら、ユーザー同士で解決・PlayCanvasを推進するためのSlackを作りましたので、もしよろしければご参加ください！

[日本PlayCanvasユーザー会 - Slack](https://t.co/IdSRIfJhN5?amp=1)