ASP.NET Core版 タイピングゲームをつくる(2)の続きです。クライアントサイドにおける処理を実装します。
Contents
HTML部分
Pages\Typing\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 |
@page @{ ViewData["Title"] = "タイピングゲーム"; Layout = "_Layout_none"; string baseurl = Global.BaseUrl; } <div class = "display-none"> <img src="@baseurl/typing/missile.png" alt="" id="missile" /> </div> <div id="container"> <canvas id="can"></canvas> <!-- デバッグ用 --> <div class="display-none"> お題:<span id="question"></span> 時間:<span id="time"></span> </div> <!-- デバッグ用ここまで --> <div> 入力:<br> <input id="input" type="text" onsubmit="return false" /> </div> <p>遊び方</p> <p>つぎつぎとあわられるひらがなを入力してSpaceキーかEnterキーを押してください。</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()"> <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/TypingHub").build(); let base_url = '@baseurl'; </script> <script src="@baseurl/typing/typing.js"></script> |
JavaScript部分
wwwroot\typingフォルダのなかにtyping.jsという名前でjsファイルを作成します。
初期化の処理
ここではレイアウトの調整、Canvasの初期化(後述)、BGMがゲーム中はエンドレスで再生されるように設定(後述)し、効果音のボリューム調整(後述)をおこないます。そのあとAspNetCore.SignalRで接続を試みます。
wwwroot\typing\typing.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 |
const CANVAS_WIDTH = 480; const CANVAS_HEIGHT = 320; let can = document.getElementById('can'); let ctx = can.getContext('2d'); // 現在プレイ中(ゲームオーバーではない)か? let isPlaying = false; // 現在プレイ中(ゲームオーバーではない)か? let isStoping = false; let startButton1 = document.getElementById('startButton1'); let showTop30Button1 = document.getElementById('showTop30Button1'); let inputElem = document.getElementById('input'); // デバッグ用 let questionElement = document.getElementById('question'); let timeElement = document.getElementById('time'); window.addEventListener('load', Init); function Init() { let elmcontainer = document.getElementById('container'); elmcontainer.style.marginTop = '50px'; elmcontainer.style.marginLeft = '20px'; inputElem.style.marginTop = '10px'; inputElem.style.marginBottom = '50px'; inputElem.style.width = CANVAS_WIDTH + 'px'; startButton1.style.marginTop = '15px'; startButton1.style.marginBottom = '15px'; InitCanvas(); // 後述 SetVolumes(0.01); // 後述 InitBgm(); // 後述 document.getElementById('sound-checkbox').checked = true; connection.start().catch(function (err) { document.getElementById("conect-result").innerHTML = '接続失敗'; }); isStoping = false; // デバッグ情報表示用 let elms = document.getElementsByClassName('display-none'); for (let i = 0; i < elms.length; i++) { elms[i].style.display = 'none'; // 'none'ではなく 'block'; にすると非表示にしているものが見える elms[i].style.color = "white"; } questionElement.innerText = ''; } |
キャンバスの初期化の処理を示します。Canvasの大きさの設定をおこない、そのあと全体を黒で塗りつぶします。そしてゲームが開始される前はCanvasに「タイピングゲーム」と表示させます。それからそれだけではさみしい(?)のでスコアと残機を0の状態で表示させます。
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 |
let scorePositionY = 30; // スコアと残機が表示されるY座標 let missilePositionY = 150; // ミサイルが表示されるY座標 let questionPositionY = 100; // お題が表示されるY座標 let timePositionY = 310; // 残り時間が表示されるY座標 function InitCanvas() { can.width = CANVAS_WIDTH; can.height = CANVAS_HEIGHT; ctx.fillStyle = '#000'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); ctx.fillStyle = '#0ff'; ctx.font = '32px MS ゴシック'; let str = 'タイピングゲーム'; let textMetrics1 = ctx.measureText(str); let x1 = (CANVAS_WIDTH - textMetrics1.width) / 2; ctx.fillText(str, x1, 200); let scoreText = 'SCORE ' + 0; let lifeText = 'LIFE ' + 0; ctx.fillStyle = 'white'; ctx.font = '24px MS ゴシック'; ctx.fillText(scoreText, 10, scorePositionY); ctx.fillText(lifeText, 200, scorePositionY); } |
効果音の初期化
ここでは効果音のボリュームの設定をおこなっています。あとタイピングゲームなのであまりBGMが大きすぎるのはどうかと思ったので小さめの値(効果音の半分)を設定しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
let correctAnswerSound = new Audio(base_url + '/typing/correct-answer.mp3'); let incorrectAnswerSound = new Audio(base_url + '/typing/incorrect-answer.mp3'); let missSound = new Audio(base_url + '/typing/miss.mp3'); let gameoverSound = new Audio(base_url + '/typing/gameover.mp3'); let bgm = new Audio(base_url + '/typing/bgm.mp3'); function SetVolumes(volume) { // 効果音の大きさに違いがあるので調整している let volume2 = volume * 3; if (volume2 > 1) volume2 = 1; bgm.volume = volume / 2; correctAnswerSound.volume = volume; incorrectAnswerSound.volume = volume; missSound.volume = volume2; gameoverSound.volume = volume; } function IsSound() { return isPlaying && document.getElementById('sound-checkbox').checked; } |
ゲーム中はBGMをエンドレスで再生したいので0.5秒おきに一定の部分まで再生されたかどうかを調べて、その場合は最初から再生しなおしています。最後まで再生した段階で最初から再生するという方法もあるのですが、素材として使っているBGMが変なところで切れているので適切な場所(このBGMなら79秒がよさそう)で最初から再生しなおしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function InitBgm() { // BGMの末尾が変な終わり方をしているので適切な場所(79秒?)で最初から再生しなおす setInterval(() => { if (IsSound()) { if (bgm.currentTime > 79 || bgm.currentTime == 0) { bgm.currentTime = 0; bgm.play(); } } else StopBgm(); }, 500); } // BGMを止める function StopBgm() { bgm.pause(); bgm.currentTime = 0; } |
接続成功時の処理
AspNetCore.SignalRで接続しようとして正常に処理が完了したときは、サーバーサイドから”SuccessfulConnectionToClient”が送信されます。クライアントサイドでこれを受信したらグローバル変数connectionIDにIDを保存しておきます。またページの下部にIDを表示させます。
1 2 3 4 5 6 |
let connectionID = ''; connection.on("SuccessfulConnectionToClient", function (result, id) { connectionID = id; document.getElementById("conect-result").innerHTML = `conect-result ${result}:${id}`; }); |
ゲーム開始時の処理
ゲーム開始のときはテキストボックスに入力されたプレイヤー名とconnectionIDに保存されている文字列をサーバーサイドに送信します。
この処理がうまくいったらサーバーサイドから”EventGameStartToClient”が送信されます。これを受信したらスタートボタンを非表示にします。またプレイが開始されたのでisPlayingフラグをtrueに変更し、ミスによる中断はしていないのでisStopingフラグをfalseに変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function GameStart() { inputElem.value = ''; if (connectionID != '') { let playerName = document.getElementById('player-name').value; connection.invoke("GameStart", connectionID, playerName).catch(function (err) { return console.error(err.toString()); }); } } connection.on("EventGameStartToClient", function () { startButton1.style.display = 'none'; showTop30Button1.style.display = 'none'; isPlaying = true; isStoping = false; }); |
キー操作時の処理
キーが押下されたときの処理を示します。
通常のタイピングゲームではアルファベットを入力するので、お題が「ふじさん」だと「huzisann」になり「fujisann」では間違いと判定されるという問題があります。また人によっては普段はローマ字入力ではなく「カナ入力」をする場合もあるかもしれません。
どれであっても最終的に「ふじさん」と表示されている状態でEnterキーまたはSpaceキーを押した段階で正誤判定ができるようにしたいので、技を使います。
EnterキーまたはSpaceキーが押された段階でテキストボックスの文字列を取得してサーバーサイドに送信したいのですが、IMEが起動しているときはe.keyCodeは229にしかなりません。ただしキーが離されたときはEnterキーなら13、Spaceキーなら32になります。
そこでキーが押下された段階でテキストボックスの文字列を取得し、グローバル変数に格納します。キーが離されたときのe.keyCodeが13または32ならこのときグローバル変数に格納されている文字列をサーバーサイドに送ることにします。
1 2 3 4 5 6 7 8 9 10 11 |
let inputtedText = ''; inputElem.onkeydown = function (e) { inputtedText = inputElem.value; } inputElem.onkeyup = function (e) { if (e.keyCode == 32 || e.keyCode == 13) { SendYourAnswer(inputtedText); } } |
引数として渡された文字列をサーバーサイドに送る処理を示します。
プレイされる前であったりゲームが中断されているときは、テキストボックスをクリアするだけでなにもしません。そうでないときはプレイヤー名と引数をサーバーサイドに送信します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function SendYourAnswer(sendText) { inputElem.value = ''; if (!isPlaying || isStoping || sendText == '') return; let playerName = document.getElementById('player-name').value; // // ゲーム中にプレイヤー名を変更可能なのでキーが押されたらそのつどプレイヤー名も送信する // 接続されていない場合はなにもしない if (connectionID != '') { connection.invoke("SendStringToServer", sendText, playerName).catch(function (err) { return console.error(err.toString()); }); } } |
Canvasへの描画
プレイ中はお題とスコアと残機を描画させます。また残り時間が少なくなるにつれてミサイルを右から左に移動させます。
ミス時はしばらくの間、背景を赤と黒でチカチカさせます。グローバル変数 drawCountOnMissでミス時の更新回数を数えて2の倍数で10未満のときだけ背景を赤にしてそれ以外の時は黒にします。ミス時以外のときはdrawCountOnMissは強制的に0にします。
ミス時はお題は消去します。またゲームオーバー時には’GAME OVER’と描画させます。
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 |
let drawCountOnMiss = 0; let missileImage = document.getElementById('missile'); function DrawToCanvas(question, ms, timeLimit, score, life) { if (!isStoping) { ctx.fillStyle = '#000'; drawCountOnMiss = 0; } else { drawCountOnMiss++; if (drawCountOnMiss % 2 == 0 && drawCountOnMiss < 10) ctx.fillStyle = '#f00'; else ctx.fillStyle = '#000'; } ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); ctx.fillStyle = '#0ff'; ctx.font = '32px MS ゴシック'; if (isStoping) question = ''; if (!isPlaying) question = 'GAME OVER'; // 文字列を中心に描画するためのX座標を求める let textMetrics1 = ctx.measureText(question); let x1 = (CANVAS_WIDTH - textMetrics1.width) / 2; // お題または'GAME OVER'の文字列を描画する if (isPlaying) ctx.fillText(question, x1, questionPositionY); else ctx.fillText(question, x1, questionPositionY - 50); // 制限時間と残り時間からミサイルのX座標を求める let mx = ms / timeLimit * CANVAS_WIDTH; // プレイ中でミス時以外の時だけミサイルを描画する if (!isStoping && isPlaying) ctx.drawImage(missileImage, mx, 50); // 残り時間として描画すべき文字列を求める(少数点以下2桁まで 0以下のときは0.00とする) let msText = (ms / 1000).toString() + '00'; if (ms > 10 * 1000) msText = msText.slice(0, 5); else if (ms > 0) msText = msText.slice(0, 4); else msText = '0.00'; // 残り時間を中心に描画するためのX座標を求める let textMetrics2 = ctx.measureText(msText); let x2 = (CANVAS_WIDTH - textMetrics2.width) / 2; ctx.fillStyle = '#f00'; ctx.font = '28px MS ゴシック'; ctx.fillText(msText, x2, timePositionY); // スコアと残機を描画する let scoreText = 'SCORE ' + score; let lifeText = 'LIFE ' + life; ctx.fillStyle = 'white'; ctx.font = '24px MS ゴシック'; ctx.fillText(scoreText, 10, scorePositionY); ctx.fillText(lifeText, 200, scorePositionY); ctx.fillStyle = '#000'; } |
新しいお題がサーバーサイドから送られてきたときの処理を示します。
ミス時はテキストボックスに文字列が入力されている場合があるので、これをクリアします。あとは上記のDrawToCanvas関数を呼び出すだけです。
1 2 3 4 5 6 |
connection.on("SendQuestionToClient", function (question, ms, timeLimit, score, life) { inputElem.value = ''; questionElement.innerText = question; // デバッグ用 DrawToCanvas(question, ms, timeLimit, score, life); }); |
残り時間が変動したらサーバーサイドから”UpdateEventToClient”が送られてきます。このときも上記のDrawToCanvas関数を呼び出して適切な描画処理をおこないます。
1 2 3 4 5 6 |
connection.on("UpdateEventToClient", function (question, ms, timeLimit, score, life) { questionElement.innerText = question; // デバッグ用 timeElement.innerText = (ms / 1000).toString(); // デバッグ用 DrawToCanvas(question, ms, timeLimit, score, life); }); |
正誤判定の処理
お題に対する入力が正しいか間違っているかの判定結果をサーバーサイドから受け取ったときの処理を示します。
どちらも効果音を鳴らすだけです。スコアの計算等はサーバーサイドでおこなわれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
connection.on("CorrectAnswerEventToClient", function () { if (IsSound()) { correctAnswerSound.currentTime = 0; correctAnswerSound.play(); } }); connection.on("IncorrectAnswerEventToClient", function () { if (IsSound()) { incorrectAnswerSound.currentTime = 0; incorrectAnswerSound.play(); } }); |
ミス時の処理
ミスをするとサーバーサイドから”MissEventToClient”が送信されます。
テキストボックスの文字列をクリアします。そのあとisStopingフラグをセットし効果音を鳴らします。
1 2 3 4 5 6 7 8 9 |
connection.on("MissEventToClient", function () { inputElem.value = ''; isStoping = true; if (IsSound()) { missSound.currentTime = 0; missSound.play(); } }); |
ミスから復帰したときには”ResumeEventToClient”が送られてきます。この場合はミス時にセットされたフラグをクリアします。
1 2 3 |
connection.on("ResumeEventToClient", function () { isStoping = false; }); |
ゲームオーバー時の処理
ゲームオーバーになったらサーバーサイドから”GameOverEventToClient”が送信されます。
この場合はisPlayingフラグをクリアしてBGMを止め、ます。そして非表示になっていたゲーム開始用のボタンを再表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 |
connection.on("GameOverEventToClient", function (score) { isPlaying = false; StopBgm(); startButton1.style.display = 'block'; showTop30Button1.style.display = 'block'; if (document.getElementById('sound-checkbox').checked) { gameoverSound.currentTime = 0; gameoverSound.play(); } }); |