Photon JavaScript SDKのざっくりとした説明と、PlayCanvasでリアルタイム通信するプロジェクトを作る

### 関連する記事

- [そういえば、JavaScript でリアルタイム通信のゲームってどうやって作るのってなったとき。](https://qiita.com/yushimatenjin/items/3e789afd06ecb7a198a1)
- [【JavaScript】Photon + PlayCanvas を使ってモバイル・デスクトップで動く一人称視点のマルチプレイができる空間を作る【WebGL】](https://qiita.com/yushimatenjin/items/74b69d682fcaeb124308)

## PlayCanvas について

PlayCanvasは、JavaScriptで記述されたWebGLエンジンであり、OSSのエンジンとSaaS型のエディターを兼ね備えているゲームエンジンです。このエンジンを使用することで、Three.jsやBabylon.jsなどの他のエンジンと同様の方法で3Dゲームを作成できます。また、ビジュアルエディターとコードエディターを備えたエディターがあり、クラウド上でプロジェクトを作成し、公開することができます。エディターは、ドラッグアンドドロップによるGUIからの素材の操作や配置が可能で、FBXやOBJなどの3Dモデル素材を簡単に扱えます。

- [PlayCanvas公式サイト](https://playcanvas.jp/)
- 
## Photon について

マルチ対戦のゲームを作るためには、Photonというリアルタイム通信エンジンを使います。このエンジンには、JavaScript SDKというツールが用意されており、それを使ってプログラムを書いていくことで、他のプレイヤーとの通信を実現します。なお、同時に接続できるユーザー数が20人以下であれば、Photonは無料で利用することができます。
- [Photon 公式サイト](https://www.photonengine.com/ja/photon)

![pc1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257048/affb701c-fcef-c178-6699-27031eaad3e9.png)

PlayCanvas でリアルタイム通信をするを[プロジェクト](https://playcanvas.com/project/654895) を作ってみました。

![pc2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257048/89c3cbf7-635a-8cad-00a1-fb9e51757d01.png)

#### Photon SDK のドキュメント

![docs.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257048/392900e6-3f21-fd75-2c57-237d7db3ef29.png)

[Photon JavaScript SDK のドキュメント](https://doc-api.photonengine.com/en/javascript/current/)

#### PlayCanvas のプロジェクト

PlayCanvas はクラウド上でスクリプトを管理します。

![Ap.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257048/809f0563-f3bb-44fd-0f0b-3e0700216a7e.png)

### init.js

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

![Ane.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257048/a226a41d-acfb-272c-0b21-1d6d10f6b4cc.png)

### app.js

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

![ao.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257048/b633c9f3-8f07-3c0b-5120-c134352bc339.png)

```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

![playerrr.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257048/b6fa1354-8f57-3b5e-49a8-03bacd3d4b1f.png)

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);
  }
};
```

前回
[そういえば、JavaScript でリアルタイム通信のゲームってどうやって作るのってなったとき。](https://qiita.com/yushimatenjin/items/3e789afd06ecb7a198a1)

#### 以下 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)