ASP.NET Core版 デジタルインベーダーをつくる(2)の続きです。今回はクライアントサイドにおける処理を実装します。
Contents
HTML部分
HTML部分を示します。
Pages\DigitalInvader\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 | @page @{     ViewData["Title"] = "鳩でもわかるデジタルインベーダー";     Layout = "_Layout_none";     string baseurl = Global.BaseUrl;     // ただし Global.BaseUrl == "https://lets-csharp.com/samples/2204/aspnetcore-app-zero" } <div class = "display-none">     <img src="@baseurl/digital-invader/number0.png" id="num0">     <img src="@baseurl/digital-invader/number1.png" id="num1">     <img src="@baseurl/digital-invader/number2.png" id="num2">     <img src="@baseurl/digital-invader/number3.png" id="num3">     <img src="@baseurl/digital-invader/number4.png" id="num4">     <img src="@baseurl/digital-invader/number5.png" id="num5">     <img src="@baseurl/digital-invader/number6.png" id="num6">     <img src="@baseurl/digital-invader/number7.png" id="num7">     <img src="@baseurl/digital-invader/number8.png" id="num8">     <img src="@baseurl/digital-invader/number9.png" id="num9">     <img src="@baseurl/digital-invader/ufo.png" id="ufo">     <img src="@baseurl/digital-invader/none.png" id="none">     <img src="@baseurl/digital-invader/life1.png" id="life1">     <img src="@baseurl/digital-invader/life2.png" id="life2">     <img src="@baseurl/digital-invader/life3.png" id="life3"> </div> <div id="container">     <div class = "display-none">         <p><span id="target"></span>:<span id="enemies-text"></span></p>         <p>             Score:<span id="score"></span><br>             追加点:<span id="add"></span><br>             Stage:<span id="stage"></span><br>             Life:<span id="life"></span>         </p>     </div>     <canvas id="can"></canvas>     <br>     <input type="button" id="zButton" value="Z" onclick="ButtonClick('Z')">     <input type="button" id="xButton" value="X" onclick="ButtonClick('X')">     <p>遊び方</p>     <p>右側から攻め込んでくるデジタルインベーダーを撃ち落としてください。<br>         ターゲット変更は Zキー。射撃は Xキー です。<br>         先頭から撃ち落とす必要はありません。     </p>     <form name="form1">     <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;">     </form>     <input type="button" id="showTop30Button1" onclick="location.href='./hi-score'" value="上位30位をチェック">     <p id = "conect-result"></p> </div> <script>     let connection = new signalR.HubConnectionBuilder().withUrl("@baseurl/DigitalInvaderHub").build();     let base_url = "@baseurl"; </script> <script src="@baseurl/digital-invader/digital-invader.js"></script> | 
画像ファイルは以下のようになっています。これをプロジェクトフォルダ内のwwwroot\digital-invaderフォルダに設置します。
 
  
  
  
  
  
  
  
  
 
number0.png ~ number9.png
 
  
 
life1.png ~ life3.png
 
 
ufo.png none.png
音声ファイルも同様にプロジェクトフォルダ内のwwwroot\digital-invaderフォルダに設置します。
hit.mp3
hit-ufo.mp3
clear.mp3
miss.mp3
gameover.mp3
JavaScript部分
グローバル変数と定数
グローバル変数と定数は以下のように宣言されています。
| 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 | const CANVAS_WIDTH = 480; const CANVAS_HEIGHT = 240; const NUMBER_WIDTH = 46; const NUMBER_HEIGHT = 82; const WIDTH_OF_SPACE = 10; let can = document.getElementById('can'); let ctx = can.getContext('2d'); // 現在プレイ中か? let isPlaying = false; // キーが押されているか? let isZKeyDown = false; let isXKeyDown = false; let connectionID = ''; let startButton1 = document.getElementById('startButton1'); let showTop30Button1 = document.getElementById('showTop30Button1'); let num0 = document.getElementById('num0'); let num1 = document.getElementById('num1'); let num2 = document.getElementById('num2'); let num3 = document.getElementById('num3'); let num4 = document.getElementById('num4'); let num5 = document.getElementById('num5'); let num6 = document.getElementById('num6'); let num7 = document.getElementById('num7'); let num8 = document.getElementById('num8'); let num9 = document.getElementById('num9'); let ufo = document.getElementById('ufo'); let none = document.getElementById('none'); let life1 = document.getElementById('life1'); let life2 = document.getElementById('life2'); let life3 = document.getElementById('life3'); // 効果音 let hitSound = new Audio(base_url + '/digital-invader/hit.mp3'); let hitUfoSound = new Audio(base_url + '/digital-invader/hit-ufo.mp3'); let clearSound = new Audio(base_url + '/digital-invader/clear.mp3'); let missSound = new Audio(base_url + '/digital-invader/miss.mp3'); let gameoverSound = new Audio(base_url + '/digital-invader/gameover.mp3'); | 
初期化の処理
読み込まれたら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 51 52 53 54 55 56 57 58 | window.addEventListener('load', Init); function Init() {     can.width = CANVAS_WIDTH;     can.height = CANVAS_HEIGHT;     if (ctx != null) {         ctx.fillStyle = 'white';         ctx.font = '32px MS ゴシック';     }     SetVolumes(0.02);     document.getElementById('sound-checkbox').checked = true;     let zButton = document.getElementById('zButton');     let xButton = document.getElementById('xButton');     if (zButton != null) {         zButton.style.width = '100px';         zButton.style.height = '50px';         zButton.style.marginTop = '15px';         zButton.style.marginBottom = '15px';     }     if (xButton != null) {         xButton.style.width = '100px';         xButton.style.height = '50px';         xButton.style.marginTop = '15px';         xButton.style.marginBottom = '15px';         xButton.style.marginLeft = '50px';     }     // 画面の横幅を取得する。幅が600ピクセルより大きい場合はボタンを表示しない     let outerWidth = window.outerWidth;     if (outerWidth > 600) {         if (zButton != null)             zButton.style.display = 'none';         if (xButton != null)             xButton.style.display = 'none';     }     connection.start().catch(function (err) {         document.getElementById("conect-result").innerHTML = '接続失敗';     }); } function SetVolumes(volume) {     // 効果音の大きさに違いがあるので調整している     let volume2 = volume * 3;     if (volume2 > 1)         volume2 = 1;     hitSound.volume = volume2;     hitUfoSound.volume = volume2;     clearSound.volume = volume;     missSound.volume = volume2;     gameoverSound.volume = volume; } | 
接続が成功時の処理
サーバーサイドへの接続が成功したらIDが返されるので受け取り保存します。そのあと接続が成功した旨を示す文字列とIDを表示します。
| 1 2 3 4 | connection.on("SuccessfulConnectionToClient", function (result, id) {     connectionID = id;     document.getElementById("conect-result").innerHTML = `conect-result ${result}:${id}`; }); | 
ゲーム開始時の処理
プレイヤー名と接続成功時に保存しておいたIDを引数にしてサーバーサイドの”GameStart”を呼び出します。この処理が成功するとサーバーサイドから”EventGameStartToClient”が返されます。
| 1 2 3 4 5 6 7 8 | function GameStart() {     if (connectionID != '') {         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 | connection.on("EventGameStartToClient", function () {     startButton1.style.display = 'none';     showTop30Button1.style.display = 'none';     isPlaying = true; }); | 
キー操作時の処理
キーが押下されたときは押しっぱなしの場合、連続して送信されないようにするためにフラグをセットし、そのあいだはなにもしないようにします。また接続されていない場合はなにもしません。
サーバーサイドに”DownKey”をキーと一緒に送信します。またゲーム中であってもプレイヤー名を変更できるようにしたいのでキーが押されたらそのつどプレイヤー名も送信することにします。
| 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 | document.onkeydown = function (e) {     // 大文字でも小文字でも対応できるようにするために大文字に変換する     let key = e.key.toUpperCase();     // キーが押しっぱなしになっているときの二重送信を防ぐ     if (key == "Z" && isZKeyDown)         return;     if (key == "X" && isXKeyDown)         return;     if (key == "Z")         isZKeyDown = true;     if (key == "X")         isXKeyDown = true;     // 押下されたキーとプレイヤー名を送信する     if (connectionID != '') {         let playerName = document.getElementById('player-name').value;         connection.invoke("DownKey", key, playerName).catch(function (err) {             return console.error(err.toString());         });     } } document.onkeyup = function (e) {     let key = e.key.toUpperCase();     if (key == 'Z')         isZKeyDown = false;     if (key == 'X')         isXKeyDown = false; } | 
ボタンクリック時(スマホの場合)の処理
スマホでプレイする場合、ZボタンとXボタンを表示させます。これらがクリックされたときの処理を示します。
サーバーサイドに”DownKey”をキー、プレイヤー名と一緒に送信します。
| 1 2 3 4 5 6 7 8 | function ButtonClick(key){     if (connectionID != '') {         let playerName = document.getElementById('player-name').value;         connection.invoke("DownKey", key, playerName).catch(function (err) {             return console.error(err.toString());         });     } } | 
インベーダーの状態が変化したときの処理
インベーダーが前進したり撃墜されたりしてその状態が変化したとき、サーバーサイドから”SendEnemiesEventToClient”が送信されます。この場合はDrawDigitalInvaders関数を呼び出して描画処理をおこなわせます。
| 1 2 3 4 | connection.on("SendEnemiesEventToClient", function (text) {     document.getElementById('enemies-text').innerText = text; // デバッグ用     DrawDigitalInvaders(text); }); | 
DrawDigitalInvaders関数における処理を示します。
まず文字列を配列に分解します。そのあとX座標を右へずらしながら数字に対応するイメージを描画します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | function DrawDigitalInvaders(str) {     const arr = [...str];     let x = 112;     for (let i = 0; i < 6; i++) {         let image = GetImageFromNumber(arr[i]);         if (image != null) {             ctx.drawImage(image, x, 0, NUMBER_WIDTH, NUMBER_HEIGHT);         }         x += NUMBER_WIDTH + WIDTH_OF_SPACE;     } } | 
引数に対応したイメージを返す関数です。空白部分(”-“または”-“)はnone(全面黒のイメージ)を返します。
| 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 | function GetImageFromNumber(num){     if(num == '0')         return num0;     else if(num == '1')         return num1;     else if(num == '2')         return num2;     else if(num == '3')         return num3;     else if(num == '4')         return num4;     else if(num == '5')         return num5;     else if(num == '6')         return num6;     else if(num == '7')         return num7;     else if(num == '8')         return num8;     else if(num == '9')         return num9;     else if(num == 'n')         return ufo;     else         return none; } | 
ターゲット変更時の処理
Zキーが押下された場合はターゲット変更の処理をおこないます。サーバーサイドから送信された”ChangeTargetEventToClient”を受信したら数字に対応したイメージを取得して、それを描画します。
| 1 2 3 4 5 6 7 8 9 | connection.on("ChangeTargetEventToClient", function (text) {     document.getElementById('target').innerText = text; // デバッグ用     DrawTarget(text); }); function DrawTarget(num){     let image = GetImageFromNumber(num);     ctx.drawImage(image, 0, 0, NUMBER_WIDTH, NUMBER_HEIGHT); } | 
Lifeが変更時の処理
ミスやステージクリアによってLifeが変更されたときはサーバーサイドから”ChangeLifeEventToClient”が送信されます。これを受信したら引数に対応したイメージを取得して描画処理をおこないます。
| 1 2 3 4 5 | connection.on("ChangeLifeEventToClient", function (num) {     document.getElementById('life').innerText = num; // デバッグ用     DrawLife(num);     DrawLifeText(num); }); | 
DrawLife関数に渡される値は0~3の整数です。GetImageFromLife関数で対応するイメージ(life1かlife2かlife3)を取得してこれを描画します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | function DrawLife(num){     let image = null;     image = GetImageFromLife(num);     let x = NUMBER_WIDTH + WIDTH_OF_SPACE;     ctx.drawImage(image, x, 0, NUMBER_WIDTH, NUMBER_HEIGHT); } function GetImageFromLife(num){     if(num == '0')         return life1;     if(num == '1')         return life1;     if(num == '2')         return life2;     if(num == '3')         return life3; } | 
DrawLifeText関数はデジタル文字としてLifeの値を描画するためのものです。数字に対応したイメージを取得してインベーダーの半分のサイズで下のほうに描画します。
| 1 2 3 4 5 6 7 8 9 10 11 12 | function DrawLifeText(str) {     let x = 142;     let y = 162;     ctx?.fillText("LIFE", 32, y + 36);     let image = GetImageFromNumber(str);     console.log(str);     if (image != null) {         ctx.drawImage(image, x, y, NUMBER_WIDTH / 2, NUMBER_HEIGHT / 2);     } } | 
スコア変更時の処理
追加点が正しく計算されているか確認するために、グローバル変数 oldScoreを宣言しています。スコアが変動したらサーバーサイドから”ChangeScoreEventToClient”が送信されるので、その引数をDrawScoreText関数に渡してスコアの描画処理をしています。
ゲームがおわってもう一度ゲームをするときにスコアが0に戻りますが、このままでは前のゲームのスコアが描画されたままになるので0のあと9桁分を黒で塗りつぶしています。
| 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 | let oldScore = 0; connection.on("ChangeScoreEventToClient", function (num) {     document.getElementById('score').innerText = num; // デバッグ用     document.getElementById('add').innerText = (num - oldScore).toString(); // デバッグ用(追加点の確認用)     oldScore = num;     DrawScoreText(num); }); function DrawScoreText(score) {     let scoreString = '';     if (score == 0){         // 前回のスコアを塗りつぶしたい         oldScore = 0;         scoreString = '0*********';     }     else         scoreString = score.toString();     const arr = [...scoreString];     let x = 142;     let y = 112;     ctx.fillText("SCORE", 32, y + 36);     for (let i = 0; i < arr.length; i++) {         let image = null;         image = GetImageFromNumber(arr[i]);         if (image != null) {             ctx.drawImage(image, x, y, NUMBER_WIDTH / 2, NUMBER_HEIGHT / 2);         }         x += NUMBER_WIDTH / 2 + WIDTH_OF_SPACE / 2;     } } | 
ステージの番号変更時の処理
ステージクリアによってステージの番号が変更されたときの処理を示します。
DrawStageText関数に引数を渡して描画処理を行なわせます。
| 1 2 3 4 | connection.on("ChangeStageEventToClient", function (num) {     document.getElementById('stage').innerText = num; // デバッグ用     DrawStageText(num); }); | 
DrawStageText関数は”STAGE”と引数の番号に対応するイメージを下部に描画します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | function DrawStageText(num){     let str = num.toString();     const arr = [...str];     let x = 142 + 250;     let y = 162;     ctx.fillText("STAGE", 32 + 250, y + 36);     for(let i=0; i<arr.length; i++){         let image = null;         image = GetImageFromNumber(arr[i]);         if(image != null){             ctx.drawImage(image, x, y, NUMBER_WIDTH/2, NUMBER_HEIGHT/2);         }         x += NUMBER_WIDTH / 2 + WIDTH_OF_SPACE / 2;     } } | 
効果音に関する処理
プレイ中で[音を出す]にチェックがつけられているときだけ効果音を鳴らします。
| 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 | function IsSound() {     return isPlaying && document.getElementById('sound-checkbox').checked; } // UFOを撃墜したとき connection.on("HitUfoEventToClient", function () {     if (IsSound()) {         hitUfoSound.currentTime = 0;         hitUfoSound.play();     } }); // 普通のインベーダーを撃墜したとき connection.on("HitEventToClient", function () {     if (IsSound()) {         hitSound.currentTime = 0;         hitSound.play();     } }); // ステージクリア時 connection.on("StageClearEventToClient", function () {     if (IsSound()) {         clearSound.currentTime = 0;         clearSound.play();     } }); | 
ミス時の処理
ミス時の処理を示します。ミス時はサーバーサイドから”MissEventToClient”が送信されますが、このときは効果音を鳴らすだけでなく、無音のとき視覚的にわかるように数字をすべて8(インベーダーの残りが1個や2個のときも)にします。
| 1 2 3 4 5 6 7 8 9 | connection.on("MissEventToClient", function () {     document.getElementById('enemies-text').innerText = 'XXXXXX'; // デバッグ用     DrawDigitalInvaders('888888');     if (IsSound()) {         missSound.currentTime = 0;         missSound.play();     } }); | 
ゲームオーバーになったときはサーバーサイドから”GameOverEventToClient”が送信されます。このときは効果音を鳴らすとともに、非表示になっていたゲームスタート用のボタンを再表示させます。
| 1 2 3 4 5 6 7 8 9 10 | connection.on("GameOverEventToClient", function () {     startButton1.style.display = 'block';     showTop30Button1.style.display = 'block';     if (IsSound()) {         gameoverSound.currentTime = 0;         gameoverSound.play();     }     isPlaying = false; }); | 
