一緒にお絵描きSource

”一緒にお絵描き”をホームページで自由に使いたいとの要望がありましたので、作り方を載せることにしました。少しでも皆様の役に立てるかもと思い公開することにしました。あと半年で後期高齢者になってしまうにですが、これからもゲームを作り続けるつもりです。

作成

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 オプションを付けるとグローバル (システム全体で利用可能) インストールができます。
$ 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)を組み合わせた環境構築をします。

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

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

{
  "name": "photon_js",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack --mode=development",
    "release": "webpack --mode=production"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/ttak0422/photon_js.git"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/ttak0422/photon_js/issues"
  },
  "homepage": "https://github.com/ttak0422/photon_js#readme",
  "devDependencies": {
    "ts-loader": "^9.5.2",
    "typescript": "^5.8.3",
    "webpack": "^5.100.2",
    "webpack-cli": "^6.0.1"
  }
}

7)TypeScriptの設定ファイルtsconfig.jsonを作成します。メモ帳で作る時はUTF-8で保存して下さい
tsconfig.json

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

8)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: {
        }
    }
}

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

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

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<script src="../src/assets/photon.js"></script>
<style type="text/css">
  * {margin: 0;padding: 0;}
  html, body { width:100%; height:100%;}
  .guideimg {
    position: absolute;
    top: 90%;
    left: 0%;
    width: 100%;
    height: 10%;
    background-image: url('palet10.png');
    background-size: 100% 100%;}
    .guideimg canvas {
      width: 100%;
      height: 100%;}
</style>
</head>
<body ontouchmove="event.preventDefault()">
<canvas id="draw-area" style="border: 1px solid #000000; position: absolute;"></canvas>
<canvas id="draw-area2" style="border: 1px solid #000000;"></canvas>
<div id="js-guideimg" class="guideimg"></div>
<script src="bundle.js?ver=1.0"></script>
</body>
</html>

11)/distに画像palet10.png~palet28.pngを保存します

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

12)Photon Client Javascript SDKを「photon」の公式からダウンロードします。
ダウンロードするために、サインアップする必要があります。
SDK->REALTIME->JavaScriptと進みます。
ダウンロードしたphoton-javascript-sdk_v4-3-2-0.zipを展開してフォルダ「lib」にある
photon.js、photon.d.tsをsrc/assets/に入れます。

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

import Client    from './client';
import { Event } from './constants';

let scale : number;
let wh : number;
let ww : number;
let rate : number;
let colorno = 0;
const Color = ['#800000','#000000','#C0C0C0','#808080','#FF0000','#98514B','#FFFF00','#808000','#00FF00','#008000','#00FFFF','#008080','#0000FF','#000080','#FF00FF','#800080','#FEDCBD','#e6b422'];
let guide = document.getElementById('js-guideimg');
if (window.innerHeight < window.innerWidth) {
    ww = window.innerHeight / 1.29;
    wh = window.innerHeight;
    guide.style.left = '50%';
    guide.style.width = '50%';
    rate = 0.5;
} else {
    ww = window.innerWidth;
    wh = window.innerWidth * 1.29;
    guide.style.width = '100%';
    rate = 1;
}
scale = 1 / ww;

const canvas = <HTMLCanvasElement> document.getElementById('draw-area');
canvas.width = ww;
canvas.height = wh;
const context = canvas.getContext('2d');
context.lineCap = 'round';
context.lineJoin = 'round';
context.lineWidth = 6;

const canvas2 = <HTMLCanvasElement> document.getElementById('draw-area2');
canvas2.width = ww;
canvas2.height = wh;
const context2 = canvas2.getContext('2d');
context2.lineCap = 'round';
context2.lineJoin = 'round';
context2.lineWidth = 6;

//タイトル
let fontsize = Math.round(window.innerHeight / 4);
const title = document.createElement('div');
document.body.appendChild(title);
var info = document.createElement('div');
info.style.position = 'absolute';
info.style.left = '10%'; 
info.style.top = '40%';
info.style.width = '90%';
info.style.color = "blue";
info.style.fontWeight = "bold";
info.style.fontSize = String(fontsize) + "%";
title.appendChild( info );

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

    private visual : Visual;
    private start = true;
    private stop = false;
    private x : number;
    private y : number;
    private isDrawing = 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) {
                this.clear(context);this.client.myRoom().setCustomProperty("cclear",true);
            } else {
                this.clear(context2);this.client.myRoom().setCustomProperty("cclear2",true);
            };
        }
    }

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

    /**
     * 自アクターの更新
     */
    updateLocal(){
        if (this.start) {
            if (this.actorNr == 1) {
                guide.addEventListener('touchstart', (e) => {
                    e.preventDefault();
                    if (e.changedTouches[0].pageX < window.innerWidth / 10) {
                        colorno = 0;guide.style.backgroundImage = 'url(palet10.png)';return
                    }else if (e.changedTouches[0].pageX < window.innerWidth * 2 / 10) {
                        colorno = 2;guide.style.backgroundImage = 'url(palet11.png)';return
                    } else if (e.changedTouches[0].pageX < window.innerWidth * 3 / 10) {
                        colorno = 4;guide.style.backgroundImage = 'url(palet12.png)';return
                    } else if (e.changedTouches[0].pageX < window.innerWidth * 4 / 10) {
                        colorno = 6;guide.style.backgroundImage = 'url(palet13.png)';return
                    } else if (e.changedTouches[0].pageX < window.innerWidth * 5 / 10) {
                        colorno = 8;guide.style.backgroundImage = 'url(palet14.png)';return
                    } else if (e.changedTouches[0].pageX < window.innerWidth * 6 / 10) {
                        colorno = 10;guide.style.backgroundImage = 'url(palet15.png)';return
                    } else if (e.changedTouches[0].pageX < window.innerWidth * 7 / 10) {
                        colorno = 12;guide.style.backgroundImage = 'url(palet16.png)';return
                    } else if (e.changedTouches[0].pageX < window.innerWidth * 8 / 10) {
                        colorno = 14;guide.style.backgroundImage = 'url(palet17.png)';return
                    } else if (e.changedTouches[0].pageX < window.innerWidth * 9 / 10) {
                        colorno = 16;guide.style.backgroundImage = 'url(palet18.png)';return
                    } else {
                        this.clear(context);this.client.myRoom().setCustomProperty("cclear",true);
                    }
                },{passive: false})
                canvas.addEventListener('touchstart', (e) => {
                    e.preventDefault();
                    this.x = e.changedTouches[0].pageX;
                    this.y = e.changedTouches[0].pageY;
                    this.isDrawing = 1;
                },{passive: false})
                canvas.addEventListener('touchmove', (e) => {
                    e.preventDefault();
                    if (this.isDrawing == 1) {
                        this.drawLine(context, this.x, this.y, e.changedTouches[0].pageX, e.changedTouches[0].pageY, colorno);
                        this.raiseEvent(Event.Move,{0 : [this.x * scale, this.y * scale, e.changedTouches[0].pageX * scale, e.changedTouches[0].pageY * scale, colorno]});
                        this.x = e.changedTouches[0].pageX;
                        this.y = e.changedTouches[0].pageY;
                    }
                },{passive: false})
                canvas.addEventListener('touchend', (e) => {
                    e.preventDefault();
                    if (this.isDrawing == 1) {
                        this.x = 0;
                        this.y = 0;
                        this.isDrawing = 0;
                    }
                },{passive: false})
                guide.addEventListener('mousedown', e => {
                    if (e.offsetX < window.innerWidth * rate / 10) {
                        colorno = 0;guide.style.backgroundImage = 'url(palet10.png)';return
                    }else if (e.offsetX < window.innerWidth * rate * 2 / 10) {
                        colorno = 2;guide.style.backgroundImage = 'url(palet11.png)';return
                    } else if (e.offsetX < window.innerWidth * rate * 3 / 10) {
                        colorno = 4;guide.style.backgroundImage = 'url(palet12.png)';return
                    } else if (e.offsetX < window.innerWidth * rate * 4 / 10) {
                        colorno = 6;guide.style.backgroundImage = 'url(palet13.png)';return
                    } else if (e.offsetX < window.innerWidth * rate * 5 / 10) {
                        colorno = 8;guide.style.backgroundImage = 'url(palet14.png)';return
                    } else if (e.offsetX < window.innerWidth * rate * 6 / 10) {
                        colorno = 10;guide.style.backgroundImage = 'url(palet15.png)';return
                    } else if (e.offsetX < window.innerWidth * rate * 7 / 10) {
                        colorno = 12;guide.style.backgroundImage = 'url(palet16.png)';return
                    } else if (e.offsetX < window.innerWidth * rate * 8 / 10) {
                        colorno = 14;guide.style.backgroundImage = 'url(palet17.png)';return
                    } else if (e.offsetX < window.innerWidth * rate * 9 / 10) {
                        colorno = 16;guide.style.backgroundImage = 'url(palet18.png)';return
                    } else {
                        this.clear(context);this.client.myRoom().setCustomProperty("cclear",true);
                    }
                });
                canvas.addEventListener('mousedown', e => {
                    this.x = e.offsetX;
                    this.y = e.offsetY;
                    this.isDrawing = 1;
                });
                canvas.addEventListener('mousemove', e => {
                    if (this.isDrawing == 1) {
                      this.drawLine(context, this.x, this.y, e.offsetX, e.offsetY, colorno);
                      this.raiseEvent(Event.Move,{0 : [this.x * scale, this.y * scale, e.offsetX * scale, e.offsetY * scale, colorno]});
                      this.x = e.offsetX;
                      this.y = e.offsetY;
                    }
                });  
                canvas.addEventListener('mouseup', e => {
                    if (this.isDrawing == 1) {
                      this.x = 0;
                      this.y = 0;
                      this.isDrawing = 0;
                    }
                });
            } else {
                guide.addEventListener('touchstart', (e) => {
                    e.preventDefault();
                    if (e.changedTouches[0].pageX < window.innerWidth / 10) {
                        colorno = 1;guide.style.backgroundImage = 'url(palet20.png)';return
                    }else if (e.changedTouches[0].pageX < window.innerWidth * 2 / 10) {
                        colorno = 3;guide.style.backgroundImage = 'url(palet21.png)';return
                    } else if (e.changedTouches[0].pageX < window.innerWidth * 3 / 10) {
                        colorno = 5;guide.style.backgroundImage = 'url(palet22.png)';return
                    } else if (e.changedTouches[0].pageX < window.innerWidth * 4 / 10) {
                        colorno = 7;guide.style.backgroundImage = 'url(palet23.png)';return
                    } else if (e.changedTouches[0].pageX < window.innerWidth * 5 / 10) {
                        colorno = 9;guide.style.backgroundImage = 'url(palet24.png)';return
                    } else if (e.changedTouches[0].pageX < window.innerWidth * 6 / 10) {
                        colorno = 11;guide.style.backgroundImage = 'url(palet25.png)';return
                    } else if (e.changedTouches[0].pageX < window.innerWidth * 7 / 10) {
                        colorno = 13;guide.style.backgroundImage = 'url(palet26.png)';return
                    } else if (e.changedTouches[0].pageX < window.innerWidth * 8 / 10) {
                        colorno = 15;guide.style.backgroundImage = 'url(palet27.png)';return
                    } else if (e.changedTouches[0].pageX < window.innerWidth * 9 / 10) {
                        colorno = 17;guide.style.backgroundImage = 'url(palet28.png)';return
                    } else {
                        this.clear(context2);this.client.myRoom().setCustomProperty("cclear2",true);
                    }
                },{passive: false})
                canvas.addEventListener('touchstart', (e) => {
                    e.preventDefault();
                    this.x = e.changedTouches[0].pageX;
                    this.y = e.changedTouches[0].pageY;
                    this.isDrawing = 1;
                },{passive: false})
                canvas.addEventListener('touchmove', (e) => {
                    e.preventDefault();
                    if (this.isDrawing == 1) {
                        this.drawLine(context2, this.x, this.y, e.changedTouches[0].pageX, e.changedTouches[0].pageY, colorno);
                        this.raiseEvent(Event.Move,{0 : [this.x * scale, this.y * scale, e.changedTouches[0].pageX * scale, e.changedTouches[0].pageY * scale, colorno]});
                        this.x = e.changedTouches[0].pageX;
                        this.y = e.changedTouches[0].pageY;
                    }
                },{passive: false})
                canvas.addEventListener('touchend', (e) => {
                    e.preventDefault();
                    if (this.isDrawing == 1) {
                        this.x = 0;
                        this.y = 0;
                        this.isDrawing = 0;
                    }
                },{passive: false})
                guide.addEventListener('mousedown', e => {
                    if (e.offsetX < window.innerWidth * rate / 10) {
                        colorno = 1;guide.style.backgroundImage = 'url(palet20.png)';return
                    }else if (e.offsetX < window.innerWidth * rate * 2 / 10) {
                        colorno = 3;guide.style.backgroundImage = 'url(palet21.png)';return
                    } else if (e.offsetX < window.innerWidth * rate * 3 / 10) {
                        colorno = 5;guide.style.backgroundImage = 'url(palet22.png)';return
                    } else if (e.offsetX < window.innerWidth * rate * 4 / 10) {
                        colorno = 7;guide.style.backgroundImage = 'url(palet23.png)';return
                    } else if (e.offsetX < window.innerWidth * rate * 5 / 10) {
                        colorno = 9;guide.style.backgroundImage = 'url(palet24.png)';return
                    } else if (e.offsetX < window.innerWidth * rate * 6 / 10) {
                        colorno = 11;guide.style.backgroundImage = 'url(palet25.png)';return
                    } else if (e.offsetX < window.innerWidth * rate * 7 / 10) {
                        colorno = 13;guide.style.backgroundImage = 'url(palet26.png)';return
                    } else if (e.offsetX < window.innerWidth * rate * 8 / 10) {
                        colorno = 15;guide.style.backgroundImage = 'url(palet27.png)';return
                    } else if (e.offsetX < window.innerWidth * rate * 9 / 10) {
                        colorno = 17;guide.style.backgroundImage = 'url(palet28.png)';return
                    } else {
                        this.clear(context2);this.client.myRoom().setCustomProperty("cclear2",true);
                    }
                });
                canvas.addEventListener('mousedown', e => {
                    this.x = e.offsetX;
                    this.y = e.offsetY;
                    this.isDrawing = 1;
                });
                canvas.addEventListener('mousemove', e => {
                    if (this.isDrawing == 1) {
                      this.drawLine(context2, this.x, this.y, e.offsetX, e.offsetY, colorno);
                      this.raiseEvent(Event.Move,{0 : [this.x * scale, this.y * scale, e.offsetX * scale, e.offsetY * scale, colorno]});
                      this.x = e.offsetX;
                      this.y = e.offsetY;
                    }
                });  
                canvas.addEventListener('mouseup', e => {
                    if (this.isDrawing == 1) {
                      this.x = 0;
                      this.y = 0;
                      this.isDrawing = 0;
                    }
                });
            }
            let text = "";
            if (this.actorNr == 1) {text = "1"} else {text = "2";guide.style.backgroundImage = 'url(palet20.png)';colorno = 1}
            info.innerHTML = "PLAYER" + text + "が入室<br>LINEなどでお話しながら<br>お絵描きをお楽しみください";
            window.setTimeout(function() {info.innerHTML = ' ';}, 4000);
            if (this.client.myRoom().playerCount > 2) {
                info.innerHTML = `すでに2名入室しているので接続出来ません`;
                this.stop = true;
                this.client.disconnect();
            }
            this.start = false;
        }
        if (this.actorNr == 1) {
            if (this.client.myRoom().getCustomProperty("cclear2")) {
                this.clear(context2);
                this.client.myRoom().setCustomProperty("cclear2",false);
            }
        }else {
            if (this.client.myRoom().playerCount == 1) {
                info.innerHTML = "PLAYER1が退室したので<br>切断しました";this.client.disconnect();
                if (this.stop) {info.innerHTML = "すでに2名入室しているので<br>接続出来ません";}
            }
            if (this.client.myRoom().getCustomProperty("cclear")) {
                this.clear(context);
                this.client.myRoom().setCustomProperty("cclear",false);
            }
        }
    }

    /**
     * 他アクターの更新
     */
    updateRemote(){
    }

    drawLine(context : any, x1 : number, y1 : number, x2 : number, y2 : number,num : number) {
        context.beginPath();
        context.strokeStyle = Color[num];
        context.moveTo(x1, y1);
        context.lineTo(x2, y2);
        context.stroke();
        context.closePath();
    }

    clear(context : any) {context.clearRect(0, 0, canvas.width, canvas.height)}

    /*
     * @param ntPosX 移動後のx座標
     * @param ntPosY 移動後のy座標
     */
    move(posX : number, posY : number, offX : number, offY : number,cnum : number){
        if(this.visual){
            if (this.actorNr == 1) {
                this.drawLine(context, posX * ww, posY * ww, offX * ww, offY * ww, cnum);
            } else {
                this.drawLine(context2, posX * ww, posY * ww, offX * ww, offY * ww, cnum);
            }
        }
    }

    sendPosition(){
        this.client.myRoom().setCustomProperty("cclear",false);
        this.client.myRoom().setCustomProperty("cclear2",false);
    }
}
export class Visual {
    constructor( actor: Actor) {}
}

14)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();
}

15)client.ts

/// <reference path="./assets/photon.d.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 = true;
    masterStart = false;
    private timerToken: any = null;
    private readonly intervalTime = 16;

   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}`);
    }

    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));
            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 = this.myRoomActors()[actorNr];
        if (actor) {
            actor.move(content[0][0], content[0][1], content[0][2], content[0][3], content[0][4]);
        }
    }

    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;
                    }
                    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(){
        const actors = this.myRoomActors();
        for(let x in actors){
            if (x === "-1") break;

            actors[x].update();
        }
    }

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

16)config.ts

export default {
    photon : {
        // 各自で設定
        Wss     : true, 
        Id      : 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
        Version : '1.0'
    }
}

17)constants.ts

export const Event = {
    Move : 1
}

18)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
│ ├─ palet10.png~palet28.png
│ └─ bundle.js
├─ node_modules(ES Modulesファイル群)
├─ src
│ ├─ assets
│ │ ├─ photon.js
│ │ └─ photon.d.ts
│ ├─ actor.ts
│ ├─ app.ts
│ ├─ client.ts
│ ├─ config.ts
│ ├─ constants.ts
│ └─ room.ts
├─ package.json
├─ package-lock.json
├─ tsconfig.json
└─ webpack.config.js (webpackの設定ファイル)