ASP.NET Core版 対戦型ラリーエックスをつくる(3)の続きですクライアントサイドにおける処理を考えます。
cshtmlファイル
ここではPages\RallyXフォルダ内にgame.cshtmlファイルを作成します。そしてgame.cshtmlには以下のように記述します。
Pages\RallyX\game.cshtml
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | @page @{     ViewData["Title"] = "対戦型ラリーエックス";     Layout = "_Layout_none";     string baseurl = Global.BaseUrl; }     <div class = "display-none">         <img src = "@baseurl/rally-x/blue-car-n.png" id ="player1n">         <img src = "@baseurl/rally-x/red-car-n.png" id ="player2n">         <img src = "@baseurl/rally-x/green-car-n.png" id ="player3n">         <img src = "@baseurl/rally-x/blue-car-e.png" id ="player1e">         <img src = "@baseurl/rally-x/red-car-e.png" id ="player2e">         <img src = "@baseurl/rally-x/green-car-e.png" id ="player3e">         <img src = "@baseurl/rally-x/blue-car-s.png" id ="player1s">         <img src = "@baseurl/rally-x/red-car-s.png" id ="player2s">         <img src = "@baseurl/rally-x/green-car-s.png" id ="player3s">         <img src = "@baseurl/rally-x/blue-car-w.png" id ="player1w">         <img src = "@baseurl/rally-x/red-car-w.png" id ="player2w">         <img src = "@baseurl/rally-x/green-car-w.png" id ="player3w">         <img src = "@baseurl/rally-x/wall.png" id ="wall">         <img src = "@baseurl/rally-x/flag.png" id ="flag">         <img src = "@baseurl/rally-x/blue-smoke.png" id ="smoke1">         <img src = "@baseurl/rally-x/red-smoke.png" id ="smoke2">         <img src = "@baseurl/rally-x/green-smoke.png" id ="smoke3">         <img src = "@baseurl/rally-x/spark1.png" id ="spark1">         <img src = "@baseurl/rally-x/spark2.png" id ="spark2">         <img src = "@baseurl/rally-x/spark3.png" id ="spark3">         <img src = "@baseurl/rally-x/spark4.png" id ="spark4">         <img src = "@baseurl/rally-x/spark5.png" id ="spark5">         <img src = "@baseurl/rally-x/spark6.png" id ="spark6">     </div> <div style="position: relative; overflow: hidden; margin-left:20px;margin-top:20px;float:left">     <canvas id="can"></canvas>     <br>     <p>鳩でもわかるラリーエックスの遊び方</p>     <p>移動:↑→↓←キー<br>     煙幕:SPACEキー<br>     「音を出す」にチェックをいれてもゲームに参加していない場合は音はでません。</p>     <input type="checkbox" value="音を出す" id="sound-checkbox">音を出す     <label>ハンドルネーム</label>     <input type="text" id="player-name" maxlength='16' /><br>     <input type="button" id="startButton1" value="ゲームスタート" onclick="GameStart()" style="margin-top:15px;margin-bottom:15px;">     <p id = "conect-result"></p>     <p id = "pos-result"></p> </div> <div style="margin-left:0px;margin-top:20px;float:left;width:300px;">     <div id = "gameStatus"></div>     <input type="button" id="startButton2" value="ゲームスタート" onclick="GameStart()" style="margin-top:15px;margin-bottom:15px;"><br>     <input type="button" id="rankingButton"onclick="location.href='@baseurl/RallyX/hi-score'" value="上位30位をチェック"><br>     <div id = "how-to-play"></div>     <div id = "score"></div>     <div id = "rest"></div>     <div id = "information"></div>     <div class = "player-info">         <div><img src = "@baseurl/rally-x/blue-car.png" width="32" height="32"></div>         <div id = "playerName1" class = "player-info-text"></div>     </div>     <div class = "player-info">         <div><img src = "@baseurl/rally-x/red-car.png" width="32" height="32"></div>         <div id = "playerName2" class = "player-info-text"></div>     </div>     <div class = "player-info">         <div><img src = "@baseurl/rally-x/green-car.png" width="32" height="32"></div>         <div id = "playerName3" class = "player-info-text"></div>     </div>     <div class = "player-info">         <div><img src = "@baseurl/rally-x/blue-car.png" width="32" height="32"></div>         <div id = "playerName4" class = "player-info-text"></div>     </div>     <div class = "player-info">         <div><img src = "@baseurl/rally-x/red-car.png" width="32" height="32"></div>         <div id = "playerName5" class = "player-info-text"></div>     </div>     <div class = "player-info">         <div><img src = "@baseurl/rally-x/green-car.png" width="32" height="32"></div>         <div id = "playerName6" class = "player-info-text"></div>     </div>     <div class = "player-info">         <div><img src = "@baseurl/rally-x/blue-car.png" width="32" height="32"></div>         <div id = "playerName7" class = "player-info-text"></div>     </div>     <div class = "player-info">         <div><img src = "@baseurl/rally-x/red-car.png" width="32" height="32"></div>         <div id = "playerName8" class = "player-info-text"></div>     </div>     <div class = "player-info">         <div><img src = "@baseurl/rally-x/green-car.png" width="32" height="32"></div>         <div id = "playerName9" class = "player-info-text"></div>     </div> </div> <script>     let base_url = '@baseurl'; </script> <script src="@baseurl/rally-x/app.js"></script> | 
またwwwroot\rally-xフォルダ内には以下のファイルを置きます。
blue-car-e.png 
blue-car-n.png 
blue-car-s.png 
blue-car-w.png 
blue-car.png 
red-car-e.png 
red-car-n.png 
red-car-s.png 
red-car-w.png 
red-car.png 
green-car-e.png 
green-car-n.png 
green-car-s.png 
green-car-w.png 
green-car.png 
blue-smoke.png 
red-smoke.png 
green-smoke.png 
flag.png 
wall.png 
spark1.png 
spark2.png 
spark3.png 
spark4.png 
spark5.png 
spark6.png 
それから音声ファイルとして以下を準備しました。
bgm.mp3
flag.mp3
smoke.mp3
dead.mp3
explotion.mp3
gameover.mp3
notification.mp3
JavaScript部分
次にJavaScriptですが、wwwroot\rally-xフォルダ内にapp.jsを作成します。
グローバル変数
wwwroot\rally-x\app.js
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | // Canvasのサイズ const CANVAS_WIDTH = 540; const CANVAS_HEIGHT = 360; let can; let ctx; let connection = new signalR.HubConnectionBuilder().withUrl(base_url + "/RallyXHub").build(); let connectionID = ''; // プレイヤーのイメージ let imgPlayer1n = document.getElementById('player1n'); let imgPlayer2n = document.getElementById('player2n'); let imgPlayer3n = document.getElementById('player3n'); let imgPlayer1e = document.getElementById('player1e'); let imgPlayer2e = document.getElementById('player2e'); let imgPlayer3e = document.getElementById('player3e'); let imgPlayer1s = document.getElementById('player1s'); let imgPlayer2s = document.getElementById('player2s'); let imgPlayer3s = document.getElementById('player3s'); let imgPlayer1w = document.getElementById('player1w'); let imgPlayer2w = document.getElementById('player2w'); let imgPlayer3w = document.getElementById('player3w'); // 煙幕のイメージ let imgSmoke1 = document.getElementById('smoke1'); let imgSmoke2 = document.getElementById('smoke2'); let imgSmoke3 = document.getElementById('smoke3'); // 壁、フラッグのイメージ let imgWall = document.getElementById('wall'); let imgFlag = document.getElementById('flag'); // 火花のイメージ let imgSpark1 = document.getElementById('spark1'); let imgSpark2 = document.getElementById('spark2'); let imgSpark3 = document.getElementById('spark3'); let imgSpark4 = document.getElementById('spark4'); let imgSpark5 = document.getElementById('spark5'); let imgSpark6 = document.getElementById('spark6'); // BGMと効果音 let bgm = new Audio(base_url + '/rally-x/bgm.mp3'); let flagSound = new Audio(base_url + '/rally-x/flag.mp3'); let smokeSound = new Audio(base_url + '/rally-x/smoke.mp3'); let explodeSound = new Audio(base_url + '/rally-x/explotion.mp3'); let deadSound = new Audio(base_url + '/rally-x/dead.mp3'); let gameoverSound = new Audio(base_url + '/rally-x/gameover.mp3'); let notificationSound = new Audio(base_url + '/rally-x/notification.mp3'); // BGMは再生中か? let isPlayingBgm = false; // 現在プレイ中か? let isPlaying = false; // キーは押されているか? let isUpKeyDown = false; let isDownKeyDown = false; let isLeftKeyDown = false; let isRightKeyDown = false; // 壁のX座標とY座標 let wallXs = []; let wallYs = []; // 自機の状態 let playerName; let playerX = undefined; let playerY = undefined; let playerFuel = 0; let isPlayerDead; let playerScore; let playerRest; let invincibleTime; // 更新処理用の一時的な変数 let playersNumber = []; let playersName = []; let playersX = []; let playersY = []; let playersDirect = []; let playersFuel = []; let isPlayersDead = []; let scores = []; let rests = []; let invincibleTimes = []; let spinTimes = []; let flagsX = []; let flagsY = []; let smokeXs = []; let smokeYs = []; let smokeNumbers = []; let sparksX = []; let sparksY = []; let sparksLife = []; // 自機を中央に描画するためにXY方向にどれだけズラすか? let shiftX = 0; let shiftY = 0; // 更新回数 let updateCount = 0; // 現在、ステージとステージのあいだの状態か? let isTimeBetweenStages = false; | 
初期化の処理
ページが読み込まれたときの処理を示します。ここではcanvasの初期化、ゲームの説明を表示させるための処理とBGMをエンドレスで再生するための処理、プレイヤーのスコアや名前を表示する部分の初期化などをおこなっています。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | window.addEventListener('load', Init); function Init() {     let elms = document.getElementsByClassName('player-info');     for (let i = 0; i < elms.length; i++) {         elms[i].style.marginTop = '5px';         elms[i].style.display = 'flex';     }     elms = document.getElementsByClassName('player-info-text');     for (let i = 0; i < elms.length; i++) {         elms[i].style.marginTop = '5px';         elms[i].style.marginLeft = '10px';         elms[i].style.color = 'white'         elms[i].style.fontWeight = 'bold';     }     can = document.getElementById('can');     can.width = CANVAS_WIDTH;     can.height = CANVAS_HEIGHT;     ctx = can.getContext('2d');     document.getElementById('sound-checkbox').checked = true;     SetVolume(0.04);     InitBGM();     connection.start().catch(function (err) {         document.getElementById("conect-result").innerHTML = '接続失敗';     });     let elm = document.getElementById('how-to-play');     elm.innerHTML = ` <p style = "color: #ffff00; font-weight:bold;">はじめに読んでね。</p> <p style = "color: #ffff00; font-weight:bold; font-size:1.1em;">鳩でもわかるラリーエックスの遊び方</p> <p>移動:↑→↓←キー<br> 煙幕:SPACEキー</p> <p>あなたの車が<span style = "color: #00ffff; font-weight:bold;"> Blue Car </span>ならRed Carからは逃げて下さい。Green Carなら体当たりして破壊できます。</p> <p>あなたの車が<span style = "color: #ff0000; font-weight:bold;"> Red Car </span>ならGreen Carからは逃げて下さい。Blue Carなら体当たりして破壊できます。</p> <p>あなたの車が<span style = "color: #00ff00; font-weight:bold;"> Green Car </span>ならBlue Carからは逃げて下さい。Red Carなら体当たりして破壊できます。</p> <p>追尾を振り切るために煙幕を使用することができます。<br> 煙幕に突っ込んでしまうと1秒間車がスピンして操縦不可になります。</p> <p>ただし<span style = "color: #ff0000; font-weight:bold;"> 煙幕は自分の天敵にしか効果はない </span>です。 たとえばBlue Carが生成した煙幕はRed Carにしか効果はなく、 Blue CarとGreen Carは通り抜けることができてしまいます。</p> `; } function InitBGM() {     bgm.addEventListener("ended", function () {         isPlayingBgm = false;     }, false); } function SetVolume(volume) {     explodeSound.volume = volume;     deadSound.volume = volume;     gameoverSound.volume = volume;     notificationSound.volume = volume;     bgm.volume = volume;     flagSound.volume = volume;     // smokeSoundはやや音が小さいのでボリュームを2倍にセットする     if (volume < 0.25)         smokeSound.volume = volume * 4; } | 
接続成功時の処理
接続に成功したらIDを表示させます。また壁と煙幕の座標が送られてくるのでそれを配列に格納しておきます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | connection.on("SuccessfulConnectionToClient", function (result, id) {     connectionID = id;     document.getElementById("conect-result").innerHTML = `conect-result ${result}:${id}`; }); connection.on("SendWallsToClient", function (xs, ys, width, height) {     wallXs = xs.split(',');     wallYs = ys.split(',');     fieldWidth = width;     fieldHeight = height; }); connection.on("SendSmokesToClient", function (smokesX, smokesY, smokesNumber) {     if (smokesX != '') {         smokeXs = smokesX.split(',');         smokeYs = smokesY.split(',');         smokeNumbers = smokesNumber.split(',');     }     else {         smokeXs = [];         smokeYs = [];         smokeNumbers = [];     } }); | 
ゲーム開始の処理
| 1 2 3 4 5 6 7 8 9 | function GameStart() {     if (connectionID != '') {         document.getElementById('how-to-play').style.display = 'none';         let playerName = document.getElementById('player-name').value;         connection.invoke("GameStart", connectionID, playerName).catch(function (err) {             return console.error(err.toString());         });     } } | 
キー操作
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | document.onkeydown = function (e) {     // ゲーム開始以降はデフォルトの動作を抑制する     if (isPlaying && (e.key == "ArrowUp" || e.key == "ArrowDown" || e.key == "ArrowLeft" || e.key == "ArrowRight" || e.key == " "))         e.preventDefault();     // キーが押しっぱなしになっているときの二重送信を防ぐ     if (e.key == "ArrowUp" && isUpKeyDown)         return;     if (e.key == "ArrowDown" && isDownKeyDown)         return;     if (e.key == "ArrowLeft" && isLeftKeyDown)         return;     if (e.key == "ArrowRight" && isRightKeyDown)         return;     if (e.key == "ArrowUp")         isUpKeyDown = true;     if (e.key == "ArrowDown")         isDownKeyDown = true;     if (e.key == "ArrowLeft")         isLeftKeyDown = true;     if (e.key == "ArrowRight")         isRightKeyDown = true;     // ゲーム中にプレイヤー名を変更可能なのでキーが押されたらそのつどプレイヤー名も送信する     // 接続されていない場合はなにもしない     if (connectionID != '') {         let playerName = document.getElementById('player-name').value;         connection.invoke("DownKey", e.key, playerName).catch(function (err) {             return console.error(err.toString());         });     } } document.onkeyup = function (e) {     if (e.key == "ArrowUp")         isUpKeyDown = false;     if (e.key == "ArrowDown")         isDownKeyDown = false;     if (e.key == "ArrowLeft")         isLeftKeyDown = false;     if (e.key == "ArrowRight")         isRightKeyDown = false;     // 接続されていない場合はなにもしない     if (connectionID != '') {         connection.invoke("UpKey", e.key).catch(function (err) {             return console.error(err.toString());         });     } } | 
更新処理
更新処理がおこなわれるときはサーバーサイドからStartUpdateToClientが送られてきます。そのあと更新処理に必要なデータが送られてくるので配列に一時的に格納しておきます。最後にEndUpdateToClientが送られてくるので描画処理をおこないます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | connection.on("StartUpdateToClient", function () {     isPlaying = false;     playersNumber = [];     playersName = [];     playersX = [];     playersY = [];     playersDirect = [];     playersFuel = [];     isPlayersDead = [];     scores = [];     rests = [];     invincibleTimes = [];     spinTimes = []; }); | 
プレイヤーの座標、移動方向、燃料などがUpdatePlayer1ToClientで送られてきます。自機の場合はplayerX、playerY、playerFuel、isPlayerDead、invincibleTimeにも保存しておきます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | connection.on("UpdatePlayer1ToClient", function (key, playerNumber, x, y, direct, fuel, isdead, invincible, spin) {     if (key == connectionID) {         isPlaying = true;         playerX = x;         playerY = y;         playerFuel = fuel;         isPlayerDead = isdead;         invincibleTime = invincible;     }     playersNumber.push(playerNumber);     playersX.push(x);     playersY.push(y);     playersDirect.push(direct);     playersFuel.push(fuel);     isPlayersDead.push(isdead);     invincibleTimes.push(invincible);     spinTimes.push(spin); }); | 
プレイヤーネームやスコア、残機はUpdatePlayer2ToClientで送信されます。これも変数と配列に保存しておきます。
| 1 2 3 4 5 6 7 8 9 10 11 | connection.on("UpdatePlayer2ToClient", function (key, playername, score, rest) {     if (key == connectionID) {         playerName = playername;         playerScore = score;         playerRest = rest;     }     playersName.push(playername);     scores.push(score);     rests.push(rest); }); | 
未通過のフラッグの位置はUpdateFlagsToClientで送られてきます。
| 1 2 3 4 5 6 7 8 9 10 11 12 | connection.on("UpdateFlagsToClient", function (xs, ys) {     if (xs != '') {         console.log("ReceiveUpdateFlags");         flagsX = xs.split(',');         flagsY = ys.split(',');     }     else {         flagsX = [];         flagsY = [];     } }); | 
新しく生成された煙幕と消滅した煙幕の位置はUpdateSmokesToClientで送られてきます。送られてきたデータを配列に変換して、removeXs、smokeYs、smokeNumbersの各要素を加除します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | connection.on("UpdateSmokesToClient", function (newSmokesX, newSmokesY, newSmokesNumber, smokesDisappearX, smokesDisappearY) {     if (smokesDisappearX != '') {         let removeXs = smokesDisappearX.split(',');         let removeYs = smokesDisappearY.split(',');         for (let i = 0; i < removeXs.length; i++) {             for (let k = 0; k < smokeXs.length; k++) {                 if (removeXs[i] == smokeXs[k] && removeYs[i] == smokeYs[k]) {                     smokeXs.splice(k, 1);                     smokeYs.splice(k, 1);                     smokeNumbers.splice(k, 1);                     break;                 }             }         }     }     if (newSmokesX != '') {         let addXs = newSmokesX.split(',');         let addYs = newSmokesY.split(',');         let addNumbers = newSmokesNumber.split(',');         for (let i = 0; i < newSmokesX.length; i++) {             let find = false;             for (let k = 0; k < smokeXs.length; k++) {                 if (addXs[i] == smokeXs[k] && addYs[i] == smokeYs[k]) {                     find = true;                     break;                 }             }             if (!find) {                 smokeXs.push(addXs[i]);                 smokeYs.push(addYs[i]);                 smokeNumbers.push(addNumbers[i]);             }         }     } }); | 
火花の座標と消滅までの時間はUpdateSparksToClientで送られてきます。これも配列に格納しておきます。
| 1 2 3 4 5 6 7 8 9 10 11 12 | connection.on("UpdateSparksToClient", function (xs, ys, lifes) {     if (xs != '') {         sparksX = xs.split(',');         sparksY = ys.split(',');         sparksLife = lifes.split(',');     }     else {         sparksX = [];         sparksY = [];         sparksLife = [];     } }); | 
現在、プレイ中なのか? ゲームに参加できる状態なのか? 参加中であればプレイヤー名と車の色は何か、スコアや残機の情報を表示させるための情報はUpdateGameStatusToClientで送られてきます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | connection.on("UpdateGameStatusToClient", function (gameStatus, canGameStart, imageIndex) {     let imgUrl = '';     if (imageIndex == 0)         imgUrl = base_url + '/rally-x/blue-car.png';     if (imageIndex == 1)         imgUrl = base_url + '/rally-x/red-car.png';     if (imageIndex == 2)         imgUrl = base_url + '/rally-x/green-car.png';     let gameStatusElement = document.getElementById('gameStatus');     gameStatusElement.style.fontWeight = 'bold';     gameStatusElement.style.color = 'yellow';     if (imgUrl == '' || canGameStart)         gameStatusElement.innerHTML = gameStatus;     else         gameStatusElement.innerHTML = `             <div style = "display:flex">             <div><img src = "${imgUrl}" width = "32" height = "32"></div>             <div style = "margin-top:5px; margin-left:10px;">現在[${GetPlayerName()}]で参戦中</div>             </div>         `;     if (canGameStart) {         document.getElementById('startButton1').style.display = 'block';         document.getElementById('startButton2').style.display = 'block';         document.getElementById('rankingButton').style.display = 'block';     }     else {         document.getElementById('startButton1').style.display = 'none';         document.getElementById('startButton2').style.display = 'none';         document.getElementById('rankingButton').style.display = 'none';     } }); | 
GetPlayerName関数はページに表示させるプレイヤー名をエスケープ処理をしたあと取得するためのものです。
| 1 2 3 4 5 6 7 8 | function GetPlayerName() {     let name = playerName;     if (name.indexOf('<') != -1)         name = name.split('<').join('<');     if (name.indexOf('>') != -1)         name = name.split('>').join('>');     return name; } | 
更新用のデータがサーバーサイドからすべて送られてきたら最後にEndUpdateToClientが送られてきます。これを受信したら描画処理をおこないます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | connection.on("EndUpdateToClient", function () {     // BGMの再生が完了していたら最初から再生する     PlayBGMIfNeed();     updateCount++;     ctx.fillStyle = "#000000";     ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);     // isTimeBetweenStages == trueの場合はステージとステージのあいだに表示する文字列を表示させる     if (isTimeBetweenStages) {         let texts = textBetweenStages.split(',');         ctx.fillStyle = "#ffffff";         ctx.font = "bold 16px MS ゴシック";         if (texts[0] != undefined)             ctx.fillText(texts[0], 10, 50);         if (texts[1] != undefined)             ctx.fillText(texts[1], 10, 110);         if (texts[2] != undefined)             ctx.fillText(texts[2], 10, 170);         if (texts[3] != undefined)             ctx.fillText(texts[3], 10, 210);         if (texts[4] != undefined)             ctx.fillText(texts[4], 10, 250);         smokeNumbers = [];         smokeXs = [];         smokeYs = [];         return;     }     // 自機を中央に描画したいので、どれだけ平行移動させるかを取得する     GetShiftXY();     // 平行移動させて壁、車、煙幕、フラッグ、火花を描画する     DrawWalls();     DrawSmokes();     DrawFlags();     DrawPlayers();     DrawSparks();     // 右側にレーダーを描画     DrawRadar();     // 各プレイヤーの情報を表示     ShowPlayerInfos();     // プレイ中である場合は自機に関する情報を表示する     if (isPlaying) {         let scoreElement = document.getElementById('score');         scoreElement.style.fontWeight = 'bold';         scoreElement.style.color = 'white';         scoreElement.style.fontSize = '125%';         scoreElement.innerText = 'Score ' + playerScore;         let restElement = document.getElementById('rest');         restElement.style.fontWeight = 'bold';         restElement.style.color = 'white';         restElement.style.fontSize = '125%';         restElement.innerText = '残 ' + playerRest;     } }); | 
プレイヤー情報の表示
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | function ShowPlayerInfos() {     let playerNameElement1 = document.getElementById('playerName' + (100));     if (playerNameElement1 != null)         playerNameElement1.innerText = 100;     for (let i = 0; i < playersX.length; i++) {         let playerNameElement = document.getElementById('playerName' + (i + 1));         if (playerNameElement == null)             continue;         let score = '';         if (scores[i] != '') {             score = ' (' + scores[i] + ' - ' + rests[i] + ')';             playerNameElement.parentElement.style.display = 'flex';             playerNameElement.innerText = playersName[i] + score;         }         else {             playerNameElement.parentElement.style.display = 'none';         }     } } | 
ゲームにユーザーが参加したとか離脱したという情報はNotificationToClientとNotificationToClient2で送られてきます。両者の違いは効果音を伴うかどうかです。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | let messages = []; connection.on("NotificationToClient", function (str) {     messages.push(str);     ShowNotification();     if (isPlaying && document.getElementById('sound-checkbox').checked) {         notificationSound.currentTime = 0;         notificationSound.play();     }     setTimeout(() => {         if (messages.length > 0) {             messages.shift();             ShowNotification();         }     }, 3000); }); connection.on("NotificationToClient2", function (str) {     messages.push(str);     ShowNotification();     setTimeout(() => {         if (messages.length > 0) {             messages.shift();             ShowNotification();         }     }, 3000); }); function ShowNotification() {     let infoElement = document.getElementById('information');     infoElement.style.marginTop = '15px';     infoElement.style.marginBottom = '15px';     let message = "";     for (let i = 0; i < messages.length; i++) {         message += messages[i] + '<br>';     }     infoElement.innerHTML = message; } | 
描画処理
GetShiftXY関数は自機を中央に描画するためにそれ以外のものをどれだけ平行移動させるかを取得するためのものです。
| 1 2 3 4 5 6 | function GetShiftXY() {     if (playerX != undefined) {         shiftX = -playerX + 32 * 5;         shiftY = -playerY + 32 * 5;     } } | 
DrawWalls関数は名前のとおり壁を描画します。
| 1 2 3 4 5 6 7 | function DrawWalls() {     ctx.fillStyle = "#ffffff";     ctx.fillRect(shiftX, shiftY, fieldWidth, fieldHeight);     for (let i = 0; i < wallXs.length; i++)         ctx.drawImage(imgWall, wallXs[i] - 0 + shiftX, wallYs[i] - 0 + shiftY, 32, 32); } | 
DrawPlayers関数は車を描画します。進行方向と車の種類にあわせて適切なイメージで描画します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | function DrawPlayers() {     for (let i = 0; i < playersX.length; i++) {         let imgPlayer = null;         let carGroup = Math.floor(playersNumber[i] / 100);         if (carGroup == 1) {             if (playersDirect[i] == 'n')                 imgPlayer = imgPlayer1n;             else if (playersDirect[i] == 'e')                 imgPlayer = imgPlayer1e;             else if (playersDirect[i] == 's')                 imgPlayer = imgPlayer1s;             else if (playersDirect[i] == 'w')                 imgPlayer = imgPlayer1w;             else                 imgPlayer = imgPlayer1n;             if (spinTimes[i] > 0) {                 if (spinTimes[i] % 4 == 0)                     imgPlayer = imgPlayer1n;                 if (spinTimes[i] % 4 == 1)                     imgPlayer = imgPlayer1e;                 if (spinTimes[i] % 4 == 2)                     imgPlayer = imgPlayer1w;                 if (spinTimes[i] % 4 == 3)                     imgPlayer = imgPlayer1w;             }         }         else if (carGroup == 2) {             if (playersDirect[i] == 'n')                 imgPlayer = imgPlayer2n;             else if (playersDirect[i] == 'e')                 imgPlayer = imgPlayer2e;             else if (playersDirect[i] == 's')                 imgPlayer = imgPlayer2s;             else if (playersDirect[i] == 'w')                 imgPlayer = imgPlayer2w;             else                 imgPlayer = imgPlayer2n;             if (spinTimes[i] > 0) {                 if (spinTimes[i] % 8 < 2)                     imgPlayer = imgPlayer2n;                 else if (spinTimes[i] % 8 < 4)                     imgPlayer = imgPlayer2e;                 else if (spinTimes[i] % 8 < 6)                     imgPlayer = imgPlayer2w;                 else                     imgPlayer = imgPlayer2w;             }         }         else if (carGroup == 3) {             if (playersDirect[i] == 'n')                 imgPlayer = imgPlayer3n;             else if (playersDirect[i] == 'e')                 imgPlayer = imgPlayer3e;             else if (playersDirect[i] == 's')                 imgPlayer = imgPlayer3s;             else if (playersDirect[i] == 'w')                 imgPlayer = imgPlayer3w;             else                 imgPlayer = imgPlayer3n;             if (spinTimes[i] > 0) {                 if (spinTimes[i] % 4 == 0)                     imgPlayer = imgPlayer3n;                 if (spinTimes[i] % 4 == 1)                     imgPlayer = imgPlayer3e;                 if (spinTimes[i] % 4 == 2)                     imgPlayer = imgPlayer3w;                 if (spinTimes[i] % 4 == 3)                     imgPlayer = imgPlayer3w;             }         }         else {             if (playersDirect[i] == 'n')                 imgPlayer = imgPlayer2n;             else if (playersDirect[i] == 'e')                 imgPlayer = imgPlayer2e;             else if (playersDirect[i] == 's')                 imgPlayer = imgPlayer2s;             else if (playersDirect[i] == 'w')                 imgPlayer = imgPlayer2w;             else                 imgPlayer = imgPlayer2n;         }         // 生存しているプレイヤーだけ描画する         // 無敵状態であれば点滅させる         let show = true;         if (isPlayersDead[i] || (invincibleTimes[i] > 0 && invincibleTimes[i] % 2 == 0))             show = false;         if (show)             ctx.drawImage(imgPlayer, playersX[i] - 0 + shiftX, playersY[i] - 0 + shiftY, 32, 32);     } } | 
フラッグ、煙幕、火花を描画します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | function DrawFlags() {     for (let i = 0; i < flagsX.length; i++) {         ctx.drawImage(imgFlag, flagsX[i] - 0 + shiftX, flagsY[i] - 0 + shiftY, 32, 32);     } } function DrawSmokes() {     for (let i = 0; i < smokeXs.length; i++) {         let imgSmoke = null;         if (smokeNumbers[i] == 1)             imgSmoke = imgSmoke1;         if (smokeNumbers[i] == 2)             imgSmoke = imgSmoke2;         if (smokeNumbers[i] == 3)             imgSmoke = imgSmoke3;         if (imgSmoke != null)             ctx.drawImage(imgSmoke, smokeXs[i] - 0 + shiftX, smokeYs[i] - 0 + shiftY, 32, 32);     } } function DrawSparks() {     for (let i = 0; i < sparksX.length; i++) {         let imgSpark = null;         if (sparksLife[i] >= 11)             imgSpark = imgSpark1;         else if (sparksLife[i] >= 9)             imgSpark = imgSpark2;         else if (sparksLife[i] >= 7)             imgSpark = imgSpark3;         else if (sparksLife[i] >= 5)             imgSpark = imgSpark4;         else if (sparksLife[i] >= 3)             imgSpark = imgSpark5;         else if (sparksLife[i] >= 1)             imgSpark = imgSpark6;         if (imgSpark != null)             ctx.drawImage(imgSpark, sparksX[i] - 0 + shiftX, sparksY[i] - 0 + shiftY, 48, 48);     } } | 
canvasの右側に燃料系とレーダーを描画します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | function DrawRadar() {     ctx.fillStyle = "#000000";     ctx.fillRect(360, 0, 180, CANVAS_HEIGHT);     // 燃料計     ctx.fillStyle = "#008888";     ctx.fillRect(370, 20, 160, 20);     if (playerFuel > 0) {         ctx.fillStyle = "#00ff00";         ctx.fillRect(370, 20, 160 * playerFuel / 3200, 20);     }     // レーダー     ctx.fillStyle = "#000064";     let mapWidth = 160;     let mapHeight = CANVAS_HEIGHT - 70;     ctx.fillRect(370, 60, mapWidth, mapHeight);     for (let i = 0; i < flagsX.length; i++) {         let x = mapWidth * flagsX[i] / 1600;         let y = mapHeight * flagsY[i] / 3200;         ctx.fillStyle = "#00ff00";         ctx.fillRect(370 + x - 4, 60 + y - 4, 4, 4);     }     for (let i = 0; i < playersX.length; i++) {         let x = mapWidth * playersX[i] / 1600;         let y = mapHeight * playersY[i] / 3200;         let carGroup = Math.floor(playersNumber[i] / 100);         if (carGroup == 1)             ctx.fillStyle = "#0080ff";         if (carGroup == 2)             ctx.fillStyle = "#ff0000";         if (carGroup == 3)             ctx.fillStyle = "#00ff00";         if (x >= 0)             ctx.fillRect(370 + x - 4, 60 + y - 4, 4, 4);     }     let x = mapWidth * playerX / 1600;     let y = mapHeight * playerY / 3200;     if(!isPlayerDead)         ctx.fillStyle = "#ffffff";     else         ctx.fillStyle = "#000000";     ctx.fillRect(370 + x - 4, 60 + y - 4, 4, 4); } | 
サーバーサイドからIsTimeBetweenStagesToClientが送られてきたら、ここから現在はステージとステージのあいだなのか、そのときに表示すべき文字列を取得することができます。
| 1 2 3 4 5 | let textBetweenStages = ''; connection.on("IsTimeBetweenStagesToClient", function (isTBSs, text) {     isTimeBetweenStages = isTBSs;     textBetweenStages = text; }); | 
効果音
PlayBGMIfNeed関数はBGMが最後まで再生されているのであれば、最初から再生しなおします。
| 1 2 3 4 5 6 7 8 9 | function PlayBGMIfNeed() {     if (isPlaying && document.getElementById('sound-checkbox').checked) {         if (!isPlayingBgm) {             isPlayingBgm = true;             bgm.currentTime = 0;             bgm.play();         }     } } | 
StopBGM関数はBGMを停止します。
| 1 2 3 4 5 | function StopBGM() {     isPlayingBgm = false;     bgm.currentTime = 0;     bgm.pause(); } | 
フラッグがある位置を通過したらサーバーサイドからGetFlagToClientが送られてきます。そのときに効果音を鳴らします。
| 1 2 3 4 5 6 | connection.on("GetFlagToClient", function () {     if (isPlaying && document.getElementById('sound-checkbox').checked) {         flagSound.currentTime = 0;         flagSound.play();     } }); | 
煙幕を放出したらサーバーサイドからSmokeToClientが送られてきます。そのときも効果音を鳴らします。
| 1 2 3 4 5 6 | connection.on("SmokeToClient", function () {     if (isPlaying && document.getElementById('sound-checkbox').checked) {         smokeSound.currentTime = 0;         smokeSound.play();     } }); | 
プレイヤーが撃破された場合、別のプレイヤーを撃破した場合、それぞれPlayerDeadToClientとEnemyDeadToClientが送られてきます。その場合も効果音を鳴らします。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | connection.on("PlayerDeadToClient", function () {     if (isPlaying && document.getElementById('sound-checkbox').checked) {         deadSound.currentTime = 0;         deadSound.play();     } }); connection.on("EnemyDeadToClient", function () {     if (isPlaying && document.getElementById('sound-checkbox').checked) {         explodeSound.currentTime = 0;         explodeSound.play();     } }); | 
ゲームオーバーになったときはサーバーサイドからGameOverToClientが送られてきます。画面にゲームオーバーと表示してBGMを停止、効果音を鳴らします。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | connection.on("GameOverToClient", function () {     isPlaying = false;     StopBGM();     document.getElementById('rest').style.fontWeight = 'bold';     document.getElementById('rest').style.color = 'red';     document.getElementById('rest').innerHTML = 'Game Over';     if (document.getElementById('sound-checkbox').checked) {         gameoverSound.currentTime = 0;         gameoverSound.play();     } }); | 
