今回はJavaScriptでミスタードリラーもどきを作ります。
Contents
もともとのミスタードリラーとは?
『ミスタードリラー』 (Mr. Driller) は、ナムコ(現バンダイナムコエンターテインメント)が開発・発売したアクションパズルゲームです。
主人公のホリ・ススムを操り、色分けされたブロックで構成された地面をひたすら掘っていき、地下のゴールを目指します。主人公がブロックで押し潰されるとミスになります。
空中であっても落下中のブロックは停止している同色のブロックが隣にあるとこれとくっついて停止します。4個以上くっつくと消滅します。またブロックが消滅することでその上にある他色のブロックの落下が開始されることがあります。パズル的要素があるゲームです。
エア(酸素)という時間制限があり、時間の経過とともにエアの量が減っていきます。酸欠でミスになることを回避するために途中に存在するエアカプセルを取ってエアの補給をしなければならないのですが、ブロックの中には壊してしまうとエアが急激に減少してしまう×ブロックが存在し、これがエアカプセルの四方全てを囲んでいることが多いです。
HTML部分
それではさっそく作っていきましょう。
最初にHTMLとCSSを示します。スマホで操作するためのボタンも表示させますが、これは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 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>ミスタードリラーもどき</title> <meta name = "viewport" content = "width=device-width, initial-scale = 1.0"> <link rel="stylesheet" href="./style.css"> </head> <body> <div id = "container"> <div id = "field"> <div id = "canvas-outer"></div> <div id = "stage-clear"> ステージクリア<br>BONUS 5,000pt </div> <div id = "start-buttons"> <p> <span class="background-color-black">プレイヤー名:</span><br> <input type="text" id = "player-name"> </p> <button id = "start">START</button> <p><a href="./ranking.html" class="background-color-black">ランキングをみる</a></p> </div> <div id = "ctrl-buttons"> <p><button id = "up" class = "button">UP</button></p> <p> <button id = "left" class = "button">LEFT</button> <button id = "right" class = "button">RIGHT</button> </p> <p><button id = "down" class = "button">DOWN</button></p> </div> </div> <div id = "volume-ctrl"></div> </div> <script src="./index.js"></script> </body> </html> |
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 |
body { background-color: black; color: white; } #container { margin-left: auto; margin-right: auto; width: 360px; } #field { position: relative; height: 480px; overflow: hidden; margin-bottom: 20px; } #canvas-outer { position: absolute; left: 0px; top: 0px; } #start-buttons { position: absolute; width: 340px; left: 0px; top: 250px; text-align: center; } #start { width: 160px; height: 70px; font-size: x-large; } #ctrl-buttons { position: absolute; width: 340px; left: 0px; top: 200px; text-align: center; display: none; } .button { width: 160px; height: 70px; background-color: transparent; color: white; font-size: x-large; } #stage-clear { position: absolute; width: 340px; left: 0px; top: 600px; font-size: 32px; font-weight: bold; text-align: center; display: none; } a { color: aqua; font-weight: bold; } a:hover { color: red; } .background-color-black { background-color: black; padding: 5px 20px 5px 20px; } |
グローバル変数と定数
グローバル変数と定数を示します。
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 |
const CANVAS_WIDTH = 360; // canvasのサイズ const CANVAS_HEIGHT = 480; let scroll_y = 0; // スクロール量 // DOM要素 const $canvas = document.createElement('canvas'); const $canvasOuter = document.getElementById('canvas-outer'); const ctx = $canvas.getContext('2d'); const $startButtons = document.getElementById('start-buttons'); const $ctrlButtons = document.getElementById('ctrl-buttons'); const $playerName = document.getElementById('player-name'); // ブロック、プレイヤー、火花を描画するときに使うイメージ const blockImages = []; const playerImage = new Image(); const sparkImages = []; const BLOCK_SIZE = 48; // ブロックのサイズ const ROW_COUNT = 1000; // ブロックは縦横に何個並んでいるか? const COL_COUNT = 7; const WAIT_FOR_FALL = 60; // 落下待機モードに入って60更新で落下させる let player = null; let blocks2x2 = []; // Blockオブジェクトを格納する二次元配列 let fallingGroups = []; // くっついて落下するブロックのグループ let sparks = []; // 爆発で発生した火花オブジェクトを格納する配列 let isPlaying = false; // 現在プレイ中か? let score = 0; // スコアと残機 let rest = 5; let stage = 1; // 現在のステージ数 let capsuleTotalCount = 0; // これまでのステージのエアカプセルの総数 let getCapsuleCount = 0; // プレイ中に回収できたエアカプセルの個数 let isDanger = false; // エアが残り少なくなったら警告音を鳴らす。その処理のためのフラグ // 移動のためのキーは押下されているか? let pressLeftKey = false; let pressRightKey = false; let pressUpKey = false; let pressDownKey = false; // 効果音とBGM const bgm = new Audio('./sounds/bgm.mp3'); const breakSound = new Audio('./sounds/break.mp3'); const getSound = new Audio('./sounds/get.mp3'); const hitSound = new Audio('./sounds/hit.mp3'); const deadSound = new Audio('./sounds/dead.mp3'); const dangerSound = new Audio('./sounds/danger.mp3'); const clearSound = new Audio('./sounds/clear.mp3'); const gameoverSound = new Audio('./sounds/gameover.mp3'); const sounds = [gameoverSound, breakSound, getSound, hitSound, deadSound, clearSound, dangerSound, bgm]; let volume = 0.3; // ボリューム |
Blockクラスの定義
ブロックを操作できるようにするためにBlockクラスを定義します。
コンストラクタ
ブロックのタイプはオブジェクトの生成時にランダムに決めます。通常のブロック(タイプ番号が 2以上のもの)は4種類なので 2 ~ 5の整数をランダムに割り振り、0と1のものは座標が決まってからあとで再設定します。
タイプ0は破壊するとエアが激減するブロック、1はエアを回復するアイテム、それ以外は普通のブロックです。エアを回復するアイテム(イメージが卵のもの)はブロックと落下時の挙動が違うのですが、ブロックの一種として扱います。
プレイヤーの移動先にあるブロックは原則すぐに破壊できるのですが、破壊するとエアが激減するブロックだけは5回叩かないと破壊できない仕様にしています(原作と合わせている)。
下にあるブロックが破壊され支えを失ったブロックはしばらく振動したあと落下しはじめるのですが、Block.WaitForFallは実際に落下を開始するまでの更新回数を示すものです。最初は巨大な値を指定して落下しないようにしています。
落下はBLOCK_SIZEピクセル分だけ落下してさらに落下を継続するか調べることにします。Block.FinishedFallはいままさに落下処理が完了したブロックであることを示します(Block.FinishedFall == trueのときだけさらに落下を継続するか調べればよい)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Block{ constructor(row, col){ this.Row = row; this.Col = col; this.Y = row * BLOCK_SIZE; this.X = col * BLOCK_SIZE; this.IsDead = false; this.WaitForFall = 100000000; this.FinishedFall = false; this.DrawCount = 0; this.Type = 0; this.Life = 1; // 通常のブロック(Type が 2以上のもの)は4種類なので 2 ~ 5の整数をランダムに割り振る const types = [2, 3, 4, 5, ] const idx = Math.floor(Math.random() * types.length); this.Type = types[idx]; } } |
SetType0関数は、一度生成したブロックを破壊するとエアが激減するブロックに変更します。通常のブロックと異なり5回叩かないと壊せないので Life = 5 を設定しています。
1 2 3 4 5 6 |
class Block{ SetType0(){ this.Type = 0; this.Life = 5; } } |
落下モードへの変更
StartFall関数はブロックを落下待機状態にします。引数は実際に落下しはじめるまでの更新回数です(1秒後に落下を開始するのであれば60を指定する)。
1 2 3 4 5 6 7 |
class Block{ StartFall(wait){ // すでに落下モードに入っているとき(this.WaitForFall <= wait のとき)は無視 if(this.WaitForFall > wait) this.WaitForFall = wait; } } |
更新
ブロックが落下しているときは座標を変更しなければなりません。落下中のときはUpdate関数で座標を変更します。またBLOCK_SIZEだけ落下したら自動的にBlock.FinishedFallフラグをtrueにして落下までの待機時間を巨大な値に設定しなおします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Block{ Update(){ this.WaitForFall--; if(this.WaitForFall <= 0){ this.Y += 2; if(this.Y - this.Row * BLOCK_SIZE >= BLOCK_SIZE){ this.WaitForFall = 100000000; this.Row++; this.Y = this.Row * BLOCK_SIZE; this.FinishedFall = true; } } } } |
描画
colにBLOCK_SIZEを掛けた値がブロックを描画するときのX座標、rowにBLOCK_SIZEを掛けてスクロール量を引いた値がブロックを描画するときのY座標となります。
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 |
class Block{ Draw(){ this.DrawCount++; // すでに破壊されたブロックと描画範囲がcanvasの範囲外のものは描画の処理をしない if(this.IsDead || this.Y - scroll_y < -BLOCK_SIZE || this.Y - scroll_y > CANVAS_HEIGHT) return; let plus = 0; // 落下開始前のブロックを上下に振動させる if(this.WaitForFall <= WAIT_FOR_FALL && this.Y % BLOCK_SIZE == 0 && this.DrawCount % 8 < 4) plus = 4; if(this.Type != 1) ctx.drawImage(blockImages[this.Type], this.X, this.Y - scroll_y + plus, BLOCK_SIZE, BLOCK_SIZE); else { // カプセルは明るく描画したり暗く描画する let alpha = this.DrawCount % 240; // 0 ~ 239 alpha -= 120; // -120 ~ 119 の乱数が得られる // (0 ~ 119) / 120 * 0.5 + 0.5 => これで 0.5 ~ 1.0 の乱数が得られる alpha = Math.abs(alpha) / 120 * 0.5 + 0.5; ctx.globalAlpha = alpha; ctx.drawImage(blockImages[this.Type], this.X, this.Y - scroll_y + plus, BLOCK_SIZE, BLOCK_SIZE); ctx.globalAlpha = 1.0; } } } |
Playerクラスの定義
プレイヤーを操作できるようにするためにPlayerクラスを定義します。
XY座標がBLOCK_SIZEの倍数であるときだけユーザーは移動処理を開始できます。それ以外のときは次の倍数まで自動で移動処理を継続します。
また連続でバック(上への移動)できる回数に制限をかけます(2回まで)。この制限は下に移動することでリセットされます。
コンストラクタ
コンストラクタを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Player { constructor(){ this.Row = 0; this.Col = 0; this.Y = 0; this.X = 0; this.Direct = ''; // 移動方向。空文字列以外のときはその方向に移動中 this.IsDead = false; this.Air = 50 * 60; this.AirFull = 50 * 60; // エアの最大値 this.CanBackCount = 2; // バック(上への移動)できる回数 } } |
初期化
ゲーム開始時やプレイヤー死亡状態から復活したときに状態を初期化する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 |
class Player { Init(row, col){ this.Direct = ''; this.CanBackCount = 2; this.Air = this.AirFull; this.Row = row; this.Col = col; this.Y = row * BLOCK_SIZE; this.X = col * BLOCK_SIZE; this.IsDead = false; } } |
更新処理
更新処理を示します。
Directに空文字以外がセットされているときはその方向に座標を移動させます。そして移動先の座標に到達したらDirectに空文字をセットしてそれ以上動かないようにします(移動ボタンが押しっぱなしの場合はすぐにDirectに移動方向の文字列がセットされるが・・・)。
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 |
class Player { Update(){ if(this.IsDead) return; if(this.Direct == 'left') this.X -= 4; if(this.Direct == 'right') this.X += 4; if(this.Direct == 'down') this.Y += 4; if(this.Direct == 'up'){ if(this.CanBackCount > 0) // 移動可のとき this.Y -= 4; else // 移動不可のとき this.Direct = ''; } // 移動完了時の処理 if(this.Direct == 'left' && this.X < (this.Col - 1) * BLOCK_SIZE){ this.Col--; this.X = this.Col * BLOCK_SIZE; this.Direct = ''; } if(this.Direct == 'right' && this.X > (this.Col + 1) * BLOCK_SIZE){ this.Col++; this.X = this.Col * BLOCK_SIZE; this.Direct = ''; } if(this.Direct == 'up' && this.Y < (this.Row - 1) * BLOCK_SIZE){ this.Row--; this.Y = this.Row * BLOCK_SIZE; this.Direct = ''; this.CanBackCount--; } if(this.Direct == 'down' && this.Y > (this.Row + 1) * BLOCK_SIZE){ this.Row++; this.Y = this.Row * BLOCK_SIZE; this.Direct = ''; this.CanBackCount = 2; } } } |
描画
描画処理を示します。死亡時以外はプレイヤーを描画します。
1 2 3 4 5 6 |
class Player { Draw(){ if(!this.IsDead) ctx.drawImage(playerImage, this.X, this.Y - scroll_y, BLOCK_SIZE, BLOCK_SIZE); } } |
Sparkクラスの定義
爆発で発生した火花を操作するためにSparkクラスを定義します。
コンストラクタ
コンストラクタの引数は火花が発生した座標と火花の移動速度です。
1 2 3 4 5 6 7 8 9 10 11 |
class Spark { constructor(x, y, vx, vy){ this.X = x; this.Y = y; this.VX = vx; this.VY = vy; this.MoveCount = 0; this.IsDead = false; this.ImageIndex = 0; } } |
移動
火花を移動させる処理を示します。移動回数によって描画で使用するイメージを変更します。使用できるイメージがなくなったらその火花は消滅します。
1 2 3 4 5 6 7 8 9 10 11 |
class Spark { Move(){ this.MoveCount++; this.X += this.VX; this.Y += this.VY; if(this.MoveCount % 4 == 0) this.ImageIndex++; if(this.ImageIndex >= sparkImages.length) this.IsDead = true; } } |
描画
描画処理を示します。
すでに寿命を終えた火花と描画範囲がcanvasの範囲外のものは描画の処理をしません。描画するときはImageIndexから描画で使用するイメージを取得して描画処理をおこないます。
1 2 3 4 5 6 7 8 9 |
class Spark { Draw(){ if(this.IsDead || this.Y - scroll_y < -BLOCK_SIZE || this.Y - scroll_y > CANVAS_HEIGHT) return; const image = sparkImages[this.ImageIndex]; ctx.drawImage(image, this.X, this.Y - scroll_y, BLOCK_SIZE, BLOCK_SIZE); } } |
使用するクラスの定義が完了したので次回はこれを使ってゲームを完成させます。