こんな感じのゲームをつくります。サンタクロースがモンスターにプレゼント箱を投げつけて倒すゲームです。
通常のシューティングゲームとは違って弾丸に相当するプレゼント箱が放物線を描いて飛びます。地面に接触すると消えてしまいます。敵をかわすときはジャンプするしかありません。またジャンプすることで上空の敵を攻撃することもできるようになります。また難易度調整のためにジャンプ中でも左右の移動ができるようにしています。
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 |
<!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> <button id = "start" onclick="gameStart()">START</button> <button id = "left" class = "button">LEFT</button> <button id = "right" class = "button">RIGHT</button> <button id = "jump" class = "button">JUMP</button> <button id = "shot" class = "button">SHOT</button> </div> <input type="checkbox" id = "show-buttons" checked><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= "./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 |
body{ background-color: #000; color: #fff; } #container { width: 360px; } #field{ position: relative; height: 510px; } #start { position: absolute; width: 160px; height: 70px; left:100px; top:350px; font-size: 20px; } .button { position: absolute; width: 120px; height: 70px; font-size: 20px; display: none; background-color: transparent; color: #fff; } #left, #right { top:350px; } #left { left:10px; } #right { left:230px; } #jump, #shot { left:120px; } #jump { top:270px; } #shot { top:430px; } |
グローバル変数と定数
JavaScript部分を示します。最初にグローバル変数と定数を示します。
index.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 52 53 54 55 56 57 58 59 60 |
// canvasのサイズ const CANVAS_WIDTH = 360; const CANVAS_HEIGHT = 480; const PLAYER_SIZE = 64; // プレイヤーのサイズ const PLAYER_BASE_Y = 300; // 地上にいるプレイヤーのY座標(左上部分のY座標) const PLAYER_VX = 6; // プレイヤーのX方向の移動速度 const PLAYER_JUMP_IVY = 14; // プレイヤーがジャンプするときの初速 const PRESENT_IVX = 8; // 投げられたプレゼント箱のX方向の移動速度 const PRESENT_IVY = 14; // 投げられたプレゼント箱のY方向の初速 const ENEMY_SIZE = 64; // 敵のサイズ const ENEMY1_VX = 5; // 敵(タイプ1:地上をまっすぐ移動)のX方向の移動速度 const ENEMY2_VX = 3; // 敵(タイプ2:空中をふわふわ移動)のX方向の移動速度 const CHARCTER_SIZE = 32; // プレイヤーと敵以外のキャラクター(プレゼント箱と敵弾)のサイズ const SCROLL_VX = 4; // X方向のスクロール量 // 各キャラクタ描画用のイメージ const playerImage = new Image(); const presentImage = new Image(); const enemyImage1 = new Image(); const enemyImage2 = new Image(); const enemyBombImage = new Image(); const sparkImages = []; // 爆発で発生した火花のイメージを格納する配列 let player; // Playerオブジェクトを格納する変数 let presents = []; // Presentオブジェクトを格納する変数 let enemies = []; // Enemyオブジェクトを格納する変数 let enemyBombs = []; // EnemyBombオブジェクトを格納する変数 let holes = []; // Holeオブジェクトを格納する変数 let sparks = []; // Sparkオブジェクトを格納する変数 const INIT_REST = 5; // 残機数 let rest = INIT_REST; let score = 0; // スコア let isPlaying = false; // 現在プレイ中か? let updateCountForNextEnemy = 0; // 次の敵が生成されるまでの更新回数 let updateCountForNextHole = 0; // 次の穴が生成されるまでの更新回数 // canvas要素とコンテキスト const $canvas = document.getElementById('canvas'); const ctx = $canvas.getContext('2d'); // ボタン要素 const $start = document.getElementById('start'); // ゲームスタート const $left = document.getElementById('left'); // 移動 const $right = document.getElementById('right'); const $jump = document.getElementById('jump'); // ジャンプ const $shot = document.getElementById('shot'); // プレゼント箱の投擲 // 効果音 const shotSound = new Audio('./sounds/shot.mp3'); const hitSound = new Audio('./sounds/hit.mp3'); const deadSound = new Audio('./sounds/dead.mp3'); const gameoverSound = new Audio('./sounds/gameover.mp3'); |
Charcterクラスの定義
更新と描画のためにクラスを定義します。
まずは他のクラスの基底クラスとなるCharcterクラスを定義します(当たり判定の処理をするときにCharcterクラス同士だとわかりやすいかもしれないので)。
index.js
1 2 3 4 5 6 7 |
class Charcter{ constructor(){ this.X = 0; this.Y = 0; this.Name = ''; // クラスの名前 } } |
Playerクラスの定義
プレイヤーの更新と描画のためにPlayerクラスを定義します。
初期化
コンストラクタを示します。
1 2 3 4 5 6 7 8 9 10 11 12 |
class Player extends Charcter { constructor(){ super(); this.Name = 'Player'; this.X = 6; this.Y = PLAYER_BASE_Y; // 地上にいるプレイヤーのY座標 this.UpdateCountAfterJump = -1; // ジャンプしたあとの更新回数(-1 ならジャンプ前) this.MovingDirect = ''; // 左右の移動中なら MovingDirect == 'left' または 'right'。''なら停止 this.AllowShot = true; // プレゼント箱の投擲は可能か? this.IsDead = true; } } |
Init関数は初期化(死亡状態からの復活)をするためのものです。
1 2 3 4 5 6 7 8 9 10 |
class Player extends Charcter { Init(){ this.X = 6; this.Y = PLAYER_BASE_Y; this.UpdateCountAfterJump = -1; this.MovingDirect = ''; this.IsDead = false; this.AllowShot = true; } } |
移動と更新
Move関数は左右の移動を開始するとき、Stop関数は左右の移動を停止するときに呼び出されます。
1 2 3 4 5 6 7 8 9 |
class Player extends Charcter { Move(direct){ this.MovingDirect = direct; } Stop(){ this.MovingDirect = ''; } } |
Update関数は更新処理をおこないます。MovingDirectが空文字列でない場合は左右に移動させます。またUpdateCountAfterJumpが-1でない場合はジャンプしたあとなので上下の移動処理もおこないます。
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 Player extends Charcter { Update(){ if(this.MovingDirect == 'left' && this.X - PLAYER_VX > 0) this.X -= PLAYER_VX; if(this.MovingDirect == 'right' && this.X + PLAYER_VX < CANVAS_WIDTH - PLAYER_SIZE) this.X += PLAYER_VX; // ジャンプ中でない場合は処理はここで終わり if(this.UpdateCountAfterJump == -1) return; // ジャンプ中はUpdateCountAfterJumpの値からY座標を計算する // 重力加速度が 9.8 ではなく 1.57 なのはご愛敬 // Y座標がPLAYER_BASE_Yよりも大きい場合はジャンプ終了。Y座標はPLAYER_BASE_Yと同じにする this.UpdateCountAfterJump++; const y = PLAYER_BASE_Y - PLAYER_JUMP_IVY * 2 * this.UpdateCountAfterJump + 0.5 * 1.57 * Math.pow(this.UpdateCountAfterJump, 2); if(y < PLAYER_BASE_Y){ this.Y = y; } else{ // ジャンプ処理の終了 this.Y = PLAYER_BASE_Y; this.UpdateCountAfterJump = -1; } } // 死亡フラグが立っていなければ描画処理をおこなう Draw(){ if(!this.IsDead) ctx.drawImage(playerImage, this.X, this.Y, PLAYER_SIZE, PLAYER_SIZE); } } |
ジャンプと投擲処理
ジャンプを開始する処理とプレゼント箱を投擲する処理を示します。
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 |
class Player extends Charcter { Jump(){ // 0(-1ではない値)にすることでUpdate関数内でジャンプ処理がおこなわれる // ジャンプ処理をしている最中に二重にジャンプしないようにチェックしている if(this.UpdateCountAfterJump == -1) this.UpdateCountAfterJump = 0; } Shot(){ // 死亡時や連射規制の場合以外なら投擲処理をおこなう if(!this.IsDead && this.AllowShot){ // いったん投擲したら0.25秒間は投擲不可 this.AllowShot = false; setTimeout(() => this.AllowShot = true, 250); // Presentクラスは後述。コンストラクタの引数はプレゼント箱の初期座標。 // プレゼント箱の初期座標を求める。初期座標はプレイヤーの中心部分 const x = this.X + (PLAYER_SIZE - CHARCTER_SIZE) / 2; const y = this.Y + (PLAYER_SIZE - CHARCTER_SIZE) / 2; presents.push(new Present(x, y)); // 効果音を鳴らす shotSound.currentTime = 0; shotSound.play(); } } } |
Presentクラスの定義
プレゼント箱の更新と描画のためにPresentクラスを定義します。
コンストラクタの引数はプレゼント箱の初期座標です。初速は定数で定義されているので、コンストラクタ内でセットします。
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 Present extends Charcter { constructor(x, y){ super(); this.Name = 'Present'; this.X = x; this.Y = y; this.InitY = y; // 投げ上げられたY座標を記憶しておく(更新後のY座標を計算するときに必要) this.IVX = PRESENT_IVX; // XY方向の初速 this.IVY = PRESENT_IVY; this.UpdateCount = 0; this.IsDead = false; } Update(){ this.UpdateCount++; this.X += this.IVX; this.Y = this.InitY - this.IVY * this.UpdateCount + 0.5 * 1.57 * Math.pow(this.UpdateCount, 2); // 地面に当たったプレゼント箱には死亡フラグをセットする // Y座標が PLAYER_BASE_Y + (PLAYER_SIZE - CHARCTER_SIZE) より大きければ地面に当たったことになる if(this.Y > PLAYER_BASE_Y + (PLAYER_SIZE - CHARCTER_SIZE)) this.IsDead = true; } Draw(){ ctx.drawImage(presentImage, this.X, this.Y, CHARCTER_SIZE, CHARCTER_SIZE); } } |
Enemy1クラスの定義
敵は2種類あります。地上をまっすぐ移動する敵の更新と描画をするためにEnemy1クラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Enemy1 extends Charcter{ constructor(){ super(); this.Name = 'Enemy1'; this.X = CANVAS_WIDTH; // 最初は画面の右端に出現させる this.Y = PLAYER_BASE_Y + (PLAYER_SIZE - ENEMY_SIZE); this.IsDead = false; } Update(){ this.X -= ENEMY1_VX; // だんだん左に移動させる // X座標が - ENEMY_SIZE 以下になったら(=見えなくなったら)死亡フラグをセットする if(this.X < -ENEMY_SIZE) this.IsDead = true; } Draw(){ ctx.drawImage(enemyImage1, this.X, this.Y, ENEMY_SIZE, ENEMY_SIZE); } } |
Enemy2クラスの定義
空中をフワフワ移動する敵の更新と描画をするためにEnemy2クラスを定義します。
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 |
class Enemy2 extends Charcter{ constructor(){ super(); this.Name = 'Enemy2'; this.X = CANVAS_WIDTH; this.Y = 30 + Math.random() * 30; // 初期のY座標は 30~60の乱数 this.IsDead = false; // UpdateCountの値で上や下に動く。バラツキを持たせるために初期値は0~29の乱数 this.UpdateCount = Math.floor(Math.random() * 30); } Update(){ this.X -= ENEMY2_VX; // だんだん左に移動させる // フワフワ上下にも移動させる this.UpdateCount++; if(this.UpdateCount % 100 > 50) this.Y += 0.5; else this.Y -= 0.5; // ときどき爆弾を落とす(EnemyBombクラスは後述) // 爆弾の初期座標は敵の中心部分とする if(this.UpdateCount % 60 == 0){ const plus = (ENEMY_SIZE - CHARCTER_SIZE) / 2; enemyBombs.push(new EnemyBomb(this.X + plus, this.Y + plus)); } if(this.X < -ENEMY_SIZE) this.IsDead = true; } Draw(){ ctx.drawImage(enemyImage2, this.X, this.Y, ENEMY_SIZE, ENEMY_SIZE); } } |
EnemyBombクラスの定義
空中の敵が投下した爆弾を更新し描画するためにEnemyBombクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class EnemyBomb extends Charcter { constructor(x, y){ super(); this.Name = 'EnemyBomb'; this.X = x; this.Y = y; this.IsDead = false; } Update(){ // 爆弾は斜めに落ちる。着地したら死亡フラグをセットする this.X -= 3; this.Y += 6; if(this.Y > PLAYER_BASE_Y + PLAYER_SIZE - CHARCTER_SIZE) this.IsDead = true; } Draw(){ ctx.drawImage(enemyBombImage, this.X, this.Y, CHARCTER_SIZE, CHARCTER_SIZE); } } |
Holeクラスの定義
地面のところどころに開いている穴を更新し描画するためにHoleクラスを定義します。
当たり判定は矩形でおこないますが、描画はそれよりも幅が広いV字の谷とします。
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 Hole extends Charcter { constructor(){ super(); this.Name = 'Hole'; this.X = CANVAS_WIDTH; this.Y = PLAYER_BASE_Y + PLAYER_SIZE; // PLAYER_BASE_Yは地上にいるプレイヤーの左上部分のY座標なので // これにPLAYER_SIZEを加えたものが、穴の上部のY座標となる } Update(){ this.X -= SCROLL_VX; } Draw(){ // V字の谷を作る ctx.beginPath(); ctx.moveTo(this.X - 30, this.Y); ctx.lineTo(this.X + CHARCTER_SIZE + 30, this.Y); ctx.lineTo(this.X + CHARCTER_SIZE / 2, this.Y + CHARCTER_SIZE * 3); ctx.closePath(); ctx.fillStyle = '#000'; ctx.fill(); } } |
Sparkクラスの定義
爆発によって発生した火花の更新と描画をするためにSparkクラスを定義します。これは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 26 27 |
class Spark { // コンストラクタの引数は火花の発生場所の座標と速度 constructor(x, y, vx, vy){ this.X = x; this.Y = y; this.VX = vx; this.VY = vy; this.Type = 0; // 火花のタイプ this.UpdateCount = 0; this.IsDead = false; } Update(){ this.UpdateCount++; if(this.UpdateCount % 3 == 0) // 3回更新で火花のタイプを変える(タイプは0~5) this.Type++; if(this.Type >= 6) // タイプ6は存在しないので死亡フラグをセットして消滅させる this.IsDead = true; this.X += this.VX; this.Y += this.VY; } Draw(){ ctx.drawImage(sparkImages[this.Type], this.X, this.Y, CHARCTER_SIZE + 4, CHARCTER_SIZE + 4); } } |
続きは次回とします。