今回はJavaScriptでスクランブルのようなゲームをつくります。
スクランブル(Scramble)は、1981年コナミから発表された縦画面横スクロールシューティングゲーム。ジェット機型の宇宙船を地形や敵に衝突するのを避けながら操縦し敵基地を破壊するのが目的です。今回は遊び心も入れて敵基地の近くに「鳩でもわかるC#ビル」も作ってみました。
Contents
HTML部分
HTML部分を示します。
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 |
<!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" type = "text/css" media = "all"> </head> <body> <div id = "container"> <div id = "field"> <canvas id = "canvas"></canvas> </div> <button onclick="gameStart()" id = "start">ゲームスタート</button> <button id = "up" class = "ctrl-button">UP</button> <button id = "down" class = "ctrl-button">DOWN</button> <button id = "accelerate" class = "ctrl-button">加速</button> <button id = "shot" class = "ctrl-button">SHOT</button> <button id = "bomb" class = "ctrl-button">BOMB</button> <p>上下移動: ↑↓キー、加速:→キー、弾丸発射:Zキー、爆弾投下:Xキー</p> <input type="checkbox" id = 'show-buttons'><label for="show-buttons">スマホ用の操作ボタンを表示する</label> <p>音量: <input type = "range" id = "volume" min = "0" max = "1" step = "0.01"> <span id = "vol-range"></span> <button onclick = "playSound()">音量テスト</button> </p> </div> <script src= "./terrain.js"></script> <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 |
body { background-color: #000; color: #fff; } #container { width: 360px; } #field { position: relative; } #start { position: absolute; width: 160px; height: 50px; left: 100px; top: 300px; font-weight: bold; } .ctrl-button { position: absolute; width: 150px; height: 70px; font-weight: bold; color: #fff; background-color: transparent; font-size: 18px; } #up, #down, #accelerate { left: 30px; } #up { top: 210px; } #accelerate { top: 300px; } #down { top: 390px; } #shot, #bomb { left: 190px; } #shot { top: 255px; } #bomb { top: 345px; } |
グローバル変数と定数
JavaScript部分を示します。
terrain.jsは地形を文字列で表わしています。■は地面、▲上り坂、△下り坂、▼と▽は上からぶら下がっている障害物、Mはミサイル、Fは燃料タンク、●は建物の壁、○は下半分は当たり判定がない建物の壁、Bは敵の基地、1~7はステージの区切り(復活時はここからのスタートとなる)です。
グローバル変数と定数を示します。
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 |
// canvasサイズ const CANVAS_WIDTH = 360; const CANVAS_HEIGHT = 500; // 各キャラクターのサイズ const PLAYER_WIDTH = 67 const PLAYER_HEIGHT = 24 const BLOCK_WIDTH = 28; const BLOCK_HEIGHT = 32; const MISSILE_WIDTH = 19 const MISSILE_HEIGHT = 32 const FUELTANK_WIDTH = 32 const FUELTANK_HEIGHT = 32 const UFO_WIDTH = 28; const UFO_HEIGHT = 15; const FIREBALL_WIDTH = 31; const FIREBALL_HEIGHT = 18; const BULLET_SIZE = 10; // 幅高さ共通 const SPARK_SIZE = 32 // 背景の移動速度 const SPEED = 2; // 各キャラクターの移動速度 const PLAYER_SPEED = 6; const BULLET_SPEED = 8; const MISSILE_SPEED = 3; const UFO_SPEED = 5; const FIREBALL_SPEED = 7; // イメージ const playerImage = new Image(); const bulletImage = new Image(); const missileImage = new Image(); const fuelTankImage = new Image(); const ufoImage = new Image(); const fireballImage = new Image(); const finalbaseImage = new Image(); const sparkImage1 = new Image(); const sparkImage2 = new Image(); // 地面のイメージ const rectImage = new Image(); const nonLeftTopImage = new Image(); const nonRightTopImage = new Image(); const nonLeftBottomImage = new Image(); const nonRightBottomImage = new Image(); // 建物のイメージ const buildingBlockImage = new Image(); const buildingBlockHalfImage = new Image(); const hatoNameImage = new Image(); // canvas要素とコンテキスト const $canvas = document.getElementById('canvas'); const ctx = $canvas.getContext('2d'); // ボタン要素 const $start = document.getElementById('start'); const $up = document.getElementById('up'); const $down = document.getElementById('down'); const $accelerate = document.getElementById('accelerate'); const $shot = document.getElementById('shot'); const $bomb = document.getElementById('bomb'); const $showButtons = document.getElementById('show-buttons'); let startPositions = []; // 復活時の出発点の座標を格納する配列 let player = null; // プレイヤーオブジェクト let bullets = []; // 自機から発射された弾丸を格納する配列 let bombs = []; // 自機から投下された爆弾を格納する配列 let obstaclePositions = []; // 地形の障害物の種類と座標を格納する配列 let missiles = []; // 敵のミサイルを格納する配列 let fuelTanks = []; // 敵の燃料タンクを格納する配列 let ufos = []; // 敵のUFOを格納する配列 let fireballs = []; // 敵のファイアボールを格納する配列 let finalbases = []; // 敵の司令基地を格納する配列 let sparks = []; // 爆発で発生した火花を格納する配列 let isPlaying = false; // 現在プレイ中か? let score = 0; // スコア let stage = 1; // 現在のステージ(最初は1) const INIT_REST = 5; // 残機 let rest = INIT_REST; const INIT_FUEL = 1400; // 燃料(0になったら墜落する) let fuel = INIT_FUEL; let curPositionX = 0; // どの座標をcanvasの一番左部分に描画するか? let cleared = false; // ステージクリアしたかどうかのフラグ // 移動するためのボタンは押下されているか? let pressRight = false; let pressUp = false; let pressDown = false; // 弾丸と爆弾は発射できる状態にあるか?(連射制限) let isAllowShot = true; let isAllowBomb = true; // 効果音 const shotSound = new Audio('./sounds/shot.mp3'); const bombSound = new Audio('./sounds/bomb.mp3'); const hitSound = new Audio('./sounds/hit.mp3'); const deadSound = new Audio('./sounds/dead.mp3'); const gameoverSound = new Audio('./sounds/gameover.mp3'); |
各クラスの定義
ゲームで必要な各キャラクターを移動させたり描画するためにクラスを定義します。
Positionクラス
Positionクラスは座標を管理するためのものです。
1 2 3 4 5 6 |
class Position { constructor(x, y){ this.X = x; this.Y = y; } } |
Charcterクラス
Charcterクラスは各キャラクターの基底クラスです。
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 |
class Charcter { constructor(x, y, w, h){ this.X = x; this.Y = y; this.Width = w; this.Height = h; // 左上の座標だけでなく右下の座標もすぐに取得できるように定義する this.Right = this.X + this.Width; this.Bottom = this.Y + this.Height; } Update(){ this.Right = this.X + this.Width; this.Bottom = this.Y + this.Height; } // 引数で渡された座標はキャラクターの内部かどうか? IsInside(x, y){ if(this.X <= x && x <= this.X + this.Width && this.Y <= y && y <= this.Y + this.Height) return true; else return false; } } |
Obstacleクラス
障害物を描画と当たり判定に必要なObstacleクラスを定義します。
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 Obstacle extends Charcter { constructor(x, y, type){ let height = BLOCK_HEIGHT; // type == '○' の障害物は下半分に当たり判定がない if(type == '○') height = BLOCK_HEIGHT / 2; super(x, y, BLOCK_WIDTH, height); this.Type = type; } Draw(){ const x = this.X - curPositionX; if(x < - BLOCK_WIDTH || CANVAS_WIDTH < x) return; if(this.Type == '■') ctx.drawImage(rectImage, x, this.Y, this.Width, this.Height); if(this.Type == '▲') ctx.drawImage(nonLeftTopImage, x, this.Y, this.Width, this.Height); if(this.Type == '△') ctx.drawImage(nonRightTopImage, x, this.Y, this.Width, this.Height); if(this.Type == '▼') ctx.drawImage(nonLeftBottomImage, x, this.Y, this.Width, this.Height); if(this.Type == '▽') ctx.drawImage(nonRightBottomImage, x, this.Y, this.Width, this.Height); if(this.Type == '●') ctx.drawImage(buildingBlockImage, x, this.Y, this.Width, this.Height); if(this.Type == '○') ctx.drawImage(buildingBlockHalfImage, x, this.Y, this.Width, this.Height); } } |
Playerクラス
自機を描画と当たり判定に必要なPlayerクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Player extends Charcter{ constructor(){ super(0, 0, PLAYER_WIDTH, PLAYER_HEIGHT); this.IsDead = false; } IsInside(x, y){ // 後部のジェット噴射している部分は当たり判定はない if(this.X + this.Width / 3 <= x && x <= this.X + this.Width && this.Y <= y && y <= this.Y + this.Height) return true; else return false; } } |
Bulletクラス
自機から発射された弾丸の描画と当たり判定に必要なBulletクラスを定義します。
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 Bullet extends Charcter { constructor(x, y){ super(x, y, BULLET_SIZE, BULLET_SIZE); this.IsDead = false; } Update(){ this.X += BULLET_SPEED + SPEED; // 見えないところへ飛んでいった弾丸には死亡フラグをセットする const x = this.X - curPositionX; if(x < -this.Width || x > CANVAS_WIDTH) this.IsDead = true; super.Update(); } Draw(){ // 死んだ弾丸は描画しない if(this.IsDead) return; ctx.drawImage(bulletImage, this.X - curPositionX, this.Y, this.Width, this.Height); } } |
Bombクラス
自機から投下された爆弾の描画と当たり判定に必要なBombクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Bomb extends Charcter { constructor(x, y){ super(x, y, BULLET_SIZE, BULLET_SIZE); this.IsDead = false; this.UpdateCount = 0; } Update(){ this.UpdateCount++; this.X += SPEED; // 自機から見て真下に落下させる this.Y += SPEED + 2; if(this.UpdateCount < 8) // 投下後しばらくは自機から見ても前方にも移動させる this.X += SPEED; super.Update(); } Draw(){ if(this.IsDead) return; ctx.drawImage(bulletImage, this.X - curPositionX, this.Y, this.Width, this.Height); } } |
Missileクラス
敵のミサイルの描画と当たり判定に必要なMissileクラスを定義します。
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 |
class Missile extends Charcter{ constructor(col, row){ super(col * BLOCK_WIDTH + (BLOCK_WIDTH - MISSILE_WIDTH) / 2, row * BLOCK_HEIGHT, MISSILE_WIDTH, MISSILE_HEIGHT); this.IsMoving = false; // ミサイルは上昇しているか?(最初は置かれているだけ) this.IsDead = false; // そのミサイルは自機のどの部分(水平成分)を狙うか?(0なら中心) this.Shift = Math.random() * 240 - 120; } Update(){ if(this.IsMoving){ this.Y -= MISSILE_SPEED; super.Update(); // canvas上端部まで飛んでいったミサイルは死亡フラグをセットする if(this.Bottom < 0) this.IsDead = true; } } Draw(){ // 死亡したミサイルや見えないところにあるミサイルは描画しない const x = this.X - curPositionX; if(this.IsDead || x < -BLOCK_WIDTH || x > CANVAS_WIDTH) return; ctx.drawImage(missileImage, x, this.Y, this.Width, this.Height); } } |
FuelTankクラス
敵の燃料タンクの描画と当たり判定に必要なFuelTankクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class FuelTank extends Charcter{ constructor(col, row){ super(col * BLOCK_WIDTH - (FUELTANK_WIDTH - BLOCK_WIDTH) / 2, row * BLOCK_HEIGHT, FUELTANK_WIDTH, FUELTANK_HEIGHT); this.IsDead = false; } Draw(){ const x = this.X - curPositionX; if(this.IsDead || x < -BLOCK_WIDTH || x > CANVAS_WIDTH) return; ctx.drawImage(fuelTankImage, x, this.Y, this.Width, this.Height); } } |
UFOクラス
敵のUFOの描画と当たり判定に必要なUFOクラスを定義します。
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 |
class UFO extends Charcter{ constructor(){ super(curPositionX + CANVAS_WIDTH, CANVAS_HEIGHT / 2, UFO_WIDTH, UFO_HEIGHT); this.IsMoveUp = Math.random() < 0.5 ? true : false; this.IsDead = false; } Update(){ // 水平方向は少しずつ自機に近づいてくるようにSPEEDの半分をプラスする this.X += SPEED / 2; // 垂直方向は障害物にぶつからないように上下にジグザグに移動させる // 移動方向に障害物がなければ移動、あれば移動方向を反転する if(this.IsMoveUp && obstaclePositions.filter(block => block.IsInside(this.X, this.Y - UFO_SPEED - 32)).length > 0) this.IsMoveUp = false; if(!this.IsMoveUp && obstaclePositions.filter(block => block.IsInside(this.X, this.Y + UFO_SPEED + UFO_HEIGHT + 32)).length > 0) this.IsMoveUp = true; if(this.IsMoveUp) this.Y -= UFO_SPEED; else this.Y += UFO_SPEED; if(this.Y < 0) this.IsMoveUp = false; if(this.Y > CANVAS_HEIGHT) this.IsMoveUp = true; // 見えない部分(canvasの左端部より左)に移動したUFOには死亡フラグをセットする const x = this.X - curPositionX; if(x < - this.Width) this.IsDead = false; super.Update(); } Draw(){ // 死亡したUFOは描画しない const x = this.X - curPositionX; if(this.IsDead) return; ctx.drawImage(ufoImage, x, this.Y, this.Width, this.Height); } } |
FireBallクラス
敵のファイアボールの描画と当たり判定に必要なFireBallクラスを定義します。
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 |
class FireBall extends Charcter{ constructor(){ // canvas右端部のランダムな位置に生成するが、 // その位置に障害物が存在する場合は最大3回乱数を生成しなおす let tryCount = 0; const x = curPositionX + CANVAS_WIDTH; let y = 0; while(tryCount < 3){ tryCount++; y = Math.random() * (CANVAS_HEIGHT - 64); if(obstaclePositions.filter(block => block.IsInside(x, y)).length == 0) break; y = 0; } super(x, y, FIREBALL_WIDTH, FIREBALL_HEIGHT); this.IsDead = false; } Update(){ // 自機に向かって水平移動させる this.X -= FIREBALL_SPEED; // canvas左端部に到達したら死亡フラグをセットして消滅させる if(this.X + this.Width < curPositionX) this.IsDead = true; // 移動先に障害物が存在する場合は死亡フラグをセットして消滅させる const x = this.X - curPositionX; if(x < - this.Width) this.IsDead = false; if(obstaclePositions.filter(block => block.IsInside(this.X, this.Y)).length > 0) this.IsDead = true; if(obstaclePositions.filter(block => block.IsInside(this.X, this.Y + this.Height)).length > 0) this.IsDead = true; super.Update(); } Draw(){ if(this.IsDead) return; ctx.drawImage(fireballImage, this.X - curPositionX, this.Y, this.Width, this.Height); } } |
Finalbaseクラス
敵司令部の描画と当たり判定に必要なFinalbaseクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Finalbase extends Charcter{ constructor(col, row){ super(col * BLOCK_WIDTH, row * BLOCK_HEIGHT, FUELTANK_WIDTH, FUELTANK_HEIGHT); this.IsDead = false; } Draw(){ const x = this.X - curPositionX; if(this.IsDead || x < -this.Width || x > CANVAS_WIDTH) return; ctx.drawImage(finalbaseImage, x, this.Y, this.Width, this.Height); } } |
Sparkクラス
爆発によって発生した火花の描画と当たり判定に必要なSparkクラスを定義します。
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 |
class Spark extends Charcter { constructor(x, y){ super(x, y, SPARK_SIZE, SPARK_SIZE); this.Type = 0; this.UpdateCount = 0; } Update(){ // 時間の経過とともに火花のタイプを変える this.UpdateCount++; if(this.UpdateCount > 8) this.Type = 1; if(this.UpdateCount > 16) this.Type = 2; } Draw(){ // タイプ0と1の火花のみ描画する(タイプ2は実質死亡フラグ) const x = this.X - curPositionX; if(this.Type == 2 || x < -BLOCK_WIDTH || x > CANVAS_WIDTH) return; if(this.Type == 0) ctx.drawImage(sparkImage1, x, this.Y, this.Width, this.Height); if(this.Type == 1) ctx.drawImage(sparkImage2, x, this.Y, this.Width, this.Height); } } |
続きは次回にします。