Help us understand the problem. What is going on with this article?

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

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

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

HTML5 VTuber LIVEシステムとか作りたい

JavaScriptでリアルタイム通信するゲームを作る

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

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

PlayCanvasについて

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

Photonについて

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

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

https://www.photonengine.com/ja/photon

PlayCanvasとPhotonを組み合わせる

demo123.png

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

https://github.com/utautattaro/Photon-for-PlayCanvas

Photon x PlayCanvasでできること

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

プロジェクトをフォークして作成する

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

1. Forkする

こちらのデモ用のPlayCanvasのプロジェクトはPublicなプロジェクトとなっておりますので、PlayCanvasにログインしアクセスしていただくと、Forkできます。

2. PlayCanvas EditorのAppIdを設定する

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

a. Photonに登録する
  1. Photon 公式サイトにアクセスして登録をします。
b. PhotonからAppId取得する
  1. Photonのダッシュボードから新規アプリを作成し、AppIdを取得します。
c. AppIdをPlayCanvas上に設定する
  1. Forkしたプロジェクトに入る
  1. コピーしたAppIdを貼り付ける
PhotonのJavaScriptSDKをダウンロード

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

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

https://www.photonengine.com/ja/sdks  

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

② SETTINGSをクリック

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

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

ゲームを起動する

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

a. Launchで確認する

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

スクリプトについて

init.js

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

app.js

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

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

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. 回転のキーが押されたら
  3. 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.htmlstyle.cssに定義されているUIと、ゲームが表示されます。

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

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

おわりに

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

→ player.jsをいじった記事を書きました
そういえば、JavaScriptでリアルタイム通信のゲームってどうやって作るのってなったとき。その2

https://qiita.com/yushimatenjin/items/eb4311c3640a20bbdfb9

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

入門
- PlayCanvas入門- モデルの作成~ゲームに入れ込むまで
- JavaScriptでスロットを実装する。【PlayCanvas】
- 3Dモデルのビューワーを3分で作る【初めてのPlayCanvas】
- PlayCanvasのコードエディターでes6に対応する
- Gulpのプラグインを書いたらPlayCanvasでの開発がめちゃくちゃ便利になった
- PlayCanvas Editorに外部スクリプトを読み込む新機能が追加されたので開発方法を考える。- Reduxを組み込む

その他の記事はこちらになります。
- AR年賀状を作る
- React Native + PlayCanvasを使ってスマートフォンゲームを爆速で生み出す
- PlayCanvasのエディター上でHTML, CSSを組み込む方法
- 【iOS13】新しくなったWebVRの使い方

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

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

日本PlayCanvasユーザー会 - Slack

yushimatenjin
インターネットに無限の可能性を感じています。
https://twitter.com/Mxcn3
playcanvas
"PlayCanvasは、ブラウザ向けに作られたWebGL/HTML5ゲームエンジンです。PlayCanvas運営事務局は日本国内でのPlayCanvasの普及を目的に活動しています"
https://playcanvas.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした