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

Photon JavaScript SDKのざっくりとした説明と、PlayCanvasでリアルタイム通信をするゲームを作る。

この記事の続きになります

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

概要

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

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

今回デモとして作成したこちらのゲームは[WASD]で移動ができるシンプルなゲームとなっております。
2窓などをしていただければ一人でも遊べます。

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

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. 回転のキーが押されたら
  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);
    }
    };
    

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

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

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

日本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
ユーザーは見つかりませんでした