LoginSignup
8

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-13

新しい記事がこちらになります

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

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. 回転のキーが押されたら
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のユーザー会のSlackを作りました!

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

日本PlayCanvasユーザー会 - Slack

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
8