【クソゲープロジェクト】前提として普通のパックマンをつくる(1)の続きです。とりあえず普通のパックマンを完成させます。
ページが読み込まれたときの処理
ページが読み込まれたときの処理を示します。canvasのサイズを調整したあとgetPositions関数を呼び出してグローバル変数 pHomeX, pHomeY, eHomeCX, eHomeCY, eHomeMinX, eHomeMaxX, eHomeMinY, eHomeMaxY, eHomeFrontX, eHomeFrontYに格納する値を取得します。そのあと描画に使うイメージの初期化、イベントリスナの追加、Playerオブジェクト、Enemyオブジェクト、Foodオブジェクトなどの生成とグローバル変数への格納したあと描画処理をおこないます。またupdate関数を呼び出して更新処理をおこなえるようにします。
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 |
window.onload = () => { $canvas.width = CANVAS_WIDTH; $canvas.height = CANVAS_HEIGHT; getPositions(); initImages(); addEventListeners(); player = new Player(); player.Init(); enemies.push(new Enemy(0)); enemies.push(new Enemy(1)); enemies.push(new Enemy(2)); enemies.push(new Enemy(3)); enemies.forEach(enemy => enemy.Init()); createFoods(); update(); draw(); $checkNoPowerfoods.checked = false; $checkDisallowEnemyTurnback.checked = false; $checkHideButtonsForSp.checked = false; } |
getPositions関数は二次元配列を調べて、通路、プレイヤー、敵の初期位置、巣のなかの敵の可動範囲などを取得してグローバル変数に格納する処理をおこないます。
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 |
function getPositions(){ for(let y = 0; y < map.length; y++){ for(let x = 0; x < map[0].length; x++){ if(map[y][x] == 2){ pHomeX = x; pHomeY = y; } if(map[y][x] == 3){ eHomeFrontX = x; eHomeFrontY = y; } if(map[y][x] == -1){ eHomeCX = x; eHomeCY = y; } if(map[y][x] == -2) eHomeMinX = x; if(map[y][x] == -3) eHomeMaxX = x; if(map[y][x] == -4) eHomeMinY = y; if(map[y][x] == -5) eHomeMaxY = y; } } } |
イベントリスナを追加する処理を示します。スタートボタンがクリックされたらゲーム開始処理をおこないます。noUpdateフラグをfalseにすることで更新処理がおこなわれるようになります。
そのあとスマホ用操作ボタンを操作したときにプレイヤーを移動できるようにします。さらに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 |
function addEventListeners(){ $start.addEventListener('click', () => { // パワー餌なしのオプション if($checkNoPowerfoods.checked) powerFoods = []; playBGM(); noUpdate = false; // 各種ボタンの表示と非表示の切り替え $start.style.display = 'none'; document.getElementById('config').style.display = 'none'; if(!$checkHideButtonsForSp.checked) $controller.style.display = 'block'; }); // スマホ用操作ボタンを操作したときにプレイヤーを移動できるようにする document.getElementById('up').addEventListener('mousedown', () => player.NextDirect = 'up'); document.getElementById('down').addEventListener('mousedown', () => player.NextDirect = 'down'); document.getElementById('left').addEventListener('mousedown', () => player.NextDirect = 'left'); document.getElementById('right').addEventListener('mousedown', () => player.NextDirect = 'right'); document.getElementById('up').addEventListener('touchstart', (ev) => { ev.preventDefault(); player.NextDirect = 'up'; }); document.getElementById('down').addEventListener('touchstart', (ev) => { ev.preventDefault(); player.NextDirect = 'down'; }); document.getElementById('left').addEventListener('touchstart', (ev) => { ev.preventDefault(); player.NextDirect = 'left'; }); document.getElementById('right').addEventListener('touchstart', (ev) => { ev.preventDefault(); player.NextDirect = 'right'; }); document.onkeydown = (ev) => { if(ev.key == 'ArrowLeft') player.NextDirect = 'left'; if(ev.key == 'ArrowRight') player.NextDirect = 'right'; if(ev.key == 'ArrowUp') player.NextDirect = 'up'; if(ev.key == 'ArrowDown') player.NextDirect = 'down'; } } |
BGMを再生する処理を示します。使用している音源が先頭の32秒をループ再生するといい感じになるのでそのようにしています。それ以外の音源をつかう場合は適切な処理をしてください。
1 2 3 4 5 6 7 8 9 |
let bgmInterval = null; function playBGM(){ clearInterval(bgmInterval); bgm.currentTime = 0; bgm.play(); bgmInterval = setInterval(() => { bgm.currentTime = 0; }, 32 * 1000); } |
餌をつくる処理を示します。二次元配列 map を調べて、餌とパワー餌の位置を取得し、その位置に餌が表示されるようにオブジェクトを生成して配列に格納します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function createFoods(){ foods = []; powerFoods = []; // map[y][x] == 4 が通常の餌、map[y][x] == 5 がパワー餌の座標 for(let y = 0; y < map.length; y++){ for(let x = 0; x < map[0].length; x++){ if(map[y][x] == 4){ foods.push(new Food(x, y)); } if(map[y][x] == 5){ powerFoods.push(new PowerFood(x, y)); } } } } |
更新処理
更新処理の部分を示します。プレイヤー死亡時と noUpdate == true の場合はキャラクタを移動させず描画処理のみをおこないます。移動させる場合はプレイヤーを移動させたあと敵の行動を決定し移動させます。そのあと当たり判定の処理をして描画処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 |
function update(){ if(!player.IsDead && !noUpdate){ player.Update() enemiesThink(); enemies.forEach(enemy => enemy.Update()); check(); } draw(); requestAnimationFrame(update); } |
敵の思考ルーチン
今回の企画のメインとなる部分です。ただしいまは普通のパックマンをつくっているので、敵をUターンさせないようにただランダムに移動方向を決めているだけです。
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 enemiesThink(){ for (let i = 0; i < 4; i++){ const enemy = enemies[i]; // この場合は敵の位置はかわらないので移動方向を決める処理もしていない if(enemy.IjikeTime > 0 && enemy.IjikeTime % 2 == 0) continue; // 敵が巣の外にいて死亡していない場合だけ移動方向を考える。それ以外は自動で処理がおこなわれる if(enemy.Status == '' && !enemy.IsDead){ // 巣の正面では左か右にしか移動できないが、移動方向がいづれでもない場合は乱数で決定する if(enemy.X == eHomeFrontX && enemy.Y == eHomeFrontY && enemy.Direct != 'left' && enemy.Direct != 'right'){ if(Math.random() < 0.5) enemy.Direct = 'left'; else enemy.Direct = 'right'; } else { const directs = enemy.GetNextDirects(); directs.length = directs.length - 1; // 元来た道をバックするのは候補から排除 // 進行方向の候補が複数ある場合は乱数で決める const len = directs.length; const index = Math.floor(Math.random() * len); enemy.Direct = directs[index]; } } } } |
当たり判定
当たり判定の処理を示します。
まずプレイヤーが餌がある部分を通過した場合、そこにある餌オブジェクトの死亡フラグをセットします。餌は適当にマップ上にばらまいただけなので(←おいw)、交差点の近くだと餌が消えずに残ってしまい不自然になってしまうとのダメだし指摘がありました。なのでプレイヤーの上下左右8ピクセルにある餌はすべて死亡フラグをセットします。パワー餌は交差点とは離れた位置にあるので、このような問題はないのでプレイヤーと座標が完全一致した場合だけ死亡フラグをセットしています。
死亡フラグがセットされたオブジェクトはすぐに配列から削除しています。またプレイヤーがパワー餌がある位置を通過したら敵をイジケ状態に変化させています。
敵とプレイヤーが接する距離にあるときは敵がイジケ状態であれば撃退、そうでない場合はミス判定します。撃退の場合はonEnemyDead関数を、ミス判定の場合はonDead関数を呼び出します。またフィールド上の餌がすべてなくなった場合はステージクリアなのでonClear関数を呼び出します。
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 check(){ for(let i =0 ; i < foods.length; i++){ if(foods[i].X - 8 <= player.X && player.X <= foods[i].X + 8 && foods[i].Y - 8 <= player.Y && player.Y <= foods[i].Y + 8) foods[i].IsDead = true; } for(let i = 0; i < powerFoods.length; i++){ if(powerFoods[i].X == player.X && powerFoods[i].Y == player.Y){ powerFoods[i].IsDead = true; enemies.forEach(enemy => enemy.ChageIjikeMode()); } } foods = foods.filter(food => !food.IsDead); powerFoods = powerFoods.filter(food => !food.IsDead); for(let i = 0; i < enemies.length; i++){ if(!enemies[i].IsDead && Math.pow(enemies[i].X - player.X, 2) + Math.pow(enemies[i].Y - player.Y, 2) < Math.pow(12, 2)){ if(enemies[i].IjikeTime <= 0) onDead(); else onEnemyDead(enemies[i]); } } if(foods.length == 0 && powerFoods.length == 0) onClear(); } |
ミス時、撃退時、ステージクリア時におこなわれる処理を示します。いずれの場合もnoUpdateフラグをセットして、一時的に更新処理を停止して動きを止めます。そのあと更新処理を再開させます。プレイヤー死亡とステージクリア時の場合はプレイヤーと敵の位置を初期状態に戻します。ステージクリア時は餌オブジェクトも生成しなおします。
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 |
function onDead(){ player.IsDead = true; noUpdate = true; deadSound.play(); setTimeout(() => { noUpdate = false; player.Init(); enemies.forEach(enemy => enemy.Init()); }, 3000); } function onEnemyDead(enemy){ enemy.IsDead = true; enemy.GetPathToHome(); getSound.play(); noUpdate = true; setTimeout(() => { noUpdate = false; }, 1500); } function onClear(){ noUpdate = true; clearSound.play(); setTimeout(() => { noUpdate = false; player.Init(); enemies.forEach(enemy => enemy.Init()); createFoods(); }, 3000); } |
描画処理
描画処理を示します。canvas全体を黒で塗りつぶして迷路部分の描画と餌、プレイヤーと敵を描画します。
1 2 3 4 5 6 7 |
function draw(){ clearCanvas(); drawMaze(); drawFoods(); player.Draw(); enemies.forEach(enemy => enemy.Draw()); } |
迷路部分の描画
迷路部分を描画する処理を示します。通路がある部分に円を重ねて描画することで通路の内部と輪郭部分を描画するのと同じ処理をしています。この処理を何度もすると時間がかかるので一度イメージを生成したらグローバル変数に格納して、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 |
function drawMaze(){ // イメージが生成されていないなら生成する if(mapImage == null){ ctx.fillStyle = '#08f'; for(let y = 0; y < map.length; y++){ for(let x = 0; x < map[0].length; x++){ if(map[y][x] > 0){ ctx.fillRect(x, y, 1, 1); ctx.beginPath(); ctx.arc(x, y, 10, 0, 2 * Math.PI); ctx.fill(); } } } ctx.fillStyle = '#000'; for(let y = 0; y < map.length; y++){ for(let x = 0; x < map[0].length; x++){ if(map[y][x] > 0){ ctx.fillRect(x, y, 1, 1); ctx.beginPath(); ctx.arc(x, y, 8, 0, 2 * Math.PI); ctx.fill(); } } } const dataurl = $canvas.toDataURL(); mapImage = new Image(); mapImage.src = dataurl; } ctx.drawImage(mapImage, 0, 0); } |
餌の描画
餌を描画する処理を示します。餌がある位置に色が付いた円を描画しているだけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function drawFoods(){ ctx.fillStyle = '#fff'; for(let i = 0; i < foods.length; i++){ ctx.beginPath(); ctx.arc(foods[i].X, foods[i].Y, 1, 0, 2 * Math.PI); ctx.fill(); } for(let i = 0; i < powerFoods.length; i++){ ctx.beginPath(); ctx.arc(powerFoods[i].X, powerFoods[i].Y, 4, 0, 2 * Math.PI); ctx.fill(); } } |
クソゲープロジェクト、ようやく前提部分が完成しました。次は敵をランダムに動かすのでなく挟み撃ちを仕掛ける部分を実装します。