ボスコニアンのようなオンライン対戦ゲームをつくる(4)の続きです。今回はクライアントサイドの処理を実装します。
Contents
cshtml部分
まずcshtmlファイルを作成してPagesフォルダの配下におきます。
Pages\bosconian\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 |
@page @{ string baseurl = "https://lets-csharp.com/samples/2204/aspnetcore-app-zero"; // アプリを公開したいurl Layout = null; } <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>ボスコニアン - 鳩でもわかるASP.NET Core</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script> <link rel="stylesheet" type="text/css" href="@baseurl/bosconian/style.css"> </head> <body> <div id = "container"> <div id = "field"> <div><canvas id="canvas"></canvas></div> <div><canvas id="canvas2"></canvas></div> <p> 音量: <input type="range" id="volume" min="0" max="1" step="0.01"> <span id="vol_range"></span> <button onclick="playSound()">音量テスト</button> </p> <button id="start" class="button">スタート</button> <button id="up" class="button">↑</button> <button id="up-left" class="button"></button> <button id="left" class="button">←</button> <button id="down-left" class="button"></button> <button id="down" class="button">↓</button> <button id="down-right" class="button"></button> <button id="right" class="button">→</button> <button id="up-right" class="button"></button> <button id="shot" class="button">SHOT</button> <div> <label>ハンドルネーム</label> <input type="text" id="player-name" maxlength='16' /> </div> <div> <label><input type="checkbox" id="hide-buttons">スマホ用操作ボタンを非表示にする</label> <p>PCは↑↓←→で上下左右、Zキーで45度左回転 Xキーで45度右回転できます。</p> </div> <p><a href="./hi-score">トップ30を見る</a></p> <p id = "conect-result"></p> </div><!--/#field--> </div><!--/#container--> <script> let connection = new signalR.HubConnectionBuilder().withUrl("@baseurl/BosconianHub").build(); </script> <script src="@baseurl/bosconian/app.js"></script> </body> </html> |
cssを示します。
wwwroot\bosconian\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 |
body { background-color: #000; color: #fff; font-family: "MS ゴシック"; } #container { width: 360px; } #field { position: relative; overflow: hidden; } .display-none { display: none; } .button { position: absolute; background-color: transparent; color: white; width: 100px; height: 60px; } #start { left: 130px; top: 230px; } #up, #up-left, #up-right { top: 360px; } #left, #right, #shot { top: 430px; } #down, #down-left, #down-right { top: 500px; } #left, #up-left, #down-left { left: 0px; } #up, #down, #shot { left: 130px; } #right, #up-right, #down-right { left: 260px; } #conect-result{ color: white; } #hide-buttons { margin-top: 20px; } |
その他、描画に使う自機、他のプレイヤーの機体、敵機、要塞、砲台などの画像はwwwroot\bosconian\imagesフォルダ内に、効果音はwwwroot\bosconian\soundsフォルダ内に配置します。
JavaScript部分
次にJavaScript部分を示します。
主なグローバル変数と定数
まずは主なグローバル変数と定数です。今回はcanvas要素を2つ使います。ひとつはフィールドの描画用、もうひとつはプレイヤーと要塞の位置を表示するレーダーと上位3名のスコアの描画用です。
wwwroot\bosconian\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 |
const $conectResult = document.getElementById('conect-result'); // canvasのサイズ const CANVAS_WIDTH = 360; const CANVAS_HEIGHT = 360; const CANVAS_MAP_HEIGHT = 200; // 各キャラクターのサイズ const CHARACTER_SIZE = 32; const BULLET_SIZE = 14; // ボタン const $start = document.getElementById('start'); const $up = document.getElementById('up'); const $upleft = document.getElementById('up-left'); const $upright = document.getElementById('up-right'); const $down = document.getElementById('down'); const $downleft = document.getElementById('down-left'); const $downright = document.getElementById('down-right'); const $left = document.getElementById('left'); const $right = document.getElementById('right'); const $shot = document.getElementById('shot'); // 効果音とBGM const soundShot = new Audio('./sounds/shot.mp3'); const soundHitEnemy = new Audio('./sounds/hit-enemy.mp3'); const soundHitCore = new Audio('./sounds/hit-core.mp3'); const soundPlayerDead = new Audio('./sounds/player-dead.mp3'); const soundGameOver = new Audio('./sounds/gameover.mp3'); const bgm = new Audio('./sounds/bgm.mp3'); // 描画用のイメージを格納する配列 const enemyImages = []; // 3種類の敵 const playerImages1 = []; // 自機(8方向) const playerImages2 = []; // 他のプレイヤーの機体(8方向) const sparkImages = []; // 爆発時の火花(6種類) // 描画用のイメージ(敵弾、砲台、要塞の中心部) const enemyBulletImage = new Image(); const cannonImage = new Image(); const coreImage = new Image(); // Starオブジェクトを格納する配列 const stars = []; // canvas要素とコンテキスト(フィールド描画用) const $canvas = document.getElementById('canvas'); const ctx = $canvas.getContext('2d'); // canvas要素とコンテキスト(レーダーと上位3位のスコア描画用) const $canvasMap = document.getElementById('canvas2'); const ctxMap = $canvasMap.getContext('2d'); let fieldSize = 0; // フィールドのサイズ let connectionID = ''; // 接続ID let isPlaying = false; // 現在プレイ中か? let bgmIntervalId = null; // BGMを繰り返し再生するためのインターバルのID const playerCtrlButtons = []; // 自機操作用のボタン要素を格納する配列 |
Starクラスの定義
背景に描画する星の位置情報を格納するStarクラスを定義します。
wwwroot\bosconian\app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Star { constructor(x, y, color){ this.X = x; this.Y = y; this.Color = color; } Draw(shiftX, shiftY){ ctx.fillStyle = this.Color; const arr = [0, fieldSize, -fieldSize]; for(let i=0; i<arr.length; i++){ for(let k=0; k<arr.length; k++) ctx.fillRect(this.X - shiftX + arr[i], this.Y - shiftY + arr[k], 3, 3); } } } |
ページが読み込まれたときの処理
ページが読み込まれたときの処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
window.onload = () => { $canvasMap.style.display = 'none'; initButtons(); // 後述 initImages(); // 後述 addEventListeners(); // 後述 $canvas.width = CANVAS_WIDTH; $canvas.height = CANVAS_HEIGHT; $canvasMap.width = CANVAS_WIDTH; $canvasMap.height = CANVAS_MAP_HEIGHT; connection.start().catch((err) => { document.getElementById("conect-result").innerHTML = '接続失敗'; }); initVolumes(0.5); // 後述 } |
initButtons関数は自機操作用のボタンを初期化します。また0.5秒おきに現在プレイ中か?チェックボックスでボタンは表示する設定になっているかを確認してボタンの表示/非表示を切り替えています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function initButtons(){ playerCtrlButtons.push($up); playerCtrlButtons.push($upleft); playerCtrlButtons.push($upright); playerCtrlButtons.push($down); playerCtrlButtons.push($downleft); playerCtrlButtons.push($downright); playerCtrlButtons.push($left); playerCtrlButtons.push($right); playerCtrlButtons.push($shot); setInterval(() => { const display = isPlaying && !document.getElementById('hide-buttons').checked ? 'block' : 'none'; playerCtrlButtons.forEach(btn => { btn.style.display = display; }); }, 500); } |
initImages関数は画像ファイルを読み込んでイメージを変数、配列に格納します。
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 |
function initImages(){ const arr1 = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw']; // 8方向の自機のイメージ for(let i = 0; i < arr1.length; i++){ const $image = new Image(); $image.src = `./images/jiki1-${arr1[i]}.png`; playerImages1.push($image); } // 他のプレイヤーのイメージ for(let i = 0; i < arr1.length; i++){ const $image = new Image(); $image.src = `./images/jiki2-${arr1[i]}.png`; playerImages2.push($image); } // 敵のイメージ const arr2 = ['./images/enemy1.png', './images/enemy2.png', './images/enemy3.png']; for(let i = 0; i < arr2.length; i++){ const $image = new Image(); $image.src = arr2[i]; enemyImages.push($image); } // 敵弾、要塞の中心部と砲台のイメージ enemyBulletImage.src = './images/enemy-bullet.png'; coreImage.src = './images/core.png'; cannonImage.src = './images/cannon.png'; // 火花のイメージ for(let i = 1; i <= 6; i++){ const image = new Image(); image.src = './images/spark' + i + '.png'; sparkImages.push(image) } } |
addEventListeners関数はイベントリスナーを追加します。
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 |
function addEventListeners(){ const types = ['mousedown', 'touchstart']; // PC用とスマホ用 for(let i = 0; i < types.length; i++){ $up.addEventListener(types[i], (ev) => touchButton(ev, 'up')); $down.addEventListener(types[i], (ev) => touchButton(ev, 'down')); $left.addEventListener(types[i], (ev) => touchButton(ev, 'left')); $right.addEventListener(types[i], (ev) => touchButton(ev, 'right')); $upleft.addEventListener(types[i], (ev) => touchButton(ev, 'upleft')); $upright.addEventListener(types[i], (ev) => touchButton(ev, 'upright')); $downleft.addEventListener(types[i], (ev) => touchButton(ev, 'downleft')); $downright.addEventListener(types[i], (ev) => touchButton(ev, 'downright')); $shot.addEventListener(types[i], (ev) => touchButton(ev, 'shot')); } // PCのキー操作 document.addEventListener('keydown', (ev) => { if(ev.code == 'Space') touchButton(ev, 'shot'); if(ev.code == 'ArrowUp') touchButton(ev, 'up'); if(ev.code == 'ArrowDown') touchButton(ev, 'down'); if(ev.code == 'ArrowLeft') touchButton(ev, 'left'); if(ev.code == 'ArrowRight') touchButton(ev, 'right'); if(ev.code == 'KeyZ') touchButton(ev, 'rotateLeft'); if(ev.code == 'KeyX') touchButton(ev, 'rotateRight'); }); // スタートボタンをクリックしたらゲームスタート $start.addEventListener('click', (ev) => gameStart(ev)); function touchButton(ev, button){ // プレイ中でなければなにもしない if(!isPlaying) return; // プレイ中はデフォルトの動作を抑止する ev.preventDefault(); // ユーザーの入力をサーバーサイドに送信する if(button != 'shot') connection.invoke("ChangeDirect", button, getPlayerName()); else connection.invoke("Shot", getPlayerName()); } } |
initVolumes関数はレンジスライダーを操作することでボリュームをコントロールできるようにします。
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 |
function initVolumes(initValue){ const $elemVolume = document.getElementById("volume"); const $elemRange = document.getElementById("vol_range"); $elemVolume.addEventListener('change', () => setVolume($elemVolume.value)); setVolume(initValue); function setVolume(value){ $elemVolume.value = value; $elemRange.textContent = value; soundShot.volume = value; soundHitEnemy.volume = value; soundHitCore.volume = value; soundPlayerDead.volume = value; bgm.volume = value; soundGameOver.volume = value; } } function playSound(){ soundGameOver.currentTime = 0; soundGameOver.play(); } |
接続成功時の処理
ASP.NET SignalRでサーバーサイドへの接続が成功したら”SendToClientConnectionSuccessful”が送られてくるので、IDを保存しておきます。また星をランダムに生成して配列に格納します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
connection.on("SendToClientConnectionSuccessful", (id, size) => { connectionID = id; $conectResult.innerHTML = `接続成功:${id}`; fieldSize = Number(size); // 星の色 const arr = ['#f00', '#0f0', '#00f', '#ff0', '#f0f', '#0ff']; // 星をランダムに配置する for(let i = 0; i < 1000; i++) stars.push(new Star(Math.random() * fieldSize, Math.random() * fieldSize, arr[i % 6])); }); |
ゲーム開始時の処理
ゲーム開始時の処理を示します。
プレイヤー名を取得して、これを引数にしてサーバーサイドからGameStartメソッドを呼び出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function gameStart(ev){ ev.preventDefault(); if(connectionID == '') return; connection.invoke("GameStart", getPlayerName()).catch((err) => { return console.error(err.toString()); }); } function getPlayerName(){ let playerName = document.getElementById('player-name').value; if (playerName == '') playerName = '名無しさん'; return playerName; } |
ゲーム開始の処理が正常に行なわれたら”SendToClientGameStartSuccessful”が送られてくるので、そのときはスタートボタンを非表示にしてBGMの再生を開始します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
connection.on("SendToClientGameStartSuccessful", () => { clearInterval(bgmIntervalId); isPlaying = true; $canvasMap.style.display = 'block'; $start.style.display = 'none'; bgm.pause(); bgm.currentTime = 0; bgm.play(); bgmIntervalId = setInterval(() => { bgm.pause(); if(isPlaying){ bgm.currentTime = 0; bgm.play(); } }, 45 * 1000); // 使用している音源は45秒まで再生したら開始地点に戻して再生するのがよいらしい // 使用している音源によって各自調整してください }); |
ゲーム開始の処理が正常に行なわれなかった場合は”SendToClientGameStartFailure”が送られてくるので、そのときはゲームの開始処理が失敗した旨を表示します。
1 2 3 |
connection.on("SendToClientGameStartFailure", () => { $conectResult.innerHTML = '定員オーバーなのでゲームに参加できません'; }); |
通信が切れたらその旨を表示します。
1 2 3 4 5 6 |
connection.onclose( () => { connectionID = ''; if($conectResult != null){ $conectResult.innerHTML = '通信が切断されました'; } }); |
更新処理
更新時の処理を示します。
データが更新されたときはサーバーサイドから”SendToClientUpdate”とjsonテキストが送られてくるので、これをオブジェクトに変換して描画処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
connection.on("SendToClientUpdate", (json) => { const data = JSON.parse(json); // 自機の座標から全体をどれだけずらして描画すればいいかを計算する let position = getMyPosition(data); let centerX = CANVAS_WIDTH / 2; let centerY = CANVAS_HEIGHT / 2; let shiftX = position.x - centerX; let shiftY = position.y - centerY; // 以下の関数は後述 clearCanvas(); stars.forEach(star => star.Draw(shiftX, shiftY)) drawBullets(data, shiftX, shiftY); drawFortresses(data, shiftX, shiftY); drawEnemies(data, shiftX, shiftY); drawPlayer(data, shiftX, shiftY); drawSparks(data, shiftX, shiftY); drawScore(data); drawHighScores(data); }); |
clearCanvas関数はcanvas全体をいったん黒で塗りつぶします。またレーダーを表示する矩形部分を青で塗りつぶします。
1 2 3 4 5 6 7 8 9 10 |
function clearCanvas(){ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); ctxMap.fillStyle = '#000'; ctxMap.fillRect(0, 0, $canvasMap.width, $canvasMap.height); ctxMap.fillStyle = '#00f'; ctxMap.fillRect(0, 0, 200, 200); } |
getMyPosition関数はフィールド上の自機の座標を取得します。
1 2 3 4 5 6 7 8 9 10 |
function getMyPosition(data){ for(let i = 0; i < data.Players.length; i++){ if(data.Players[i].ConnectionId == connectionID){ return { x: Number(data.Players[i].X), y: Number(data.Players[i].Y), }; } } } |
弾丸の描画
drawBullets関数は弾丸を描画する処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function drawBullets(data, shiftX, shiftY){ for(let i = 0; i < data.Players.length; i++){ if(data.Players[i].ConnectionId == connectionID) drawMyBullets(data.Players[i].Bullets, shiftX, shiftY); // 自機の弾丸 else drawEnemyBullets(data.Players[i].Bullets, shiftX, shiftY); // 他のプレイヤーの弾丸 } // 敵の弾丸 for(let i = 0; i < data.Enemies.length; i++) drawEnemyBullets(data.Enemies[i].Bullets, shiftX, shiftY); // 敵の要塞からの弾丸 for(let i = 0; i < data.Fortresses.length; i++) drawEnemyBullets(data.Fortresses[i].Bullets, shiftX, shiftY); } |
自機から発射された弾丸を白い線で描画します。このとき弾丸のXY方向の速度でどの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 28 29 30 31 32 33 34 35 36 |
function drawMyBullets(bullets, shiftX, shiftY){ const arr = [0, fieldSize, -fieldSize]; for(let i = 0; i < bullets.length; i++){ const bullet = bullets[i]; let x1 = bullet.X; let x2 = bullet.X; let y1 = bullet.Y; let y2 = bullet.Y; if(bullet.VX > 0){ x1 += 10; x2 -= 10; } if(bullet.VX < 0){ x1 -= 10; x2 += 10; } if(bullet.VY > 0){ y1 += 10; y2 -= 10; } if(bullet.VY < 0){ y1 -= 10; y2 += 10; } for(let k = 0; k < arr.length; k++){ for(let m = 0; m < arr.length; m++){ ctx.lineWidth = 2; ctx.strokeStyle = '#fff'; ctx.beginPath(); ctx.moveTo(x1 - shiftX + arr[k], y1 - shiftY + arr[m]); ctx.lineTo(x2 - shiftX + arr[k], y2 - shiftY + arr[m]); ctx.stroke(); } } } } |
敵と他のプレイヤーが発射した弾丸を描画する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function drawEnemyBullets(bullets, shiftX, shiftY){ const arr = [0, fieldSize, -fieldSize]; for(let i = 0; i < bullets.length; i++){ const bullet = bullets[i]; for(let k = 0; k < arr.length; k++){ for(let m = 0; m < arr.length; m++){ // 座標がcanvas内でなければ描画処理はおこなわない const x = bullet.X - BULLET_SIZE / 2 - shiftX + arr[k]; const y = bullet.Y - BULLET_SIZE / 2 - shiftY + arr[m]; if(0 <= x && x < CANVAS_WIDTH && 0 <= y && y < CANVAS_HEIGHT ) ctx.drawImage(enemyBulletImage, x, y, BULLET_SIZE, BULLET_SIZE); } } } } |
要塞の描画
要塞を描画する処理を示します。生成されたばかりの要塞は点滅処理をさせるのでIsJustBornプロパティがtrueの場合は2回に1回しか描画しません。ちょっと処理が長くなったので、中心部分の描画と砲台部分の描画処理をわけました。
1 2 3 4 5 6 7 8 9 10 11 |
function drawFortresses(data, shiftX, shiftY){ for(let i = 0; i < data.Fortresses.length; i++){ const fortress = data.Fortresses[i]; if(fortress.IsJustBorn && fortress.UpdateCount % 2) continue; drawFortressCores(fortress, shiftX, shiftY); // 中心部分と砲台をつなぐ部分の描画 drawFortressCannons(fortress, shiftX, shiftY); // 砲台部分の描画 blotMap(fortress.X, fortress.Y, '#0f0'); // 要塞の位置をレーダー部分に描画 } } |
中心部分と砲台をつなぐ部分の描画する処理を示します。
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 |
function drawFortressCores(fortress, shiftX, shiftY){ const arr = [0, fieldSize, -fieldSize]; for(let i = 0; i < arr.length; i++){ for(let k = 0; k < arr.length; k++){ ctx.lineWidth = 8; ctx.strokeStyle = '#080'; // 中心から砲台がある座標に直線を描画する ctx.beginPath(); const centerX = fortress.X - shiftX + arr[i]; const centerY = fortress.Y - shiftY + arr[k]; for(let m =0; m < 6; m++){ ctx.moveTo(centerX, centerY); ctx.lineTo(centerX + 48 * Math.cos(Math.PI / 3 * m), centerY + 48 * Math.sin(Math.PI / 3 * m)); } // 中心部分を囲うように上下が欠けた円を描画する ctx.stroke(); ctx.beginPath(); ctx.arc(centerX, centerY, 24, Math.PI * 2 / 3, Math.PI * 4 / 3); ctx.stroke(); ctx.beginPath(); ctx.arc(centerX, centerY, 24, Math.PI * 5 / 3, Math.PI * 1 / 3); ctx.stroke(); // 中心部分を空洞にするため黒で塗りつぶす ctx.beginPath(); ctx.arc(centerX, centerY, 20, 0, Math.PI * 2); ctx.fillStyle = '#000'; ctx.fill(); // 中心部分のキャラクターを描画する ctx.drawImage(coreImage, centerX - CHARACTER_SIZE / 2, centerY - CHARACTER_SIZE / 2, CHARACTER_SIZE, CHARACTER_SIZE); } } } |
砲台部分を描画する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function drawFortressCannons(fortress, shiftX, shiftY){ const arr = [0, fieldSize, -fieldSize]; for(let i = 0; i < fortress.Cannons.length; i++){ const cannon = fortress.Cannons[i]; for(let k = 0; k < arr.length; k++){ for(let m = 0; m < arr.length; m++){ const centerX = cannon.X - shiftX + arr[k]; const centerY = cannon.Y - shiftY + arr[m]; ctx.drawImage(cannonImage, centerX - CHARACTER_SIZE / 2, centerY - CHARACTER_SIZE / 2, CHARACTER_SIZE, CHARACTER_SIZE); } } } } |
レーダーへの表示
blotMap関数はレーダーに自機と他のプレイヤー、要塞の位置を描画します。
1 2 3 4 |
function blotMap(x, y, color){ ctxMap.fillStyle = color; ctxMap.fillRect(x / fieldSize * 200 - 2, y / fieldSize * 200 - 2, 5, 5); } |
敵の描画
敵も生成されたばかりのものは点滅表示させるのでIsJustBornプロパティがtrueのときは2回に1回の描画とします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function drawEnemies(data, shiftX, shiftY){ const arr = [0, fieldSize, -fieldSize]; for(let i = 0; i < data.Enemies.length; i++){ const enemy = data.Enemies[i]; if(enemy.IsJustBorn && enemy.UpdateCount % 2) continue; for(let k = 0; k < arr.length; k++){ for(let m = 0; m < arr.length; m++){ const centerX = enemy.X - shiftX + arr[k]; const centerY = enemy.Y - shiftY + arr[m]; ctx.drawImage(enemyImages[enemy.Type], centerX - CHARACTER_SIZE / 2, centerY - CHARACTER_SIZE / 2, CHARACTER_SIZE, CHARACTER_SIZE); } } } } |
プレイヤーの描画
プレイヤーを描画する処理を示します。死亡したプレイヤーは描画しません。機体のXY方向の移動速度から機体の向きを調べて適切なイメージを使用します。
死亡状態から復帰したばかりのプレイヤーは点滅表示させますが、ここではIsShowプロパティを使って描画するかどうかを判断します。
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 |
function drawPlayer(data, shiftX, shiftY){ for(let i = 0; i < data.Players.length; i++){ const player = data.Players[i]; if(player.IsDead) continue; const x = player.X - shiftX; const y = player.Y - shiftY; // 機体のXY方向の移動速度からどのイメージを使うかを調べる const arr = [ [7, 0, 1], [6, 0, 2], [5, 4, 3], ]; const vx = Number(player.VX); const vy = Number(player.VY); let col = 1; let row = 1; if(vx > 0) col = 2; if(vx < 0) col = 0; if(vy > 0) row = 2; if(vy < 0) row = 0; // 機体の描画 if(player.IsShow){ let image = playerImages1[arr[row][col]]; if(player.ConnectionId != connectionID) image = playerImages2[arr[row][col]]; const arr = [0, fieldSize, -fieldSize]; for(let k = 0; k < arr.length; k++){ for(let m = 0; m < arr.length; m++) ctx.drawImage(image, x - CHARACTER_SIZE / 2 + arr[k], y - CHARACTER_SIZE / 2 + arr[m], CHARACTER_SIZE, CHARACTER_SIZE); } } // 機体の近くにプレイヤー名とスコアを描画する ctx.fillStyle = '#fff'; ctx.font = '16px Arial bold'; ctx.textBaseline = 'top'; ctx.fillText(player.Name, x + 20, y + 10); ctx.fillText(player.Score, x + 20, y + 30); // レーダーに点を描画する(自機なら白、それ以外なら赤) if(player.ConnectionId == connectionID) blotMap(player.X, player.Y, '#fff'); else blotMap(player.X, player.Y, '#f00'); } } |
火花の描画
火花を描画する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function drawSparks(data, shiftX, shiftY){ const arr = [0, fieldSize, -fieldSize]; for(let i = 0; i < data.Sparks.length; i++){ const spark = data.Sparks[i]; for(let k = 0; k < arr.length; k++){ for(let m = 0; m < arr.length; m++){ const x = spark.X - shiftX + arr[k] - CHARACTER_SIZE / 2; const y = spark.Y - shiftY + arr[m] - CHARACTER_SIZE / 2; if(0 <= x && x < CANVAS_WIDTH && 0 <= y && y < CANVAS_HEIGHT && sparkImages.length > spark.Type) ctx.drawImage(sparkImages[spark.Type], x, y, CHARACTER_SIZE, CHARACTER_SIZE); } } } } |
スコアの描画
canvas上部にスコアと残機を描画する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function drawScore(data){ // data.Playersのなかから自機はどれか調べて描画する for(let i = 0; i < data.Players.length; i++){ const player = data.Players[i]; if(player.ConnectionId == connectionID){ ctx.fillStyle = '#fff'; ctx.font = '20px Arial bold'; ctx.textBaseline = 'top'; ctx.fillText(`SCORE ${player.Score} 残 ${player.Rest}`, 10, 10); return; } } } |
上位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 drawHighScores(data){ // プレイヤー名とスコア、残機数を取得 const scoreInfos = []; for(let i = 0; i < data.Players.length; i++){ const player = data.Players[i]; const scoreInfo = { name:player.Name, score: player.Score, rest: player.Rest, }; scoreInfos.push(scoreInfo); } // スコアでソート const result = scoreInfos.sort(function(a, b) { return (a.score > b.score) ? -1 : 1; }); ctxMap.fillStyle = '#fff'; ctxMap.font = '16px Arial bold'; ctxMap.textBaseline = 'top'; const count = Math.min(result.length, 3); for (let i = 0; i < count; i++){ ctxMap.fillText(result[i].name, 210, 50 * i + 10); ctxMap.fillText(`${result[i].score} (${result[i].rest})` , 220, 50 * i + 30); } } |
効果音の再生
自機から弾丸が発射されたらサーバーサイドから”SendToClientShoted”が送信されるので、そのときは効果音を再生します。
1 2 3 4 |
connection.on("SendToClientShoted", () => { soundShot.currentTime = 0; soundShot.play(); }); |
自機が敵を撃墜したときはサーバーサイドから”SendToClientHitEnemy”が、自機が要塞の中心部を破壊したときはサーバーサイドから”SendToClientHitEnemy”が、自機が死亡したときは”SendToClientPlayerDead”がそれぞれ送られてくるので、そのときは対応する効果音を鳴らします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
connection.on("SendToClientHitEnemy", () => { soundHitEnemy.currentTime = 0; soundHitEnemy.play(); }); connection.on("SendToClientHitCore", () => { soundHitCore.currentTime = 0; soundHitCore.play(); }); connection.on("SendToClientPlayerDead", () => { soundPlayerDead.currentTime = 0; soundPlayerDead.play(); }); |
ゲームオーバー時の処理
ゲームオーバーになったらサーバーサイドから”SendToClientPlayerGameOvered”が送られてくるので、この場合はisPlayingフラグのクリア、BGMの停止、BGM再生用のインターバルの削除をおこないます。そしてゲームオーバーの効果音を鳴らします。そのあと非表示になっていたゲームスタートボタンを再表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
connection.on("SendToClientPlayerGameOvered", () => { // まだ自機死亡の爆発音が再生されているのでしばらく待つ setTimeout(() => { isPlaying = false; clearInterval(bgmIntervalId); bgm.pause(); soundGameOver.currentTime = 0; soundGameOver.play(); }, 2000); // さらにしばらく待ってゲームスタートボタンを再表示する setTimeout(() => { $start.style.display = 'block'; }, 4500); }); |