TypeScriptでオンラインゲーム

Photon-Javascript_SDKを使ったJavascriptオンラインプラウザゲームでは、JSファイルをまとめるモジュールバンドラー(webpack)で組み合わせる必要があります。
JavaScriptに型定義などの機能を追加したTypeScript、Photon Client Javascript SDKそしてmatterモジュールをwebpackで組み合わせます。
通信エアホッケーを例として、同時実行して2人で遊ぶゲームが出来上がるまでを記載します。

1)Visual Studio Codeをインストールします。メモ帳でもJSファイル、TSファイルのソースコードの編集はできますが、ソースコードのデバッグにはVisual Studio Codeがオススメです。

2)Node.jsをインストールします。公式(https://nodejs.org/ja/)からダウンロードします。
Node.jsをインストールするとnpmコマンドが利用可能になります。

3)npmコマンドを使ってTypeScriptコンパイラをインストールします。
コマンドプロンプトやターミナルなどを開き、以下2つを実行してください。
$ npm install -g ts-node
$ npm install -g typescript
インストールが完了したか確認するため、以下のコマンドを実行してください。
$ tsc –v
「Version x.x.x」と表示されれば、TypeScriptのコンパイラのインストールは完了です。

4)任意の名前を付けたフォルダを作ります。
TypeScriptはコンパイラによってJavaScriptのコードが得られますが、ES Modules(importやexport文)をまとめる機能が提供されていないため、そのフォルダにES ModulesのJSファイルをまとめるモジュールバンドラー(webpack)を組み合わせた環境構築をします。

コマンドプロンプトやターミナルなどを開き、作ったフォルダで以下を実行してください。
$ npm i -D webpack webpack-cli typescript ts-loader
そのフォルダに、ファイルpackage.jsonとpackage-lock.jsonと、サブフォルダnode_modulesが出来ます。

5)続いて、実行用の「matter」モジュールをインストール
実行ファイルのモジュールとして利用できるように、コマンドオプション「-S」を指定します。
$ npm i -S matter-js @types/matter-js

package.jsonには、以下のように自前のビルドコマンドを追記します。
package.json

{
  "name": "photon_js",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack --mode=development",
    "release": "webpack --mode=production"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "ts-loader": "^9.4.4",
    "typescript": "^5.2.2",
    "webpack": "^5.88.2",
    "webpack-cli": "^5.1.4"
  },
  "dependencies": {
    "@types/matter-js": "^0.19.0",
    "matter-js": "^0.19.0"
  }
}

次にTypeScriptの設定ファイルtsconfig.jsonというテキストファイルを作成します。
tsconfig.json

{
  "compilerOptions": {
    "sourceMap": true,
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "lib": [
      "es2018",
      "dom"
    ]
  }
}

次にwebpackの設定ファイルwebpack.config.jsというjsファイルを作成します。
webpack.config.js

const webpack = require('webpack');
const path    = require('path');

module.exports = {
    entry: {
        bundle: './src/app.ts'
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                // 拡張子 .ts の場合
                test: /\.ts$/,
                // TypeScript をコンパイルする
                use: 'ts-loader'
            }
        ]
    },
    resolve: {
        extensions: ['.js', '.ts'],
        alias: {
        }
    }
}

6)作ったフォルダにサブフォルダdist、srcを作ります。srcにサブフォルダassetsを作ります。

7)下記のファイルを/distに作成します。
index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<script src="../src/assets/Photon-Javascript_SDK.js"></script>
<style type="text/css">
* {margin: 0;padding: 0;}
html, body { width:100%; height:100%; }
.engine {
  position: absolute;
  left: 0;
  width: 700px;
  height: 1000px; }
  .engine canvas {
    width: 100%;
    height: 100%; }
</style>
</head>
<body ontouchmove="event.preventDefault()">
<div id="js-engine" class="engine"></div>
<script src="bundle.js?ver=1.0"></script>
</body>
</html>

├─ dist(サブフォルダ)
│ └─ index.html
├─ node_modules(サブフォルダ:ES Modulesファイル群)
├─ src(サブフォルダ)
│ └─ assets(サブフォルダ)
├─ package.json
├─ package-lock.json
├─ tsconfig.json
└─ webpack.config.js (webpackの設定ファイル)
ここまでの構成です。

6)Photon Client Javascript SDKを「photon」の公式からダウンロードします。
ダウンロードするために、サインアップする必要があります。
SDK->REALTIME->JavaScriptと進みます。
SDKをダウンロードでは、v4.1.1.5ではなく旧バージョン: Photon-Javascript-SDK_v4-1-1-4.zipを選択します。(v4.1.1.5では、なぜか接続できません。この記事のコメント欄で、誰か原因を教えてくれると嬉しいのですが)
ダウンロードしたPhoton-Javascript-SDK_v4-1-1-4.zipを展開してフォルダ「lib」にある
Photon-Javascript_SDK.js、Photon-Javascript_SDK.d.tsをsrc/assets/に入れます。

7)下記のactor.ts、app.ts、client.ts、config.ts、constants.ts、room.tsを/srcに作成します。
actor.ts

import Client from './client';
import { Event } from './constants';
import * as Matter from 'matter-js';

    const container = document.getElementById('js-engine');
    const engine = Matter.Engine.create();
    const render = Matter.Render.create({element: container,engine: engine,options: {wireframes: false,width: 700,height: 1000, background: 'green'}});//,background: 'floor.jpg'画像があればセット
    const world = engine.world;
    let divHeight: number;
    let divWidth: number;
    let collision = false;
    let isTouch : boolean;
    let nxtPosX : number;
    let nxtPosY : number;
    let center_y : number;
    if (window.innerHeight < window.innerWidth * 10 / 7) {
        center_y = window.innerHeight / 2;
    } else {
        center_y = window.innerWidth * 10 / 14;
    }

    const player1 = Matter.Bodies.circle(350, 900, 47,{density:0.9,collisionFilter: { group: -1 },inertia: Infinity});//,render: {sprite: {xScale: 1,yScale: 1,texture: 'player1.png'}}画像があればセット
    const body1 = Matter.Bodies.circle(350, 900, 47,{collisionFilter: { group: -1 },inertia: Infinity,render: {visible: false}});
    const player2 = Matter.Bodies.circle(350, -300, 47,{density:0.9,collisionFilter: { group: -2 },inertia: Infinity});//,render: {sprite: {xScale: 1,yScale: 1,texture: 'player2.png'}}画像があればセット
    const body2 = Matter.Bodies.circle(350, -300, 47,{collisionFilter: { group: -2 },inertia: Infinity,render: {visible: false}});
    const ball = Matter.Bodies.circle(350,500, 35,{density:0.01,friction: 0,frictionAir: 0,restitution: 1,inertia: Infinity,label: 'Pack'});//,render: {sprite: {xScale: 1,yScale: 1,texture: 'Pack.png'}}画像があればセット
    //マルチボディ拘束追加
    const constraint1 = Matter.Constraint.create({
        bodyA: body1,
        bodyB: player1,
        render: {visible: false}
    });
    const constraint2 = Matter.Constraint.create({
        bodyA: body2,
        bodyB: player2,
        render: {visible: false}
    });
    Matter.World.add(world, [constraint1,constraint2]);
    Matter.World.add(world, [constraint1.bodyA,constraint1.bodyB,constraint2.bodyA,constraint2.bodyB,ball]);

    Matter.World.add(world, [
        Matter.Bodies.rectangle(108,-79,216,200,{isStatic:true}),//,render: {visible: false}透明にする
        Matter.Bodies.rectangle(700-108,-79,216,200,{isStatic:true}),//,render: {visible: false}透明にする
        Matter.Bodies.rectangle(-74,500,200,1000,{isStatic:true}),//,render: {visible: false}透明にする
        Matter.Bodies.rectangle(774,500,200,1000,{isStatic:true}),//,render: {visible: false}透明にする
        Matter.Bodies.rectangle(108,1079,216,200,{isStatic:true}),//,render: {visible: false}透明にする
        Matter.Bodies.rectangle(700-108,1079,216,200,{isStatic:true})//,render: {visible: false}透明にする
        ]);

    world.gravity.x = 0;
    world.gravity.y = 0;

    //衝突チェック
    const collisionEvent = (event: Matter.IEventCollision<Matter.Engine>) => {
        const pairs = event.pairs;
        for (let pair of pairs) {if (pair.bodyA.label == 'Pack' || pair.bodyB.label == 'Pack') {collision = true}}
    }

    Matter.Engine.run(engine);
    Matter.Render.run(render);

    const c1 = container;
    window.addEventListener('load', function(){
        let h : number;
        let w : number;
        if (window.innerHeight < window.innerWidth * 10 / 7) {
            h = window.innerHeight;
            w = window.innerHeight * 0.7;
        } else {
            w = window.innerWidth;
            h = window.innerWidth * 10 / 7;
        }
        c1.style.height = h + 'px';
        c1.style.width = w + 'px';
        divHeight = 1000 / h;
        divWidth = 700 / w;
    });

    //タイトル
    const fontsize = Math.round(window.innerHeight / 4);
    const title = document.createElement( 'div' );
    document.body.appendChild( title );
    const info = document.createElement( 'div' );
    info.style.position = 'absolute';
    info.style.left = '10%'; 
    info.style.top = '50%';
    info.style.width = '80%';
    info.style.color = "blue";
    info.style.textShadow = "0px 0px 9px yellow";
    info.style.fontWeight = "bold";
    info.style.fontSize = String(fontsize) + "%";
    container.appendChild( info );
    //スコア1
    const title1 = document.createElement( 'div' );
    document.body.appendChild( title1 );
    const info1 = document.createElement( 'div' );
    info1.style.position = 'absolute';
    info1.style.left = '5%'; 
    info1.style.top = '93%';
    info1.style.width = '10%';
    info1.style.color = "blue";
    info1.style.textShadow = "0px 0px 9px green";
    info1.style.fontWeight = "bold";
    info1.style.fontSize = String(fontsize) + "%";
    container.appendChild( info1 );
    //スコア2
    const title2 = document.createElement( 'div' );
    document.body.appendChild( title2 );
    const info2 = document.createElement( 'div' );
    info2.style.position = 'absolute';
    info2.style.left = '5%'; 
    info2.style.top = '1%';
    info2.style.width = '10%';
    info2.style.color = "blue";
    info2.style.textShadow = "0px 0px 9px red";
    info2.style.fontWeight = "bold";
    info2.style.fontSize = String(fontsize) + "%";
    container.appendChild( info2 );

export default class Actor extends Photon.LoadBalancing.Actor {
    client  : Client;
    name    : string;
    actorNr : number;
    isLocal : boolean;

    private visual : Visual;
    private start = true;
    private score1 = 0;
    private score2 = 0;
    private sadd = true;
    private disp = false;
    private dtimer = 0;
    private stop = false;
    private ballx = 0;
    private bally = 0;
    private ballvx = 0;
    private ballvy = 0;

    constructor(client : Client, name : string, actorNr : number, isLocal : boolean){
        super(name, actorNr, isLocal);
        this.client  = client;
        this.name    = name;
        this.actorNr = actorNr;
        this.isLocal = isLocal;
    }

    getRoom(){
        return super.getRoom();
    }

    hasVisual(){
        return !!(this.visual);
    }

    setVisual(visual : Visual){
        this.visual = visual;
    }

    clearVisual(){
        if(!!this.visual){
            if (this.actorNr == 1) {
                Matter.World.remove(world, constraint1);
            } else {
                Matter.Body.setPosition(constraint2.bodyB,{x:350,y:-300});
            };
        }
    }

    /**
     * クライアントによるアクターの更新
     */
    update(){
        if(this.isLocal){
            this.updateLocal();
        }else{
            this.updateRemote();
        }
    }

    /**
     * 自アクターの更新
     */
    updateLocal(){
        if (this.start) {
            if (this.actorNr == 1) {
                Matter.Events.on(engine, 'collisionStart', collisionEvent);
                //スマホでタッチイベント
                window.addEventListener('touchstart', (e) => {
                    e.preventDefault();
                    nxtPosX = e.changedTouches[0].pageX * divWidth;
                    nxtPosY = (e.changedTouches[0].pageY - 94) * divHeight;
                    if (nxtPosX < 73) {nxtPosX = 73};
                    if (nxtPosX > 627) {nxtPosX = 627};
                    if (nxtPosY < 500) {nxtPosY = 500};
                    if (nxtPosY > 931) {nxtPosY = 931};
                    this.move(nxtPosX, nxtPosY);this.raiseEvent(Event.Move,{0 : [nxtPosX, nxtPosY]});
                    isTouch = true;
                },{passive: false})
                window.addEventListener('touchmove', (e) => {
                    e.preventDefault();
                    if (isTouch) {
                        nxtPosX = e.changedTouches[0].pageX * divWidth;
                        nxtPosY = (e.changedTouches[0].pageY - 94) * divHeight;
                        if (nxtPosX < 73) {nxtPosX = 73};
                        if (nxtPosX > 627) {nxtPosX = 627};
                        if (nxtPosY < 500) {nxtPosY = 500};
                        if (nxtPosY > 931) {nxtPosY = 931};
                        this.move(nxtPosX, nxtPosY);this.raiseEvent(Event.Move,{0 : [nxtPosX, nxtPosY]})
                    }
                },{passive: false})
                window.addEventListener('touchend', (e) => {
                    e.preventDefault();
                    isTouch = false;
                },{passive: false})
                //パソコンでマウスイベント
                window.addEventListener('mousedown', (e) => {
                    e.preventDefault();
                    nxtPosX = e.offsetX * divWidth;
                    nxtPosY = e.offsetY * divHeight;
                    if (nxtPosX < 73) {nxtPosX = 73};
                    if (nxtPosX > 627) {nxtPosX = 627};
                    if (nxtPosY < 500) {nxtPosY = 500};
                    if (nxtPosY > 931) {nxtPosY = 931};
                    this.move(nxtPosX, nxtPosY);this.raiseEvent(Event.Move,{0 : [nxtPosX, nxtPosY]});
                    isTouch = true;
                },false)
                window.addEventListener('mousemove', (e) => {
                    e.preventDefault();
                    if (isTouch) {
                        nxtPosX = e.offsetX * divWidth;
                        nxtPosY = e.offsetY * divHeight;
                        if (nxtPosX < 73) {nxtPosX = 73};
                        if (nxtPosX > 627) {nxtPosX = 627};
                        if (nxtPosY < 500) {nxtPosY = 500};
                        if (nxtPosY > 931) {nxtPosY = 931};
                        this.move(nxtPosX, nxtPosY);this.raiseEvent(Event.Move,{0 : [nxtPosX, nxtPosY]});
                    }
                },false)
                window.addEventListener('mouseup', (e) => {
                    e.preventDefault();
                    isTouch = false;
                },false)
            } else {
                //スマホでタッチイベント
                window.addEventListener('touchstart', (e) => {
                    e.preventDefault();
                    nxtPosX = e.changedTouches[0].pageX * divWidth;
                    nxtPosY = (e.changedTouches[0].pageY - 94) * divHeight;
                    if (nxtPosX < 73) {nxtPosX = 73};
                    if (nxtPosX > 627) {nxtPosX = 627};
                    if (nxtPosY < 500) {nxtPosY = 500};
                    if (nxtPosY > 931) {nxtPosY = 931};
                    this.move(700 - nxtPosX,1000 - nxtPosY);this.raiseEvent(Event.Move,{0 : [700 - nxtPosX,1000 - nxtPosY]})
                    isTouch = true;
                },{passive: false})
                window.addEventListener('touchmove', (e) => {
                    e.preventDefault();
                    if (isTouch) {
                        nxtPosX = e.changedTouches[0].pageX * divWidth;
                        nxtPosY = (e.changedTouches[0].pageY - 94) * divHeight;
                        if (nxtPosX < 73) {nxtPosX = 73};
                        if (nxtPosX > 627) {nxtPosX = 627};
                        if (nxtPosY < 500) {nxtPosY = 500};
                        if (nxtPosY > 931) {nxtPosY = 931};
                        this.move(700 - nxtPosX,1000 - nxtPosY);this.raiseEvent(Event.Move,{0 : [700 - nxtPosX,1000 - nxtPosY]})
                    }
                },{passive: false})
                window.addEventListener('touchend', (e) => {
                    e.preventDefault();
                    isTouch = false;
                },{passive: false})
                //パソコンでマウスイベント
                window.addEventListener('mousedown', (e) => {
                    e.preventDefault();
                    nxtPosX = e.offsetX * divWidth;
                    nxtPosY = e.offsetY * divHeight;
                    if (nxtPosX < 73) {nxtPosX = 73};
                    if (nxtPosX > 627) {nxtPosX = 627};
                    if (nxtPosY < 69) {nxtPosY = 69};
                    if (nxtPosY > 500) {nxtPosY = 500};
                    this.move(nxtPosX, nxtPosY);this.raiseEvent(Event.Move,{0 : [nxtPosX, nxtPosY]})
                    isTouch = true;
                },false)
                window.addEventListener('mousemove', (e) => {
                    e.preventDefault();
                    if (isTouch) {
                        nxtPosX = e.offsetX * divWidth;
                        nxtPosY = e.offsetY * divHeight;
                        if (nxtPosX < 0 || nxtPosX > 700 || nxtPosY < 0 || nxtPosY > 1000) {return}
                        if (nxtPosX < 73) {nxtPosX = 73};
                        if (nxtPosX > 627) {nxtPosX = 627};
                        if (nxtPosY < 69) {nxtPosY = 69};
                        if (nxtPosY > 500) {nxtPosY = 500};
                        this.move(nxtPosX, nxtPosY);this.raiseEvent(Event.Move,{0 : [nxtPosX, nxtPosY]})
                    }
                },false)
                window.addEventListener('mouseup', (e) => {
                    e.preventDefault();
                    isTouch = false;
                },false)
            }
            const playerno = this.client.myRoom().playerCount;
            this.client.myRoom().setCustomProperty("Win",0);
            let text = "";
            if (this.actorNr == 1) {text = "1"} else {
                text = "2";c1.style.transform = "rotate(180deg)";info.style.transform = "rotate(180deg)";
                info1.style.transform = "rotate(180deg)";info2.style.transform = "rotate(180deg)"
            }
            info.innerHTML = "PLAYER" + text + "が入室<br>さきに7ポイントでかち";
            window.setTimeout(function() {info.innerHTML = ' '}, 3000);
            if (this.client.myRoom().playerCount > 2) {
                info.innerHTML = `すでに2名入室しているので接続出来ません`;
                this.stop = true;
                this.client.disconnect();
                Matter.World.remove(world, player1);Matter.World.remove(world, body1);
                Matter.World.remove(world, player2);Matter.World.remove(world, body2);
            }
            if (playerno > 1) {Matter.Body.setPosition(constraint2.bodyB,{x:350,y: 100});ball.collisionFilter = { group: -2 }};
            this.start = false;
        };
        if (this.actorNr == 1) {
            if (collision) {
                this.client.myRoom().setCustomProperty("Pack",ball.position.x + "," + ball.position.y + "," + ball.velocity.x + "," + ball.velocity.y);
                collision = false}
            this.raiseEvent(Event.Move, {0 : [constraint1.bodyB.position.x,constraint1.bodyB.position.y]});
            if (ball.position.y < 0 || ball.position.y > 1000) {
                if (this.sadd) {
                    if (ball.position.y < 0) {this.score1++} else {this.score2++};
                    info1.innerHTML = String(this.score1);info2.innerHTML = String(this.score2);
                    info.innerHTML = "あなた:" + this.score1 + " あいて:" + this.score2;
                    if (this.score1 == 7) {info.innerHTML = "あなたのかち";this.score1 = 0;this.score2 = 0;info1.innerHTML = "0";info2.innerHTML = "0";this.client.myRoom().setCustomProperty("Win",1)}
                    if (this.score2 == 7) {info.innerHTML = "あなたのまけ";this.score1 = 0;this.score2 = 0;info1.innerHTML = "0";info2.innerHTML = "0";this.client.myRoom().setCustomProperty("Win",2)}
                    this.client.myRoom().setCustomProperty("Score",this.score1 + "," + this.score2);
                    window.setTimeout(function() {info.innerHTML = ' '}, 3000);
                    Matter.Body.setPosition(ball,{x:300,y:1200});Matter.Body.setVelocity(ball,{x:0, y:0});
                    this.sadd = false;
                }
                if (this.disp) {
                    this.disp = false;this.sadd = true;
                    Matter.Body.setPosition(ball,{x:350,y:500});Matter.Body.setVelocity(ball,{x:0, y:0});
                    this.client.myRoom().setCustomProperty("Win",0)
                } else {
                    this.dtimer++;
                    if (this.dtimer == 30) {this.disp = true;this.dtimer = 0;}
                }
            }
            if (collision) {
                this.client.myRoom().setCustomProperty("Pack",ball.position.x + "," + ball.position.y + "," + ball.velocity.x + "," + ball.velocity.y);
                collision = false}
        } else {
            if (this.client.myRoom().playerCount == 1) {
                info.style.left = '10%';info.style.top = '20%';info.style.width = '80%';
                info.innerHTML = "PLAYER1が退室したので<br>切断しました";this.client.disconnect();
                if (this.stop) {info.innerHTML = "すでに2名入室しているので<br>接続出来ません"}
            }
            this.raiseEvent(Event.Move, {0 : [constraint2.bodyB.position.x,constraint2.bodyB.position.y]});
            if (ball.position.y < 0 || ball.position.y > 1000) {
                const score = this.client.myRoom().getCustomProperty("Score").split(',');
                info1.innerHTML = score[0];info2.innerHTML = score[1];
                info.innerHTML = "あなた:" + score[1] + " あいて:" + score[0];
                const win = this.client.myRoom().getCustomProperty("Win");
                if (win == 2) {info.innerHTML = "あなたのかち";info1.innerHTML = "0";info2.innerHTML = "0"}
                if (win == 1) {info.innerHTML = "あなたのまけ";info1.innerHTML = "0";info2.innerHTML = "0"}
                window.setTimeout(function() {info.innerHTML = ' '}, 3000);
            }
        }
    }

    /**
     * 他アクターの更新
     */
    updateRemote(){
        if (this.actorNr == 1) {
            const point = this.client.myRoom().getCustomProperty("Pack").split(',');
            if (this.ballx != point[0] || this.bally != point[1] || this.ballvx != point[2] || this.ballvy != point[3]) {
                Matter.Body.setPosition(ball,{x:point[0],y:point[1]});
                Matter.Body.setVelocity(ball,{x:point[2],y:point[3]})
            }
            this.ballx = point[0];this.bally = point[1];this.ballvx = point[2];this.ballvy = point[3];
        } else {
            this.client.myRoom().setCustomProperty("Pack",ball.position.x + "," + ball.position.y + "," + ball.velocity.x + "," + ball.velocity.y)
        }
    }

    /**
     * 移動処理を行う
     * @param nxtPosX 移動後のx座標
     * @param nxtPosY 移動後のy座標
     */
    move(ntPosX : number, ntPosY : number){
        if(this.visual){
            if (this.actorNr == 1) {
                Matter.Body.setPosition(constraint1.bodyB,{x:ntPosX,y:ntPosY})
            } else {
                Matter.Body.setPosition(constraint2.bodyB,{x:ntPosX,y:ntPosY})
            }
        }
    }

    sendPosition(){
        if (this.client.myRoom().playerCount == 1) {this.client.myRoom().setCustomProperty("Pack",350 + "," + 500 + "," + 0 + "," + 0)}
    }
}
export class Visual {
    constructor( actor: Actor) {}
}

app.ts

import CONFIG from './config';
import Client from './client';
const APP_WSS     = CONFIG.photon.Wss;
const APP_ID      = CONFIG.photon.Id;
const APP_VERSION = CONFIG.photon.Version;

window.onload = () => {
    const client = new Client(APP_WSS, APP_ID, APP_VERSION);
    client.start();
}

client.ts

import Room      from './room';
import Actor     from './actor';
import { Visual }    from './actor';

export default class Client extends Photon.LoadBalancing.LoadBalancingClient {
    wss     : boolean;
    id      : string;
    version : string;

    autoConnect : boolean = true;
    masterStart : boolean = false;

    private timerToken : any = null;
    private readonly intervalTime : number = 18;

    constructor(wss : boolean, id  : string, version : string){
        super(wss ? Photon.ConnectionProtocol.Wss : Photon.ConnectionProtocol.Ws, id, version);
        this.wss     = wss;
        this.id      = id;
        this.version = version;

        const addr = this.masterStart ? this.getMasterServerAddress() : this.getNameServerAddress();
        //console.log(`Init addr : ${addr}, id : ${id}, version : ${version}`);

        this.myActor().setName(`Actor_${Math.floor(Math.random() * 10000)}`);
    }

    roomFactory(name : string){
        return new Room(name);
    }

    actorFactory(name : string, actorNr : number, isLocal : boolean){
        return new Actor(this, name, actorNr, isLocal);
    }

    myRoom(){
        return <Room>super.myRoom();
    }

    myActor(){
        return <Actor>super.myActor();
    }

    myRoomActors(){
        return  <{ [index: number]: Actor }>super.myRoomActors();
    }

    onJoinRoom(){
        //console.log(`Room[${this.myRoom().name}]に入室しました`);
        this.setupScene();
    }

    onActorJoin(actor : Actor){
        if (this.myRoomActorCount() < 3) {
            //console.log(`Actor ${actor.actorNr} が入室しました`);
            if(!actor.hasVisual()) actor.setVisual(new Visual(actor));
            // 入室してきたactorのために位置情報を発信
            this.myActor().sendPosition();
        }
    }

    onActorLeave(actor : Actor){
        if(!actor.isLocal){
            actor.clearVisual();
        }
        //console.log(`Actor ${actor.actorNr} が退出しました`);
    }

    onEvent(code : number, content : any, actorNr : number){
        const actor = <Actor>(this.myRoomActors()[actorNr]);
        actor.move(content[0][0], content[0][1]);
    }

    onStateChange = (() => {
        const LBC = Photon.LoadBalancing.LoadBalancingClient;
        return (state) => {
            const stateText =
                state == LBC.State.Joined ?
                    `State: ${LBC.StateToName(state)}, RoomName: ${this.myRoom().name}` :
                    `State: ${LBC.StateToName(state)}`;
            //console.log(stateText);
            switch(state){
                case LBC.State.ConnectedToNameServer:
                    this.getRegions();
                    this.connectToRegionMaster("JP");
                    break;
                case LBC.State.ConnectedToMaster:
                    // this.WebRpc("GetGameList");
                    break;
                case LBC.State.JoinedLobby:
                    if(this.autoConnect){
                        //console.log(`joining random room ...`);
                        this.joinRandomRoom();
                        //console.log(`RoomName: ${this.myRoom().name}`);
                    }
                    break;
                default:
                    break;
            }
        };
    })();

    onOperationResponse(errorCode : number, errorMessage : string, code : number, content : any) {
        if(errorCode){
            switch(code){
                case Photon.LoadBalancing.Constants.OperationCode.JoinRandomGame:
                    switch(errorCode){
                        case Photon.LoadBalancing.Constants.ErrorCode.NoRandomMatchFound:
                            //console.log(`Join Random ${errorCode} : 入室できる部屋が見つからなかったので部屋を作成して入室します.`);
                            this.createRoom();
                            break;
                        default:
                            //console.log(`Join Random ${errorCode}`);
                            break;
                    }
                default:
                    //console.log(`Operarion Response Error ${errorCode}, ${errorMessage}, ${code}, ${content}`);
                    break;
            }
        }
    }

    start(){
        this.setCustomAuthentication("username=" + "yes" + "&token=" + "yes");
        if(this.masterStart){
            this.connect({ keepMasterConnection : true });
        }else{
            this.connectToRegionMaster("JP");
        }

        this.setupScene();

        this.timerToken = setInterval(() => {
            this.update();
        }, this.intervalTime);
    }

    stop(){
        if(this.timerToken != null) clearInterval(this.timerToken);
    }

    update(){
        for(let x in this.myRoomActors()){
            if(x === "-1") break; // 誰もいない
            this.myRoomActors()[x].update();
        }
    }

    private setupScene(){
        for(let aNr in this.myRoomActors()){
            const actor = <Actor>this.myRoomActors()[aNr];
            if(!actor.hasVisual()) actor.setVisual(new Visual(actor));
        }
    }

}

config.ts

export default {
    photon : {
        // 各自で設定
        Wss     : true, 
        Id      : '<AppId>',
        Version : '<Version>'
    }
}

constants.ts

export const Event = {Move : 1}

room.ts

export default class Room extends Photon.LoadBalancing.Room {
    constructor(name : string){
        super(name);

        this.setEmptyRoomLiveTime(10000);
        this.setSuspendedPlayerLiveTime(10000);
    }
}

## ビルド
コマンドプロンプトやターミナルなどを開き、作ったフォルダで以下を実行してください。
$ npm run build か、$ npm run release を実行

## 実行
dist/index.htmlを開く

最終的な構成は以下のようになります。
├─ dist
│ ├─ index.html
│ └─ bundle.js
├─ node_modules(ES Modulesファイル群)
├─ src
│ ├─ assets
│ │ ├─ Photon-Javascript_SDK.d.ts
│ │ └─ Photon-Javascript_SDK.js
│ ├─ actor.ts
│ ├─ app.ts
│ ├─ client.ts
│ ├─ config.ts
│ ├─ constants.ts
│ └─ room.ts
├─ package.json
├─ package-lock.json
├─ tsconfig.json
└─ webpack.config.js (webpackの設定ファイル)

https://github.com/ttak0422/photon_jsを参考にしました。

コメント