ディフェンダーに似たオンライン対戦ゲームをつくる(3)の続きです。今回はクライアントサイドの処理を実装します。
Contents
HTML部分
Pages\defender\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 |
@page @{ Layout = ""; string baseUrl = "https://lets-csharp.com/samples/2204/aspnetcore-app-zero"; // 公開したいurl } <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>鳩でもわかるDEFENDER - 鳩でもわかるASP.NET Core</title> <meta name="viewport" content="width=device-width, initial-scale = 1.0"> <link rel="stylesheet" href="./style.css?@time" type="text/css" media="all"> <script src="https://lets-csharp.com/samples/2204/aspnetcore-app-zero/js/signalr.js"></script> </head> <body> <div id="container"> <div id="field"> <canvas id="canvas"></canvas> <button id="start">START</button> <button id="up" class="button">加速 ↑ 減速</button> <button id="left" class="button">Left</button> <button id="right" class="button">Right</button> <button id="down" class="button">加速 ↓ 減速</button> <button id="shot" class="button">SHOT</button> <div id="player-info"> プレイヤー名:<input type="text" id="player-name"> </div> <p> 音量: <input type="range" id="volume" min="0" max="1" step="0.01"> <span id="vol-range"></span> <button onclick="playSound()">音量テスト</button> </p> <input type="checkbox" id="show-button" checked> <label for="show-button">操作ボタンを表示する</label> <p><a href="./hi-score">トップ30を見る</a></p> <div id="conect-result"></div> </div><!-- /#field --> </div><!-- /#container --> <script> let connection = new signalR.HubConnectionBuilder().withUrl("@baseUrl/DefenderHub").build(); </script> <script src="./app.js"></script> </body> </html> |
CSS
wwwroot\defender\style.css
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 |
body { color: #fff; background-color: #000; } #container{ width: 380px; } #field{ position: relative; } #start { position: absolute; width: 150px; height: 70px; left:115px; top:300px; font-size: 150%; } #player-info { position: absolute; width: 200px; left:90px; top:400px; } #player-name { width: 200px; } #up { top: 220px; left: 110px; } #left { top: 300px; left: 0px; } #right { top: 300px; left: 220px; } #down { top: 380px; left: 110px; } #shot { top: 490px; left: 110px; } .button { position: absolute; background-color: transparent; color: white; width: 130px; height: 70px; display: none; font-size: 16px; } a { color: #0ff; font-weight: bold; } a:hover { color: #f00; font-weight: bold; } |
JavaScript部分
グローバル変数と定数を示します。
wwwroot\defender\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 |
// canvasのサイズ const CANVAS_WIDTH = 380; const CANVAS_HEIGHT = 560; // フィールドのサイズ const FIELD_WIDTH = 360; const FIELD_HEIGHT = 10000; // 各キャラクターのサイズ const PLAYER_SIZE = 40; const ENEMY_SIZE = 38; const BULLET_SIZE = 14; // canvas要素 const $canvas = document.getElementById('canvas'); const ctx = $canvas.getContext('2d'); // 接続状態の表示用 const $conectResult = document.getElementById('conect-result'); // ボタン const $start = document.getElementById('start'); const $up = document.getElementById('up'); const $down = document.getElementById('down'); const $left = document.getElementById('left'); const $right = document.getElementById('right'); const $shot = document.getElementById('shot'); // プレイヤー名 const $playerInfo = document.getElementById('player-info'); // ボタンの表示非表示を切り替えるチェックボックス const $showButton = document.getElementById('show-button'); // 卵のイメージ const eggImage = new Image(); // イメージを格納する配列 const playerUpImages = []; const playerDownImages = []; const bulletImages = []; const enemyImages = []; const sparkImages = []; // 星を描画するためのオブジェクトを格納する配列 const stars = []; // 効果音 const shotSound = new Audio('./sounds/shot.mp3'); const hitSound = new Audio('./sounds/hit2.mp3'); const upSound = new Audio('./sounds/up.mp3'); const deadSound = new Audio('./sounds/dead.mp3'); const gameoverSound = new Audio('./sounds/gameover.mp3'); // プレイ中かどうか? let isPlaying = false; // キー操作をしたときデフォルトの動作を抑止するかどうか? let preventDefault = false; |
レンジスライダーでボリューム変更
ボリュームを調整するための処理を示します。レンジスライダーを操作するとボリュームを変更できるようにしています。
wwwroot\defender\app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
const $volume = document.getElementById("volume"); const $volRange = document.getElementById("vol-range"); function playSound(){ gameoverSound.currentTime = 0; gameoverSound.play(); } $volume.addEventListener("change", function(){ setVolume($volume.value); }, false); function setVolume(value){ $volume.value = value; $volRange.textContent = value; shotSound.volume = value; hitSound.volume = value; upSound.volume = value; deadSound.volume = value; gameoverSound.volume = value; } |
Starクラスの定義
星を描画するためにStarクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Star { constructor(x, y, color){ this.X = x; this.Y = y; this.Color = color; } Draw(shiftY){ ctx.fillStyle = this.Color; ctx.fillRect(this.X, this.Y + shiftY, 3, 3); ctx.fillRect(this.X, this.Y + shiftY + FIELD_HEIGHT, 3, 3); ctx.fillRect(this.X, this.Y + shiftY - FIELD_HEIGHT, 3, 3); } } |
ページが読み込まれたときの処理
ページが読み込まれたら画像ファイルを読み込んでイメージを配列に格納します。
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 |
window.onload = () => { $canvas.width = CANVAS_WIDTH; $canvas.height = CANVAS_HEIGHT; // 卵のイメージ eggImage.src = './images/egg.png'; // 上向きのプレイヤー(添え字0は自分、1は自分以外のプレイヤー) for (let i = 1; i <= 2; i++) { const image = new Image(); image.src = './images/jiki-up' + i + '.png'; playerUpImages.push(image); } // 下向きのプレイヤー(添え字0は自分、1は自分以外のプレイヤー) for (let i = 1; i <= 2; i++) { const image = new Image(); image.src = './images/jiki-down' + i + '.png'; playerDownImages.push(image); } // 敵のイメージ for(let i=1; i<=3; i++){ const image = new Image(); image.src = './images/enemy' + i + '.png'; enemyImages.push(image); } // 火花のイメージ for(let i = 1; i <= 6; i++){ const image = new Image(); image.src = './images/spark' + i + '.png'; sparkImages.push(image) } // 弾丸のイメージ(添え字0は自分、1は敵と自分以外のプレイヤーが発射した弾丸) for (let i = 1; i <= 2; i++) { const image = new Image(); image.src = './images/bullet' + i + '.png'; bulletImages.push(image); } // 星の色 const arr = ['#f00', '#0f0', '#00f', '#ff0', '#f0f', '#0ff']; // 星をランダムに配置する for(let i = 0; i < 1000; i++) stars.push(new Star(Math.random() * FIELD_WIDTH, Math.random() * FIELD_HEIGHT, arr[i % 6])); // イベントリスナーの追加 $start.addEventListener('click', () => gameStart()); const arr1 = ['mousedown', 'touchstart']; const arr2 = ['mouseup', 'touchend']; for(let i = 0; i < 2; i++){ $up.addEventListener(arr1[i], (ev) => onPressButton(ev, 'up', true)); $down.addEventListener(arr1[i], (ev) => onPressButton(ev, 'down', true)); $left.addEventListener(arr1[i], (ev) => onPressButton(ev, 'left', true)); $right.addEventListener(arr1[i], (ev) => onPressButton(ev, 'right', true)); $shot.addEventListener(arr1[i], (ev) => onPressButton(ev, 'shot', true)); $up.addEventListener(arr2[i], (ev) => onPressButton(ev, 'up', false)); $down.addEventListener(arr2[i], (ev) => onPressButton(ev, 'down', false)); $left.addEventListener(arr2[i], (ev) => onPressButton(ev, 'left', false)); $right.addEventListener(arr2[i], (ev) => onPressButton(ev, 'right', false)); } document.addEventListener('mouseup', ev => { onPressButton(ev, 'up', false); onPressButton(ev, 'down', false); onPressButton(ev, 'left', false); onPressButton(ev, 'right', false); }) // ハブに接続する connection.start().catch((err) => { console.log("err"); }); // canvasを黒で塗りつぶす ctx.fillStyle = '#000'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); // プレイ中に操作用のボタンを表示する // チェックボックスの状態で切り替えることができるようにする setInterval(() => { let display = 'block'; // プレイ中でチェックされている場合だけ表示 if ($showButton.checked && isPlaying) { display = 'block'; } else display = 'none'; $up.style.display = display; $down.style.display = display; $left.style.display = display; $right.style.display = display; $shot.style.display = display; }, 100); // ボリュームの設定 setVolume(0.5); } |
キー操作で自機を操作できるようにする
追加するイベントリスナーを示します。プレイ中のみ(正確にはプレイ中とゲームオーバー直後の3秒間)’ArrowUp’, ‘ArrowDown’, ‘ArrowLeft’, ‘ArrowRight’, ‘Space’キーのデフォルトの動作を抑止します。’ArrowUp’を押下すると上へ加速、’ArrowDown’を押下すると下へ加速、’ArrowLeft’, ‘ArrowRight’を押下しているあいだは左右に移動するようにします。
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 |
// 押しっぱなしにすることで加速し続けないようにする let pressUp = false; let pressDown = false; document.onkeydown = (ev) => { if (preventDefault) { const codes = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space']; if (codes.filter(code => code == ev.code).length > 0) ev.preventDefault(); } // 左に移動開始 if (ev.code == 'ArrowLeft') { connection.invoke("SetVelocityX", 'left').catch((err) => { return console.error(err.toString()); }); } // 1回だけ上へ加速 if (ev.code == 'ArrowUp') { if (pressUp) return; pressUp = true; connection.invoke("AddVelocityY", 'up').catch((err) => { return console.error(err.toString()); }); } // 右に移動開始 if (ev.code == 'ArrowRight') { connection.invoke("SetVelocityX", 'right').catch((err) => { return console.error(err.toString()); }); } // 1回だけ下へ加速 if (ev.code == 'ArrowDown') { if (pressDown) return; pressDown = true; connection.invoke("AddVelocityY", 'down').catch((err) => { return console.error(err.toString()); }); } // 弾丸の発射 if (ev.code == 'Space') { connection.invoke("Shot").catch((err) => { return console.error(err.toString()); }); } } document.onkeyup = (ev) => { // 左右の移動停止 if (ev.code == 'ArrowLeft' || ev.code == 'ArrowRight') { connection.invoke("SetVelocityX", 'none').catch((err) => { return console.error(err.toString()); }); } // キーが離されたのでフラグをクリアする if (ev.code == 'ArrowUp') pressUp = false; if (ev.code == 'ArrowDown') pressDown = false; } |
マウス操作とタップで自機を操作できるようにする
追加するイベントリスナーを示します。
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 onPressButton(ev, button, isPressed){ ev.preventDefault(); if(button == 'up'){ if(isPressed) connection.invoke("AddVelocityY", 'up').catch((err) => {return console.error(err.toString())}); } if(button == 'down'){ if(isPressed) connection.invoke("AddVelocityY", 'down').catch((err) => {return console.error(err.toString())}); } if(button == 'left'){ if(isPressed) connection.invoke("SetVelocityX", 'left').catch((err) => { return console.error(err.toString())}); else connection.invoke("SetVelocityX", 'none').catch((err) => { return console.error(err.toString())}); } if(button == 'right'){ if(isPressed) connection.invoke("SetVelocityX", 'right').catch((err) => { return console.error(err.toString())}); else connection.invoke("SetVelocityX", 'none').catch((err) => { return console.error(err.toString())}); } if(button == 'shot'){ if(isPressed) connection.invoke("Shot").catch((err) => {return console.error(err.toString())}); } } |
ハブに接続したときの処理
接続に成功したらconnectionIDをグローバル変数に格納し、切断されたら空文字列を格納します。
1 2 3 4 5 6 7 8 9 10 11 12 |
let connectionID = ''; connection.on("SendToClientConnectionSuccessful", (id) => { connectionID = id; $conectResult.innerHTML = `接続完了:${id}`; }); connection.onclose( () => { connectionID = ''; if($conectResult != null){ $conectResult.innerHTML = '切断'; } }); |
ゲーム開始時の処理
ゲーム開始ボタンがクリックされたらサーバーサイドから”GameStart”メソッドを呼び出します。このときユーザーが設定したプレイヤー名もいっしょに送信します。
1 2 3 4 5 6 |
function gameStart(){ const playerName = document.getElementById('player-name').value; connection.invoke("GameStart", playerName).catch((err) => { return console.error(err.toString()); }); } |
ゲームの開始処理が成功したらサーバーサイドから”SendToClientGameStartSuccessful”が送られてくるので、そのときはスタートボタンとプレイヤー名設定用のテキストボックスを非表示にします。またisPlayingフラグとpreventDefaultフラグをセットします。
失敗したときはプレイヤー数の上限を超えているということなので、ゲームに参加できない旨を表示させます。
1 2 3 4 5 6 7 8 9 10 |
connection.on("SendToClientGameStartSuccessful", () => { $start.style.display = 'none'; $playerInfo.style.display = 'none'; isPlaying = true; preventDefault = true; // プレイ中はデフォルトの動作を抑止 }); connection.on("SendToClientGameStartFailure", () => { $conectResult.innerHTML = '同時プレイできるユーザーの上限を超えています。'; }); |
プレイヤーの描画
サーバーサイドから送られてきた文字列からプレイヤーを描画する処理を示します。
文字列を改行で分割した場合、最初の部分がプレイヤーに関する情報です。座標、スコア、方向、残機数などプレイヤーの情報はカンマ区切りの文字列になっていて、複数のプレイヤーの情報がタブ文字区切りで送られてくるので、これを解析します。自分自身であれば自機として描画します。それ以外のプレイヤーは別のイメージを使って描画します。
自機を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 59 60 61 62 63 64 65 66 67 |
function drawPlayers(text, shiftY) { const arr1 = text.split('\n'); const arr2 = arr1[0].split('\t'); const players = []; for (let i = 0; i < arr2.length; i++) { if (arr2[i] == '') continue; const arr3 = arr2[i].split(','); const isDead = arr3[7] == 'true' ? true : false; const isShow = arr3[8] == 'true' ? true : false; const player = { name: arr3[0], score: Number(arr3[1]), id: arr3[2], x: Number(arr3[3]), y: Number(arr3[4]), direct: arr3[5], life: arr3[6], isDead: isDead, isShow: isShow }; players.push(player); } // ユーザー自身の情報が存在しないのであればプレイ中ではない if (players.filter(player => player.id == connectionID).length == 0) { ctx.fillStyle = '#fff'; ctx.textBaseline = 'top'; ctx.font = '28px Arial'; ctx.fillText('GAME OVER', 100, 200); drawBest3(text); // 上位3名のプレイヤー名とスコアを描画(後述) return; } for (let i = 0; i < players.length; i++) drawPlayer(players[i], shiftY); } function drawPlayer(player, shiftY) { if (player.isDead == true) return; ctx.fillStyle = '#fff'; ctx.font = '16px Arial'; // プレイヤーがユーザー自身かどうかで自機のイメージを変える const index = player.id == connectionID ? 0 : 1; // 自機は上向き?下向き? let playerImage = playerUpImages[index]; if (player.direct == 'Down') playerImage = playerDownImages[index]; // プレイヤーの座標がワープした位置にある場合があるので3箇所に描画する const arr = [0, FIELD_HEIGHT, -FIELD_HEIGHT]; for (let k = 0; k < 3; k++) { if (player.isShow == true) ctx.drawImage(playerImage, player.x, player.y + shiftY + arr[k], PLAYER_SIZE, PLAYER_SIZE); ctx.fillText(player.name, player.x + 50, player.y + shiftY + arr[k]) ctx.fillText(player.score, player.x + 50, player.y + shiftY + 18 + arr[k]) } } |
敵を描画する
サーバーサイドから送られてきた文字列から敵を描画する処理を示します。
文字列を改行で分割した場合、3番目の部分が敵に関する情報です。敵の座標や状態がカンマ区切りの文字列になっていて、複数の敵の情報がタブ文字区切りで送られてくるので、これを解析して適切な位置に描画します。
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 |
function drawEnemies(text, shiftY) { const enemyXs = []; const enemyYs = []; const enemyTypes = []; const enemyJustBorns = []; const arr1 = text.split('\n'); const arr2 = arr1[2].split('\t'); for (let i = 0; i < arr2.length; i++) { if (arr2[i] == '') continue; const arr3 = arr2[i].split(','); enemyXs.push(Number(arr3[0])); enemyYs.push(Number(arr3[1])); enemyTypes.push(Number(arr3[2])); enemyJustBorns.push(arr3[3]); } for (let i = 0; i < enemyXs.length; i++) { const arr = [0, FIELD_HEIGHT, -FIELD_HEIGHT]; let image = enemyImages[enemyTypes[i]]; if (enemyJustBorns[i] == 'true') image = eggImage; for (let k = 0; k < 3; k++) ctx.drawImage(image, enemyXs[i], enemyYs[i] + shiftY + arr[k], ENEMY_SIZE, ENEMY_SIZE); } } |
弾丸の描画
サーバーサイドから送られてきた文字列から弾丸を描画する処理を示します。
文字列を改行で分割した場合、2番目の部分がプレイヤーが発射した弾丸で、4番目が敵が発射した弾丸の情報です。敵と他のプレイヤーが発射した弾丸は自分が発射した弾丸とは別のイメージで描画します。これも座標や状態がカンマ区切りの文字列になっていて、複数の敵の情報がタブ文字区切りで送られてくるので、これを解析して適切な位置に描画します。
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 |
function drawBullets(text, shiftY) { const bullet1Xs = []; const bullet1Ys = []; const bullet2Xs = []; const bullet2Ys = []; const bullet2UpdateCounts = []; const arr1 = text.split('\n'); // プレイヤーが発射した弾丸 const arr2 = arr1[1].split('\t'); for (let i = 0; i < arr2.length; i++) { if (arr2[i] == '') continue; const arr3 = arr2[i].split(','); // 自分が発射した弾丸とそうでないものにわける if (arr3[0] == connectionID) { bullet1Xs.push(Number(arr3[1])); bullet1Ys.push(Number(arr3[2])); } else { bullet2Xs.push(Number(arr3[1])); bullet2Ys.push(Number(arr3[2])); bullet2UpdateCounts.push(Number(arr3[3])); } } // 敵が発射した弾丸 const arr4 = arr1[3].split('\t'); for (let i = 0; i < arr4.length; i++) { if (arr4[i] == '') continue; const arr5 = arr4[i].split(','); bullet2Xs.push(Number(arr5[0])); bullet2Ys.push(Number(arr5[1])); bullet2UpdateCounts.push(Number(arr5[2])); } // 自分が発射した弾丸を描画する for (let i = 0; i < bullet1Xs.length; i++) { const arr = [0, FIELD_HEIGHT, -FIELD_HEIGHT]; for (let k = 0; k < 3; k++) ctx.drawImage(bulletImages[0], bullet1Xs[i], bullet1Ys[i] + shiftY + arr[k], BULLET_SIZE, BULLET_SIZE); } // それ以外の弾丸を描画する for (let i = 0; i < bullet2Xs.length; i++) { const arr = [0, FIELD_HEIGHT, -FIELD_HEIGHT]; for (let k = 0; k < 3; k++) { // 敵が発射した弾丸であることをわかりやすくするため時間によって大きさを変える if (bullet2UpdateCounts[i] % 32 < 16) ctx.drawImage(bulletImages[1], bullet2Xs[i], bullet2Ys[i] + shiftY + arr[k], BULLET_SIZE, BULLET_SIZE); else { ctx.drawImage(bulletImages[1], bullet2Xs[i] - 2, bullet2Ys[i] + shiftY + arr[k] - 2, BULLET_SIZE + 4, BULLET_SIZE + 4); } } } } |
火花を描画する
サーバーサイドから送られてきた文字列から火花を描画する処理を示します。
文字列を改行で分割した場合、5番目の部分が火花の情報です。これも座標や状態がカンマ区切りの文字列になっていて、複数の火花の情報がタブ文字区切りで送られてくるので、これを解析して適切な位置に描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function drawSparks(text, shiftY) { const sparkXs = []; const sparkYs = []; const sparkTypes = []; const arr0 = text.split('\n'); const arr1 = arr0[4].split('\t'); for (let i = 0; i < arr1.length; i++) { if (arr1[i] == '') continue; const arr2 = arr1[i].split(','); sparkXs.push(Number(arr2[0])); sparkYs.push(Number(arr2[1])); sparkTypes.push(Number(arr2[2])); } for (let i = 0; i < sparkXs.length; i++) { const arr = [0, FIELD_HEIGHT, -FIELD_HEIGHT]; for (let k = 0; k < 3; k++) ctx.drawImage(sparkImages[sparkTypes[i]], sparkXs[i], sparkYs[i] + shiftY + arr[k], ENEMY_SIZE, ENEMY_SIZE); } } |
スコアの表示
スコアを表示する処理を示します。プレイヤーを描画するときに使用した文字列を使って、プレイヤーのスコアのなかからユーザー自身のものを取り出して描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function drawScore(text) { let score = 0; let life = 0; const arr0 = text.split('\n'); const arr1 = arr0[0].split('\t'); for (let i = 0; i < arr1.length; i++) { if (arr1[i] == '') continue; const arr2 = arr1[i].split(','); if (connectionID == arr2[2]) { score = Number(arr2[1]); life = arr2[6]; } } ctx.fillStyle = '#fff'; ctx.textBaseline = 'top'; ctx.font = '20px Arial'; ctx.fillText('SCORE ' + score, 10, 10); ctx.fillText('LIFE ' + life, 250, 10); } |
更新処理
フィールド上に存在するキャラクターをどれだけ垂直方向にズラして描画するのかを求める処理を示します。
ユーザー自身のプレイヤーを中央付近に描画するのですが、進行方向によって中央から100ピクセル上下にズラします。また進行方向を反転したとき急に描画される座標が変わらないようにゆっくり変化させます。そのためグローバル変数curYを定義しています。死亡時はゆっくりcurYを0に戻します。
もし戻り値がnullである場合はユーザー自身はプレイをしていないことになります。
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 |
let curY = 0; // 中央から上下にどれだけズラすか? function getShiftY(text) { const arr1 = text.split('\n'); let playerY = 0; let playerDead = ''; let playerDirect = ''; let isGet = false; const arr2 = arr1[0].split('\t'); for (let i = 0; i < arr2.length; i++) { if (arr2[i] == '') continue; const arr3 = arr2[i].split(','); if (arr3[2] == connectionID) { playerDead = arr3[7]; playerDirect = arr3[5]; playerY = Number(arr3[4]); isGet = true; break; } } if (!isGet) return null; let shiftY = - playerY + CANVAS_HEIGHT / 2; if (playerDead == 'false') { if (playerDirect == 'Down' && curY > -100) curY -= 1; if (playerDirect == 'Up' && curY < 100) curY += 1; } else { if (curY > 0) curY -= 1; if (curY < 0) curY += 1; } shiftY += curY; return shiftY; } |
canvasの下部にプレイ中の上位3名の名前とスコアを描画します。
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 |
function drawBest3(text) { const scoreInfos = []; const arr0 = text.split('\n'); // プレイヤーの座標 const arr1 = arr0[0].split('\t'); for (let i = 0; i < arr1.length; i++) { if (arr1[i] == '') continue; const arr2 = arr1[i].split(','); scoreInfos.push({ name: arr2[0], score: Number(arr2[1]), life: Number(arr2[6]), }); } // スコアを大きい順にならべて要素が3より大きいときは3つだけ取る scoreInfos.sort((a, b) => b.score - a.score); if (scoreInfos.length > 3) scoreInfos.length = 3; // スコアを小さい順にならべる(下に空白ができないように下位から下詰めで描画したい) scoreInfos.sort((a, b) => a.score - b.score); // ゲームオーバー時に文字のうえに文字が描画されないようにBEST3のスコアが描画される領域だけクリアする if (!isPlaying && scoreInfos.length > 0) { ctx.fillStyle = '#000'; ctx.fillRect(0, CANVAS_HEIGHT - 30 - 24 * (scoreInfos.length), CANVAS_WIDTH, CANVAS_HEIGHT); } // プレイヤー名(残機)とスコアを描画する ctx.fillStyle = '#fff'; ctx.textBaseline = 'top'; ctx.font = '16px Arial'; for (let i = scoreInfos.length - 1; i >= 0; i--) { const text = `${scoreInfos[i].score} (${scoreInfos[i].life}) ${scoreInfos[i].name}`; ctx.fillText(text, 100, CANVAS_HEIGHT - 30 - 24 * i); } if (scoreInfos.length > 0) ctx.fillText('BEST', 100, CANVAS_HEIGHT - 30 - 24 * (scoreInfos.length)); } |
サーバーサイドから文字列が送られてきたら上記の関数をつかって描画処理をおこないます。
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 |
connection.on("Update", (text) => { let shiftY = getShiftY(text); // ユーザー自身はプレイをしていない = ゲームオーバー時 if (shiftY == null) { ctx.fillStyle = '#fff'; ctx.textBaseline = 'top'; ctx.font = '28px Arial'; ctx.fillText('GAME OVER', 100, 200); drawBest3(text); return; } ctx.fillStyle = '#000'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); stars.forEach(star => star.Draw(shiftY)); drawBullets(text, shiftY); drawSparks(text, shiftY); drawPlayers(text, shiftY); drawEnemies(text, shiftY); drawScore(text); drawBest3(text); }); |
効果音を鳴らす
スピードアップや弾丸の発射をしたときは効果音を鳴らします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// スピードアップ connection.on("SendToClientSpeedUp", () => { upSound.currentTime = 0; upSound.play(); }); // 弾丸の発射 connection.on("SendToClientShot", () => { shotSound.currentTime = 0; shotSound.play(); }); // 敵を倒した connection.on("SendToClientHitEnemy", () => { hitSound.currentTime = 0; hitSound.play(); }); // 敵にやられた connection.on("SendToClientPlayerDead", () => { deadSound.currentTime = 0; deadSound.play(); }); |
ゲームオーバー時の処理
ゲームオーバー時はisPlayingフラグをクリアしてゲームオーバー時の効果音を鳴らします。その3秒後にスタートボタンを表示させ、キー操作時のデフォルトの動作を抑制するフラグをクリアします。
1 2 3 4 5 6 7 8 9 10 11 12 |
connection.on("SendToClientPlayerGameOvered", () => { isPlaying = false; gameoverSound.currentTime = 0; gameoverSound.play(); setTimeout(() => { $start.style.display = 'block'; $playerInfo.style.display = 'block'; preventDefault = false; }, 3000); }); |