9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PlayCanvasAdvent Calendar 2019

Day 12

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

Last updated at Posted at 2019-12-13

関連する記事

PlayCanvas について

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

Photon について

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

pc1.PNG

PlayCanvas でリアルタイム通信をするをプロジェクト を作ってみました。

pc2.PNG

Photon SDK のドキュメント

docs.PNG

Photon JavaScript SDK のドキュメント

PlayCanvas のプロジェクト

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

Ap.PNG

init.js

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

Ane.PNG

app.js

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

ao.PNG

/*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")としてゲーム画面に配置
// 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")としてゲーム画面に配置
// 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によってPositionRotationかの判定を行う
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

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

/*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

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

raiseEvent(eventCode, data, options);

型はそれぞれ

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

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

raiseEvent - Photon Document

initialize

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

  1. Photon のプロパティを代入
  2. タッチされた際に発火するイベントの定義
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. 回転のキーが押されたら
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 でリアルタイム通信のゲームってどうやって作るのってなったとき。

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

その他の記事はこちらになります。

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

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

日本 PlayCanvas ユーザー会 - Slack

9
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?