ASP.NET coreで対戦型『スペースウォー!』(Spacewar!)をつくる(3)の続きです。今回はクライアントサイドの処理を定義します。
Contents
cshtml部分
style.cssとapp.jsを相対urlで指定していますが、これらのファイルはwwwroot\space-war-appフォルダ内につくります。
Pages\space-war-app\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 |
@page @{ Layout = null; } <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>鳩でもわかるスペースウォー!(Spacewar!)</title> <meta name="viewport" content="width=device-width, initial-scale = 1.0"> <link rel="stylesheet" href="./style.css" type="text/css" media="all"> </head> <body> <div id="container"> <div id = "field"> <canvas id="canvas"></canvas> <div id="game-info"> <p id="result"></p> <p id="waiting"></p> <button id="entry">エントリーする</button> <p id="game-count"></p> <div id="games"></div> </div> <button id="stop-watch-game" onclick="stopWatchGame()">観戦を終了する</button> </div> <div id="ctrl-buttons"> <button id="up" class="buttons">加速</button> <button id="left" class="buttons">LEFT</button> <button id="right" class="buttons">RIGHT</button> <button id="shot" class="buttons">SHOT</button> </div> <div id="config"> <p><label for="player-name">プレーヤー名:</label><input type="text" id="player-name" maxlength="24"></p> <p> 効果音: <input type="range" id="volume-range" min="0" max="1" step="0.01"> <span id="volume-value"></span> <button id="volume-test">効果音のテスト</button> </p> <p><input type="checkbox" id="hide-buttons"><label for="hide-buttons">スマホ用のボタンを非表示にする</label></p> <p>中心にある太陽の重力に引き込まれないように注意して敵を倒してください。</p> <p> 方向転換しただけでは進行方向を変えることはできません。 また減速するときは逆方向に回頭してから加速する必要があります。操作が難しいゲームです。 </p> <p> 『スペースウォー!』(Spacewar!)は1962年、当時マサチューセッツ工科大学(MIT)の学生であったスティーブ・ラッセルを中心に、 DEC社のミニコンPDP-1上で稼動するデモンストレーションプログラムとして開発されました。世界初のシューティングゲームとされています。 </p> </div> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script> <script> const connection = new signalR.HubConnectionBuilder().withUrl("/spacewar-app-hub").build(); </script> <script src="./app.js"></script> </body> </html> |
wwwroot\space-war-app\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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
body { background-color: #000; color: #fff; } #container { width: 360px; } #field { position: relative; height: 360px; } #canvas { display: block; position: absolute; left: 0px; top: 0px; } #game-info { position: absolute; left: 0px; top: 0px; z-index: 1; } #stop-watch-game { display: none; position: absolute; left: 250px; top: 340px; z-index: 1; } #result { font-weight: bold; color: #ff0; } #ctrl-buttons { margin-top: -50px; height: 160px; position: relative; } .buttons { position: absolute; width: 160px; height: 60px; background-color: transparent; color: #fff; border-color: #fff; } #up { left: 100px; top: 0px; } #entry { width: 180px; height: 60px; margin-left: 90px; } #left { left: 10px; top: 70px; } #right { left: 190px; top: 70px; } #shot { left: 100px; top: 140px; } #config { margin-top: 50px; } #volume-range { width: 240px; vertical-align: middle; margin-bottom: 20px; } a { color: #0ff; } a:hover { color: #f00; } |
定数とグローバル変数
次にJavaScript部分を示します。最初に定数とグローバル変数を示します。
wwwroot\space-war-app\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 |
// canvasのサイズ const CANVAS_WIDTH = 360; const CANVAS_HEIGHT = 360; // canvas要素とコンテキスト const $canvas = document.getElementById('canvas') const ctx = $canvas.getContext('2d'); // それ以外の要素 const $gameInfo = document.getElementById('game-info'); const $entry = document.getElementById('entry'); const $result = document.getElementById('result'); const $stopWatchGame = document.getElementById('stop-watch-game'); const $gameCount = document.getElementById('game-count'); const $games = document.getElementById('games'); const $playerName = document.getElementById('player-name'); const $waiting = document.getElementById('waiting'); const $hideButtons = document.getElementById('hide-buttons'); const $ctrlButtons = document.getElementById('ctrl-buttons'); // 描画時に使用するイメージ const playerImage0 = new Image(); const playerImage1 = new Image(); // 効果音 const shotSounds = [ new Audio('./sounds/shot.mp3'), new Audio('./sounds/shot.mp3'), new Audio('./sounds/shot.mp3'), new Audio('./sounds/shot.mp3'), new Audio('./sounds/shot.mp3'), new Audio('./sounds/shot.mp3'), ]; const accelerateSound = new Audio('./sounds/accelerate.mp3'); const explodeSound = new Audio('./sounds/explode.mp3'); const entriedSound = new Audio('./sounds/entried.mp3'); const matchingSound = new Audio('./sounds/matching.mp3'); const byeWinSound = new Audio('./sounds/bye-win.mp3'); const loseSound = new Audio('./sounds/lose.mp3'); const winSound = new Audio('./sounds/win.mp3'); let connectionID = ''; // ASP.NET SignalRで使われる接続の一意のID let volume = 0.5; // 初期設定される効果音のボリューム let isPlaying = false; // 現在プレイ中か? let isDead = false; // 死亡フラグ(効果音の制御で必要) let watchGameNumber = -1; // どのゲームを観戦しているか? let playGameNumber = -1; // どのゲームのプレーヤーになっているか? |
ページが読み込まれたときの処理
ページが読み込まれたときの処理を示します。
canvasのサイズを調整し、画像ファイルを読み込んで描画に使うイメージを初期化します。イベントリスナを追加して効果音をレンジスライダーで調整できるようにします。そのあとASP.NET SignalRのハブに接続を開始します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
window.addEventListener('load', () => { $canvas.width = CANVAS_WIDTH; $canvas.height = CANVAS_HEIGHT; playerImage0.src = './images/wedge.png'; playerImage1.src = './images/needle.png'; addEventListeners(); initVolume(); $result.innerHTML = '接続中・・・。しばらくお待ちください。'; connection.start(); // ハブに接続を試みる }); |
イベントリスナの追加
イベントリスナを追加する処理を示します。
1 2 3 4 |
function addEventListeners(){ onButtonOperated(); // ボタンがクリック、タップされたときの動作 onKeyOperated(); // PCでキーが押下されたときの動作 } |
ボタンがクリック、タップ時のイベントリスナーを示します。
回転処理と加速処理はボタンが押下されている間だけおこないます。PCでmousedownしてずらし別のところでmouseupしたときにボタン押下状態が解除されないことを防ぐために①の処理を定義しています。
ゲーム中にプレイヤー名を変更可能なのでキーが押されたらそのつどプレイヤー名も送信します。接続されていない場合(connectionID == ”のとき)はなにもしません。
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 |
function onButtonOperated(){ const $left = document.getElementById('left'); const $right = document.getElementById('right'); const $up = document.getElementById('up'); const $shot = document.getElementById('shot'); const arr1 = ['mousedown', 'touchstart']; const arr2 = ['mouseup', 'touchend']; const arr3 = [$left, $right]; const arr4 = ['ArrowLeft', 'ArrowRight']; let accelerateInterval = null; for(let i=0; i<2; i++){ for(let k = 0; k < arr3.length; k++){ // 回転処理はPC、スマホともにボタンが押下されているあいだは継続する arr3[k].addEventListener(arr1[i], () => { if (connectionID == '') return; let playerName = $playerName.value; connection.invoke('DownKey', arr4[k], playerName); }); arr3[k].addEventListener(arr2[i], () => { if (connectionID == '') return; connection.invoke('UpKey', arr4[k]); }); } $shot.addEventListener(arr1[i], () => { if (connectionID == '') return; connection.invoke('DownKey', 'Space', $playerName.value); }); // 加速処理は、PC、スマホともにボタンが押下されているあいだは0.5秒間隔で実行する $up.addEventListener(arr1[i], () => { if (connectionID == '') return; connection.invoke('DownKey', 'ArrowUp', $playerName.value); // 加速処理をしたら効果音を鳴らす。自機死亡時はサーバーサイドでは処理を受け付けないが、 // クライアントサイドで効果音が再生される問題を回避するために // isPlayingフラグやisDeadフラグで制御する if(isPlaying && !isDead && accelerateInterval == null){ accelerateSound.volume = volume; accelerateSound.play(); accelerateInterval = setInterval(() => { connection.invoke('DownKey', 'ArrowUp', $playerName.value); }, 500); } }); $up.addEventListener(arr2[i], () => { clearInterval(accelerateInterval); accelerateInterval = null; }); } // ① PCの場合、ボタン以外のところでmouseupイベントが発生したら回転や加速処理を中止する document.addEventListener('mouseup', () => { connection.invoke('UpKey', 'ArrowLeft'); connection.invoke('UpKey', 'ArrowRight'); clearInterval(accelerateInterval); accelerateInterval = null; }); // スマホでボタンをタップしたときのデフォルトの動作を抑止する const arr5 = [$left, $right, $up, $shot]; for(let i=0; i<arr5.length; i++){ arr5[i]?.addEventListener('touchstart', (ev) => ev.preventDefault()); arr5[i]?.addEventListener('touchend', (ev) => ev.preventDefault()); } // エントリーボタンを押下したらエントリーする $entry?.addEventListener('click', () => { if (connectionID != '') { let playerName = $playerName.value; connection.invoke("Entry", playerName); } }); } |
PCでキーを押下した時のイベントリスナーを示します。
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 |
function onKeyOperated(){ let accelerateInterval = null; // キーが押されているか? let isLeftKeyDown = false; let isRightKeyDown = false; let isUpKeyDown = false; document.onkeydown = (ev) => { // ゲーム開始以降はデフォルトの動作を抑制する if (isPlaying && (ev.code == 'ArrowUp' || ev.code == 'ArrowDown' || ev.code == 'ArrowLeft' || ev.code == 'ArrowRight' || ev.code == 'Space')) ev.preventDefault(); // キーが押しっぱなしになっているときの二重送信を防ぐ if (ev.code == 'ArrowLeft' && isLeftKeyDown) return; if (ev.code == 'ArrowRight' && isRightKeyDown) return; if (ev.code == 'ArrowUp' && isUpKeyDown) return; if (ev.code == 'ArrowLeft') isLeftKeyDown = true; if (ev.code == 'ArrowRight') isRightKeyDown = true; if (ev.code == 'ArrowUp') isUpKeyDown = true; if (connectionID != '') { let playerName = $playerName.value; connection.invoke('DownKey', ev.code, playerName); if(isPlaying && !isDead && isUpKeyDown && accelerateInterval == null){ accelerateSound.volume = volume; accelerateSound.play(); accelerateInterval = setInterval(() => { connection.invoke('DownKey', 'ArrowUp', playerName); }, 500); } } } document.onkeyup = (e) => { if (e.code == "ArrowLeft") isLeftKeyDown = false; if (e.code == "ArrowRight") isRightKeyDown = false; if (e.code == "ArrowUp"){ isUpKeyDown = false; clearInterval(accelerateInterval); accelerateInterval = null; } if (connectionID != '') { connection.invoke("UpKey", e.code); } } } |
ボリュームの調整
ボリュームをレンジスライダーで調整できるようにする処理を示します。
1 2 3 4 5 6 7 8 9 10 11 |
function initVolume(){ const $volumeRange = document.getElementById('volume-range'); const $volumeValue = document.getElementById('volume-value'); $volumeRange.addEventListener('input', () => { $volumeValue.innerHTML = $volumeRange.value; volume = Number($volumeRange.value); }) $volumeRange.value = volume; $volumeValue.innerHTML = volume; } |
ボリュームのテストをする処理を示します。
1 2 3 4 |
document.getElementById('volume-test')?.addEventListener('click', () => { loseSound.volume = volume; loseSound.play(); }); |
接続成功時の処理
ハブに接続し成功したときの処理を示します。この場合はサーバーサイドからASP.NET SignalRで使われる接続の一意のIDが送られてくるのでそれをグローバル変数に保存し、接続が成功した旨を表示します。
1 2 3 4 |
connection.on('SucceedConnectionToClient', (id) => { connectionID = id; $result.innerHTML = '接続完了'; }); |
通信が切れてしまったときの処理は以下のようになります。画面に通信が切断された旨を表示します。
1 2 3 4 5 6 |
connection.onclose(async () => { $entry.style.display = 'block'; $result.innerHTML = 'エラー:通信が切断されました。'; $gameInfo.style.display = 'block'; isPlaying = false; }); |
エントリー時の処理
エントリーをしてサーバーサイドで正常に処理がおこなわれた場合はサーバーサイドからエントリーしたユーザーに’SucceedEntryToClient’が送信されます。この場合はエントリーした旨を表示し、エントリーボタンを非表示にします。
1 2 3 4 5 6 |
connection.on('SucceedEntryToClient', () => { $result.innerHTML = 'エントリーしました。'; $entry.style.display = 'none'; entriedSound.volume = volume; entriedSound.play(); }); |
エントリーし対戦相手を待っているユーザーがいる場合は全ユーザーにそのプレーヤー名を送信します。このときプレーヤー名に<や>があるかもしれないのでエスケープ処理をおこなっています。
1 2 3 4 5 6 7 8 9 |
connection.on('WaitingPlayerToClient', (message) => { $waiting.innerHTML = escapeText(message); }); // エスケープ処理 function escapeText(text){ const text2 = text.split('<').join('<'); return text2.split('>').join('>'); } |
マッチング成功時の処理
マッチングが成功したときの処理を示します。
マッチングが成功したときはサーバーサイドから対戦する2人のユーザーにgameNumberとともに’SucceedMatchingToClient’イベントが送信されます。この場合は対戦リストや待機ユーザーが表示されている要素を非表示にして、対戦中であることを示すisPlayingフラグをセットします。また送られてきたgameNumberをplayGameNumberに保存します。そしてマッチング成功の効果音を鳴らします。
1 2 3 4 5 6 7 8 |
connection.on('SucceedMatchingToClient', (gameNumber) => { $gameInfo.style.display = 'none'; // 対戦リストや待機ユーザーが表示されている要素を非表示に isPlaying = true; playGameNumber = gameNumber; matchingSound.volume = volume; matchingSound.play(); }); |
試合放棄時の処理
試合放棄があったときの処理を示します。サーバーサイドから試合放棄があったことと自身が勝者であることを示すメッセージとともに’ByeWinToClient’イベントが送信されます。送信の対象になるのは試合放棄をしたプレーヤーの対戦者です。またそのプレーヤー番号が0だった場合はその対戦を観戦していた全ユーザーにも送信されます。
この場合はisPlayingフラグのクリア、エントリー用ボタン、対戦リストや待機ユーザーが表示されている要素を再表示、効果音の再生がおこなわれます。
1 2 3 4 5 6 7 8 9 10 11 |
connection.on('ByeWinToClient', (message) => { isPlaying = false; setTimeout(() => { $entry.style.display = 'block'; // エントリーボタンの再表示 $result.innerHTML = escapeText(message); // メッセージの表示 $gameInfo.style.display = 'block'; // 対戦リストや待機ユーザーが表示されている要素を再表示 byeWinSound.volume = volume; byeWinSound.play(); }, 200); }); |
対戦終了時の処理
対戦が正常に終了した場合はサーバーサイドから’GameFinishedToClient’イベントが送信されます。
1 2 3 4 5 6 7 8 9 10 11 12 |
connection.on('GameFinishedToClient', (message, win) => { isPlaying = false; setTimeout(() => { $result.innerHTML = escapeText(message); $entry.style.display = 'block'; $gameInfo.style.display = 'block'; const sound = win ? winSound : loseSound; // 勝敗の結果で効果音を変える sound.volume = volume; sound.play(); }, 200); }); |
観戦の開始時・終了時の処理
ユーザーが別のプレイを観戦しようとしたとき、やめようとしたときの処理を示します。
[観戦する]ボタンをクリックしたときは、観戦を希望しているgameNumberとともに、サーバーサイドに’WatchGame’が送信されます。またこのときはどれを観戦しているのかがわかるようにグローバル変数watchGameNumberにgameNumberが保存されます。以降は更新処理時に観戦しているユーザーにもプレイの状態が描画されるようになります。
観戦をやめるときはこれまで観戦していたwatchGameNumberとともに’StopWatchGame’をサーバーサイドに送信します。また以降はどれも観戦していない状態になるのでグローバル変数watchGameNumberには-1が格納されます。また再び観戦できるように観戦用のボタンが表示され、観戦をやめるボタンは非表示になります。
1 2 3 4 5 6 7 8 9 10 11 |
function watchGame(gameNumber){ watchGameNumber = gameNumber; connection.invoke('WatchGame', gameNumber); } function stopWatchGame(){ connection.invoke('StopWatchGame', watchGameNumber); watchGameNumber = -1; $stopWatchGame.style.display = 'none'; $gameInfo.style.display = 'block'; } |
更新時の処理
サーバーサイドから’UpdateGameToClient’イベントが送信された場合は描画の更新処理がおこなわれます。同時に送られてくるJSONテキストをparseして適切な描画をおこないます。
描画は現在の対戦数、プレイを観戦するためのボタン(これらは対戦者本人には文字列が表示される要素が非表示なので見えない)、対戦しているプレーヤーの機体、中心にある太陽、爆発の火球など(見えるのは対戦しているユーザーと観戦しているユーザーのみ。死亡時の機体は非表示)です。
更新イベントをうけとったユーザーがプレーヤーや観戦者であるかどうかはJSONテキストをparseして得られるGameNumberの値とwatchGameNumberやplayGameNumberに格納されている値を比較すればわかります。観戦している最中にエントリーした場合はゲームのプレーヤーであり観戦者でもあると判定されます。この場合は「プレーヤー」であることを優先して描画処理をおこないます。
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 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
connection.on('UpdateGameToClient', (json) => { const obj = JSON.parse(json); // ゲーム全体の状態を表示する const count = obj.length; $gameCount.innerHTML = `現在 ${count} 個の対戦がおこなわれています`; // 観戦用のボタンを表示させる let text = ''; for(let i=0; i<count; i++){ const game = obj[i]; text += `<p><button onclick="watchGame(${game.GameNumber})">観戦する</button> ${escapeText(game.Player0.Name)} VS ${escapeText(game.Player1.Name)}</p>`; } if($games.innerHTML != text){ $games.innerHTML = text; } // プレーヤーと観戦者にそのゲームの状態を表示する let playGame = false; // 自分はプレーヤーか? let watchGame = false; // 自分は観戦者か? let game = null; // 描画の対象になるゲームオブジェクト for(let gameIndex=0; gameIndex<obj.length; gameIndex++){ if(playGameNumber == obj[gameIndex].GameNumber){ // ゲームのプレーヤーであり観戦者であると判定された場合は「プレーヤー」と判定する playGame = true; watchGame = false; game = obj[gameIndex]; break; } if(watchGameNumber == obj[gameIndex].GameNumber){ watchGame = true; game = obj[gameIndex]; } } // スマホ用の操作ボタン、ゲームの情報、観戦を中止するボタンを // 表示させる必要があるなら表示させ、ないなら非表示にする $ctrlButtons.style.display = playGame && !$hideButtons.checked ? 'block' : 'none'; $gameInfo.style.display = playGame || watchGame ? 'none' : 'block'; $stopWatchGame.style.display = watchGame ? 'block' : 'none'; // 描画処理をおこなう必要がないならここで終了 if(game == null) return; // ゲームの描画処理:canvasに表示されているものをクリアする ctx.fillStyle = '#000'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); // 中心の太陽の描画 drawSun(); for(let playerNum=0; playerNum<2; playerNum++){ const player = playerNum == 0 ? game.Player0 : game.Player1; // プレーヤーが発射した弾丸の描画 for(let i=0; i<player.Bullets.length; i++){ ctx.beginPath(); ctx.moveTo(player.Bullets[i].HeadX, player.Bullets[i].HeadY); ctx.lineTo(player.Bullets[i].TailX, player.Bullets[i].TailY); ctx.lineWidth = 2; if(playerNum == 0){ ctx.strokeStyle = '#0ff'; ctx.shadowColor = '#0ff'; } else { ctx.strokeStyle = '#f0f'; ctx.shadowColor = '#f0f'; } ctx.shadowBlur = 6; ctx.stroke(); // ぼかしをある程度濃くするために3回描画する ctx.stroke(); ctx.stroke(); ctx.shadowBlur = 0; } // 自機死亡時は機体の描画はしない。isDeadフラグをセットする if(player.IsDead){ isDead = true; continue; } // 自機生存時はisDeadフラグをクリアして機体の描画処理をおこなう isDead = false; const x = player.CenterX; const y = player.CenterY; const angle = player.Angle; ctx.save(); ctx.translate(x, y); ctx.rotate(angle); ctx.translate(-x, -y); if(playerNum == 0) ctx.fillStyle = '#f00'; else ctx.fillStyle = '#ff0'; // 謎のマジックナンバーがあるが描画用イメージの(25, 13)(31, 7)が中心点である if(playerNum == 0) ctx.drawImage(playerImage0, x-25, y-13); else ctx.drawImage(playerImage1, x-31, y-7); ctx.restore(); // 機体の近くにプレイヤー名も描画する ctx.fillStyle = '#fff'; ctx.font = '14px Arial'; ctx.textBaseline = 'top'; if(playerNum == 0) ctx.fillText(player.Name, x + 25, y + 25); else ctx.fillText(player.Name, x + 25, y + 25); } // スコアの描画 ctx.fillStyle = '#fff'; ctx.font = '24px Arial'; ctx.textBaseline = 'top'; ctx.fillText(`${game.Player0.Score} - ${game.Player1.Score}`, CANVAS_WIDTH - 80, 10); // 火球が存在するなら描画する for(let i=0; i<game.Fireballs.length; i++){ const fireball = game.Fireballs[i]; const image = getFireballImage(fireball.Type, fireball.Radius); ctx.drawImage(image, fireball.X - image.width / 2, fireball.Y - image.height / 2); } }); |
太陽の描画
canvasの中央に太陽を描画する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 |
function drawSun(){ ctx.beginPath(); ctx.arc(CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2, 24, 0, Math.PI * 2); ctx.shadowColor = '#f00'; ctx.shadowBlur = 24; ctx.fillStyle = '#000'; ctx.fill(); ctx.fill(); ctx.fill(); ctx.shadowBlur = 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 29 30 |
// 火球の種類は2つあるので配列も2つ用意する const fireballImages = [ [],[] ]; function getFireballImage(type, radius){ // イメージが存在しない場合は生成する if(fireballImages[type][radius] == undefined){ const shadowBlur = 24; const $tempCanvas = document.createElement('canvas'); $tempCanvas.width = (radius + shadowBlur) * 2; $tempCanvas.height = (radius + shadowBlur) * 2; const tempCtx = $tempCanvas.getContext('2d'); tempCtx.shadowBlur = shadowBlur; if(type == 0) tempCtx.shadowColor = 'rgb(0, 200, 200)'; else tempCtx.shadowColor = 'rgb(200, 0, 200)'; tempCtx.fillStyle = 'rgb(200, 200, 200)'; tempCtx.beginPath(); tempCtx.arc(radius + shadowBlur, radius + shadowBlur, radius, 0, Math.PI * 2); tempCtx.fill(); const image = new Image(); image.src = $tempCanvas.toDataURL(); fireballImages[type][radius] = image; } return fireballImages[type][radius]; } |
弾丸発射時の処理
弾丸の発射処理がおこなわれた場合はサーバーサイドから’ShotToClient’が送信されます。送信の対象になるのは弾丸を発射したプレーヤーと、そのプレーヤー番号が0のときは観戦している全ユーザーです。
連続発射されたときに前の発射音が再生中でも現在の発射音が再生されるようにしなければなりません。そこで同じ効果音のAudioオブジェクトをshotSounds配列に格納して交互に再生処理をおこなっています。
1 2 3 4 5 6 |
connection.on('ShotToClient', () => { shotSounds[0].volume = volume; shotSounds[0].play(); const sound = shotSounds.shift(); // 効果音のAudioオブジェクトを交互に使う shotSounds.push(sound); }); |
爆発発生時の処理
爆発が発生したときはサーバーサイドから’ExplodedToClient’イベントが送信されます。この場合は効果音を鳴らします。送信の対象になるのは対戦している2人のプレーヤーとこれを観戦している全ユーザーです。
1 2 3 4 5 |
connection.on('ExplodedToClient', () => { explodeSound.currentTime = 0; explodeSound.volume = volume; explodeSound.play(); }); |