ウサギの糞で部屋を埋め尽くすゲームをつくる(1)の続きです。今回はGameクラスを実装してゲームを完成させます。
Contents
Gameクラスの定義
Game全体の進行を管理するためのGameクラスを定義します。
コンストラクタ
最初にコンストラクタを示します。
|
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 |
class Game { constructor(){ this.$score = document.getElementById('score'); // スコア、残機数表示用のDOM要素 this.$life = document.getElementById('life'); this.IsPlaying = false; this.StopingUpdate = true; // 更新処理を停止する this.Score = 0; this.Stage = 1; this.InitLife = 5; // 残機数は 5 this.Life = this.InitLife; this.InitDeadDistance = 48; // 初期の飼い主さんの視界は 48 this.DeadDistance = this.InitDeadDistance; // マスを描画するための Cellオブジェクトを生成する this.Cells = []; for(let row = 0; row < ROW_COUNT; row++){ for(let col = 0; col < COL_COUNT; col++) this.Cells.push(new Cell(row, col)); } // PlayerオブジェクトとEnemyオブジェクトを生成して描画する this.Player = new Player(); this.Player.Init(); this.Enemy = new Enemy(); this.Enemy.Init(); this.Draw(); // 効果音 this.SoundSelect = new Audio('./sounds/select.mp3'); this.SoundTapi = new Audio('./sounds/tapi.mp3'); this.SoundStageClear = new Audio('./sounds/stage-clear.mp3'); this.SoundMiss = new Audio('./sounds/miss1.mp3'); this.SoundGameover1 = new Audio('./sounds/gameover1.mp3'); this.SoundGameover2 = new Audio('./sounds/gameover2.mp3'); this.Sounds = [this.SoundSelect, this.SoundTapi, this.SoundMiss]; this.Sounds2 = [this.SoundStageClear, this.SoundGameover1, this.SoundGameover2]; this.PrevUpdateTime = Date.now(); } } |
ゲームの開始処理
ゲームを開始するための処理を示します。
スコア、ステージ数、残機数、飼い主さんの視界を初期状態に戻し、マスの状態を初期化しています。そのあとプレイ中であることを示すIsPlayingフラグをtrueにして更新処理を開始します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Game { GameStart(){ this.Score = 0; this.Stage = 1; this.Life = this.InitLife; this.DeadDistance = this.InitDeadDistance; this.Cells.forEach(cell => cell.Reset()); this.Player.Init(); this.Enemy.Init(); document.getElementById('score').innerHTML = `Score ${this.Score.toLocaleString()}`; this.IsPlaying = true; this.StopingUpdate = false; document.getElementById('start-buttons').style.display = 'none'; this.SoundSelect.play(); } } |
移動処理
CanMove関数は第一、第二引数で指定された座標から第三引数で指定された方向に移動可能かどうかを調べるためのものです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Game { CanMove(x, y, dir){ // 余白分を引いて X,Y 座標がCELL_SIZEで割り切れれば方向転換可能な座標にいることになる。 // フィールドの外に移動しないように注意する。 x -= MARGIN; y -= MARGIN; if(y % CELL_SIZE == 0){ const col = x / CELL_SIZE; if(dir == 'L') return col > 0; if(dir == 'R') return col < COL_COUNT - 1; } if(x % CELL_SIZE == 0){ const row = y / CELL_SIZE; if(dir == 'U') return row > 0; if(dir == 'D') return row < COL_COUNT - 1; } return false; } } |
GetCellByPosition関数は引数で渡された座標に対応するCellオブジェクトを返します。方向転換可能な位置でないなら null を返します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Game { GetCellByPosition(x, y){ x -= CELL_SIZE / 2; y -= CELL_SIZE / 2; if(x % CELL_SIZE == 0 && y % CELL_SIZE == 0){ const row = y / CELL_SIZE; const col = x / CELL_SIZE; for(let i = 0; i < this.Cells.length; i++){ if(this.Cells[i].Row == row && this.Cells[i].Col == col) return this.Cells[i]; } } return null; } } |
GetNearestCellByPosition関数は引数で渡された座標の近くにあるCellオブジェクトを返します(GetCellByPosition関数と違って null は返さない)。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
class Game { GetNearestCellByPosition(x, y){ x -= CELL_SIZE / 2; y -= CELL_SIZE / 2; const row = Math.round(y / CELL_SIZE); const col = Math.round(x / CELL_SIZE); for(let i = 0; i < this.Cells.length; i++){ if(this.Cells[i].Row == row && this.Cells[i].Col == col) return this.Cells[i]; } } } |
SetPlayerDirct関数はプレイヤーの移動方向を変更するためのものです。じっさいにプレイヤーの移動方向が変更されるのは移動変更可能点に到達したときです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Game { SetPlayerDirct(dir){ if(dir == 'N') return; if(dir == 'L') this.Player.NextDirect = 'L'; if(dir == 'R') this.Player.NextDirect = 'R'; if(dir == 'U') this.Player.NextDirect = 'U'; if(dir == 'D') this.Player.NextDirect = 'D'; } } |
MovePlayer関数はプレイヤーを移動させる処理をおこないます。
|
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 |
class Game { MovePlayer(){ // 移動方向変更可能点にいるなら方向転換する if(this.Player.NextDirect == 'L' && this.CanMove(this.Player.X, this.Player.Y, 'L')) this.Player.Direct = 'L'; if(this.Player.NextDirect == 'R' && this.CanMove(this.Player.X, this.Player.Y, 'R')) this.Player.Direct = 'R'; if(this.Player.NextDirect == 'U' && this.CanMove(this.Player.X, this.Player.Y, 'U')) this.Player.Direct = 'U'; if(this.Player.NextDirect == 'D' && this.CanMove(this.Player.X, this.Player.Y, 'D')) this.Player.Direct = 'D'; // 設定されている移動方向に移動できない場合は停止させる if(this.Player.Direct == 'L' && !this.CanMove(this.Player.X, this.Player.Y, 'L')) this.Player.Direct = 'N'; if(this.Player.Direct == 'R' && !this.CanMove(this.Player.X, this.Player.Y, 'R')) this.Player.Direct = 'N'; if(this.Player.Direct == 'U' && !this.CanMove(this.Player.X, this.Player.Y, 'U')) this.Player.Direct = 'N'; if(this.Player.Direct == 'D' && !this.CanMove(this.Player.X, this.Player.Y, 'D')) this.Player.Direct = 'N'; // 設定されている移動方向にPlayerオブジェクトの座標を移動させる if(this.Player.Direct == 'L') this.Player.X--; if(this.Player.Direct == 'R') this.Player.X++; if(this.Player.Direct == 'U') this.Player.Y--; if(this.Player.Direct == 'D') this.Player.Y++; } } |
糞をする
糞をするときは近くのマスを取得してそこにします。そのマスに糞が置かれていないときはスコアを加算します。
|
1 2 3 4 5 6 7 8 9 10 |
class Game { Defecate(){ const cell = this.GetNearestCellByPosition(this.Player.X, this.Player.Y); if(cell != null && cell.Defecate()){ this.Score += 10; this.SoundTapi.currentTime = 0; this.SoundTapi.play(); } } } |
飼い主さんの移動
敵役の飼い主さんを移動させる処理をおこないます。
飼い主さんは一番強い臭いがあるマスにむけて移動します。またすでに飼い主さんが一番強い臭いがあるマスにいる場合はそのつぎに強い臭いがあるマスにむけて移動します。臭いがあるマスが存在しない場合はプレイヤーに向かって移動します。
|
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 |
class Game { MoveEnemy(){ const cur_cell = this.GetCellByPosition(this.Enemy.X, this.Enemy.Y); if(cur_cell != null){ // 方向転換可能な位置にいるのであれば・・・ let target = this.Player; // 移動先のターゲットは? let smell = 0; for(let i = 0; i < this.Cells.length; i++){ if(this.Cells[i] != cur_cell && smell < this.Cells[i].Smell){ smell = this.Cells[i].Smell; target = this.Cells[i]; } } // ターゲットと飼い主さんの現在座標から移動方向を決める let d = target.X - this.Enemy.X; let dir = 'R'; if(d < this.Enemy.X - target.X){ d = this.Enemy.X - target.X; dir = 'L'; } if(d < target.Y - this.Enemy.Y){ d = target.Y - this.Enemy.Y; dir = 'D'; } if(d < this.Enemy.Y - target.Y){ d = this.Enemy.Y - target.Y; dir = 'U'; } this.Enemy.Direct = dir; // 決定された移動方向に移動できない場合は停止させる(そんなことあるのか?) if(!this.CanMove(this.Enemy.X, this.Enemy.Y, dir)) this.Enemy.Direct = 'N'; } // 設定されている移動方向に移動する if(this.Enemy.Direct == 'U') this.Enemy.Y--; if(this.Enemy.Direct == 'D') this.Enemy.Y++; if(this.Enemy.Direct == 'L') this.Enemy.X--; if(this.Enemy.Direct == 'R') this.Enemy.X++; } } |
当たり判定
当たり判定の処理を示します。
プレイヤーと飼い主さんの距離と飼い主さんの視界を数値で表示します。飼い主さんの視界のなかにプレイヤーが入った場合はミスとなります。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
class Game { HitJudge(){ const d2 = Math.pow(this.Player.X - this.Enemy.X, 2) + Math.pow(this.Player.Y - this.Enemy.Y, 2); const d = Math.floor(Math.sqrt(d2)); const dead_distance = Math.floor(this.DeadDistance); if(d < dead_distance) this.PlayerDead(); // ミス(後述) // プレイヤーと飼い主さんの距離と飼い主さんの視界を数値で表示する document.getElementById('distance').innerHTML = `distance = ${d} / ${dead_distance}`; } } |
ミス時の処理
ミス時はStopingUpdateフラグをセットしてプレイヤーと飼い主さんの移動処理を停止させます。そして残機を減らして 0 でない場合は 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 |
class Game { PlayerDead(){ if(this.StopingUpdate) return; this.StopingUpdate = true; this.SoundMiss.play(); this.Life--; setTimeout(() => { if(this.Life > 0){ this.Player.Init(); this.Enemy.Init(); this.StopingUpdate = false; // 飼い主さんの視界を初期化する this.DeadDistance = this.InitDeadDistance + (this.Stage - 1) * 10; } else if (this.IsPlaying) { this.GameOver(); // ゲームオーバー(後述) } }, 2000); } } |
ゲームオーバー時の処理
ゲームオーバー時の処理を示します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Game { GameOver(){ if(!this.IsPlaying) return; this.IsPlaying = false; // スコアランキングへの登録(後述) const player_name = document.getElementById('player-name').value; this.SaveScore(player_name, this.Score); // 効果音とゲーム開始ボタンの再表示 setTimeout(() => this.SoundGameover1.play(), 500); setTimeout(() => this.SoundGameover2.play(), 2500); setTimeout(() => document.getElementById('start-buttons').style.display = 'block', 3000); } } |
ステージクリア時の処理
ステージクリア時の処理を示します。
すべてのマスに糞がセットされているのであればステージクリアです。ステージクリアの場合はボーナスポイントを追加し、2 秒後にすべてのキャラクタとマスを初期化してゲームを再開します。このときに飼い主さんの初期の視界を少しだけ広げて難易度を上げます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Game { CheckStageClear(){ for(let i = 0; i < this.Cells.length; i++){ if(!this.Cells[i].IsDefecated) return false; } this.StopingUpdate = true; this.SoundStageClear.play(); setTimeout(() => { this.Score += this.Stage * 1000; this.Stage++; this.Player.Init(); this.Enemy.Init(); this.Cells.forEach(cell => cell.Reset()); this.DeadDistance = this.InitDeadDistance + (this.Stage - 1) * 10; this.StopingUpdate = false; }, 2000); return true; } } |
更新処理と描画処理
更新処理を示します。最後の更新から 16 ms 以上経過している場合は各キャラクタの座標を更新します。その後当たり判定とステージクリア判定をおこないます。そのさい飼い主さんの視界を少しずつ広げていきます(永久に逃げ続けられないようにする)。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Game { Update(){ requestAnimationFrame(() => this.Update()); const cur = Date.now(); const diff = cur - this.PrevUpdateTime; if(diff >= 16) this.PrevUpdateTime = cur; if(diff >= 16 && this.IsPlaying && !this.StopingUpdate){ this.MovePlayer(); this.MoveEnemy(); this.Cells.forEach(cell => cell.Update()); if(!this.CheckStageClear()){ this.DeadDistance += 0.01; this.HitJudge(); } } this.Draw(); } } |
描画処理をしめします。
プレイヤーと飼い主さんを描画するとともにスコアと残機数を表示します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Game { Draw(){ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); this.Cells.forEach(cell => cell.Draw()); this.Player.Draw(); this.Enemy.Draw(this.DeadDistance); this.$score.innerHTML = `Score ${this.Score.toLocaleString()}`; this.$life.innerHTML = `Life ${this.Life}`; } } |
スコアランキングへの登録
スコアランキングにプレイヤー名とスコアを登録する処理を示します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Game { SaveScore(player_name, score){ if(player_name == '') player_name = '名無しのゴンベ'; // JSON形式でPOST fetch('./ranking.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: player_name, score: score, }) }); } } |
サーバー側の処理とスコアランキングを表示させる処理はゲーム開始以降の処理 鳩でもわかるXORパズルをつくる(2)と同じなので省略します。
ページが読み込まれたときの処理
ページが読み込まれたときの処理を示します。
Gameオブジェクトを生成してキー操作やボタンの押下に対応できるようにイベントリスナを追加します。そのあと更新処理を開始します。
|
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 |
window.addEventListener('load', () => { const $playerName = document.getElementById('player-name'); const savedName = localStorage.getItem('hatodemowakaru-player-name'); if(savedName && $playerName != null) $playerName.value = savedName; $playerName?.addEventListener('change', () => { localStorage.setItem('hatodemowakaru-player-name', $playerName.value ); }); const game = new Game(); document.getElementById('start').addEventListener('click', () => game.GameStart()); document.addEventListener('keydown', (ev) => { if(game.IsPlaying) ev.preventDefault(); if(ev.key == 'ArrowLeft') game.SetPlayerDirct('L'); if(ev.key == 'ArrowRight') game.SetPlayerDirct('R'); if(ev.key == 'ArrowUp') game.SetPlayerDirct('U'); if(ev.key == 'ArrowDown') game.SetPlayerDirct('D'); if(ev.key == ' ') game.Defecate(); }); const $up = document.getElementById('up'); const $down = document.getElementById('down'); const $left = document.getElementById('left'); const $right = document.getElementById('right'); const $defecate = document.getElementById('defecate'); $up.addEventListener('mousedown', () => game.SetPlayerDirct('U')); $up.addEventListener('touchstart', () => game.SetPlayerDirct('U')); $down.addEventListener('mousedown', () => game.SetPlayerDirct('D')); $down.addEventListener('touchstart', () => game.SetPlayerDirct('D')); $left.addEventListener('mousedown', () => game.SetPlayerDirct('L')); $left.addEventListener('touchstart', () => game.SetPlayerDirct('L')); $right.addEventListener('mousedown', () => game.SetPlayerDirct('R')); $right.addEventListener('touchstart', () => game.SetPlayerDirct('R')); $defecate.addEventListener('mousedown', () => game.Defecate()); $defecate.addEventListener('touchstart', () => game.Defecate()); game.Update(); initVolume('volume', game.Sounds, game.Sounds2); }); |
レンジスライダーでボリューム調整ができるようにする処理はいつもどおりです。
|
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 |
function initVolume(elementId, sounds){ let volume = 0.3; const savedVolume = localStorage.getItem('hatodemowakaru-volume'); if(savedVolume) volume = Number(savedVolume); const $element = document.getElementById(elementId); const $div = document.createElement('div'); const $span1 = document.createElement('span'); $span1.innerHTML = '音量'; $div.appendChild($span1); const $range = document.createElement('input'); $range.type = 'range'; $div.appendChild($range); const $span2 = document.createElement('span'); $div.appendChild($span2); $range.addEventListener('input', () => { const value = $range.value; $span2.innerText = value; volume = Number(value) / 100; setVolume(); }); $range.addEventListener('change', () => localStorage.setItem('hatodemowakaru-volume', volume.toString())); setVolume(); $span2.innerText = Math.round(volume * 100).toString(); $span2.style.marginLeft = '16px'; $range.value = Math.round(volume * 100).toString(); $range.style.width = '230px'; $range.style.verticalAlign = 'middle'; $element.appendChild($div); const $button = document.createElement('button'); $button.innerHTML = '音量テスト'; $button.style.width = '120px'; $button.style.height = '45px'; $button.style.marginTop = '12px'; $button.style.marginLeft = '32px'; $button.addEventListener('click', () => { sounds[0].currentTime = 0; sounds[0].play(); }); $element.appendChild($button); function setVolume(){ for(let i = 0; i < sounds.length; i++) sounds[i].volume = volume; } } |
