めでたくブログ1000回更新を達成したので前回紹介したCanvas Confettiを使ってゲームをつくることにします。敵を倒すと紙吹雪が舞い、敵弾に当たると汚物がブチまけられるという文字通りのク○ゲーです。
Contents
HTML部分
最初にHTML部分を示します。canvasにゲームのキャラクターと紙吹雪を同時に描画しようとするとうまくいかないのでcanvasを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 32 33 34 35 36 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>ブログ1000回更新記念につくったシューティングゲーム</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 = "confetti-canvas"></canvas> <canvas id = "canvas"></canvas> <button id = "start">START</button> <button id = "up" class = "button">UP</button> <button id = "shot" class = "button">SHOT</button> <button id = "down" class = "button">DOWN</button> <button id = "left" class = "button">LEFT</button> <button id = "right" class = "button">RIGHT</button> </div> <p><input type="checkbox" id = "hide-buttons"><label for="hide-buttons">スマホ操作用のボタンを表示しない</label></p> <p><input type="checkbox" id = "rapid-fire"><label for="rapid-fire">自動連射モード</label></p> <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="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.2/dist/confetti.browser.min.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 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 |
/* スタイルシート */ body { background-color: #000; color: #fff; } #container{ width: 360px; } #field { position: relative; width: 360px; height: 480px; } /* ゲームのキャラクター描画用のcanvas */ #canvas { position: absolute; left: 0px; top: 0px; z-index: -1; } /* 紙吹雪描画用のcanvas */ #confetti-canvas { position: absolute; left: 0px; top: 0px; border: 1px solid #000; } /* ゲーム開始用のボタン */ #start { position: absolute; width: 120px; height: 60px; background-color: transparent; color: #fff; font-size: 20px; border-color: #fff; left: 120px; top: 300px; } /* プレイヤー操作用のボタン共通 */ .button { position: absolute; width: 100px; height: 60px; background-color: transparent; color: #fff; font-size: 20px; border-color: #fff; } /* プレイヤー操作用の各ボタン */ #up { left: 130px; top: 260px; } #left { left: 10px; top: 330px; } #shot { left: 130px; top: 330px; } #right { left: 250px; top: 330px; } #down { left: 130px; top: 400px; } /* ボリュームコントロール用のレンジスライダーの表示調整 */ #volume { vertical-align: middle; } |
グローバル変数と定数
グローバル変数と定数を示します。
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 |
// canvasのサイズ const CANVAS_WIDTH = 360; const CANVAS_HEIGHT = 480; // canvas要素とコンテキスト const $confettiCanvas = document.getElementById('confetti-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 $left = document.getElementById('left'); const $right = document.getElementById('right'); const $shot = document.getElementById('shot'); // 操作用のボタンの表示非表示をまとめて切り替えることができるように配列に格納しておく const ctrlButtons = [$up, $down, $left, $right, $shot]; // チェックボックス要素(自動連射とスマホ操作用ボタンの非表示) const $rapidFire = document.getElementById('rapid-fire'); const $hideButtons = document.getElementById('hide-buttons'); // 描画するイメージ(自機、敵、弾丸) const playerImage = new Image(); const enemyImages = []; const playerBulletImage = new Image(); const enemyBulletImage = new Image(); // プレイヤーに関する変数と定数 let player; // Playerオブジェクト let playerBullets = []; // PlayerBulletオブジェクトを格納する配列 const PLAYER_SIZE = 48; // プレイヤーのサイズ const PLAYER_BULLET_SIZE = 24;// 弾丸のサイズと速度 const PLAYER_BULLET_SPEED = 12; const MAX_LIFE = 8; // 8回敵弾に命中したらゲームオーバー let muteki = false; // 無敵状態かどうか? const PLAYER_SPEED = 8; // 移動速度 let up = false; // 上下左右に移動するボタンが押されているかどうか? let down = false; let left = false; let right = false; // 敵に関する変数と定数 let enemies = []; // Enemyオブジェクトを格納する配列 let enemyBullets = []; // EnemyBulletオブジェクトを格納する配列 const ENEMY_SIZE = 32; // 敵のサイズ const ENEMY_BULLET_SIZE = 24; // 弾丸のサイズと速度 const ENEMY_BULLET_SPEED = 6; const backColor1 = '#0cc'; // 通常時の背景の色 const backColor2 = '#f00'; // 被弾時の背景の色 let backColor = backColor1; // 背景の色 let updateCount = 0; // 更新回数 let isPlaying = false; // 現在プレイ中か? // 効果音とBGM const hitSound = new Audio('./sounds/hit.mp3'); const damageSound = new Audio('./sounds/damage.mp3'); const gameoverSound = new Audio('./sounds/gameover.mp3'); const bgm = new Audio('./sounds/bgm.mp3'); |
Playerクラスの定義
プレイヤーの状態を更新し描画するためにPlayerクラスを定義します。
1 2 3 4 5 6 7 8 9 10 |
class Player{ constructor(){ this.X = 0; // プレイヤーの座標 this.Y = 0; this.IsDead = true; // 死亡フラグ(プレイ中はfalseに) this.AllowShot = true; // 弾丸の発射は可能か?(連射制限) this.Life = MAX_LIFE; // 0になったらゲームオーバー this.Score = 0; // スコア } } |
ゲーム開始時にプレイヤーの状態を初期化する処理を示します。
プレイヤーの座標を初期位置に移動します。同時にLifeを最大値にして死亡フラグのクリアし、スコアをリセットする処理をおこないます。
1 2 3 4 5 6 7 8 9 10 |
class Player{ Init(){ // プレイヤーの座標を初期位置に移動 this.X = (CANVAS_WIDTH - PLAYER_SIZE) / 2; this.Y = 400; this.Life = MAX_LIFE; this.Score = 0; player.IsDead = false; } } |
更新時の処理を示します。
ボタンが押下されていたらその方向に自機の座標を移動させます。このときフィールドの外に出てしまわないように条件分岐で処理をします。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Player{ Update(){ // フィールドの外に出てしまわない場合、上に行きすぎない場合だけ移動させる if(up && this.Y - PLAYER_SPEED > 200) this.Y -= PLAYER_SPEED; if(down && this.Y + PLAYER_SPEED < CANVAS_HEIGHT - PLAYER_SIZE) this.Y += PLAYER_SPEED; if(left && this.X - PLAYER_SPEED > 0) this.X -= PLAYER_SPEED; if(right && this.X + PLAYER_SPEED < CANVAS_WIDTH - PLAYER_SIZE) this.X += PLAYER_SPEED; } } |
描画する処理を示します。
1 2 3 4 5 6 7 |
class Player{ Draw(){ // プレイヤーが生存する場合だけ描画する if(!this.IsDead) ctx.drawImage(playerImage, this.X, this.Y, PLAYER_SIZE, PLAYER_SIZE); } } |
弾丸を発射する処理を示します。無限に連射できないように(0.25秒以上あける)AllowShotがtrueの場合しか発射処理はしないようにしています。発射処理をするときはPlayerBulletオブジェクトを生成してplayerBulletsに格納しています。
1 2 3 4 5 6 7 8 9 10 11 |
class Player{ Shot(){ if(!this.AllowShot || this.IsDead) return; this.AllowShot = false; setTimeout(() => this.AllowShot = true, 250); const bullet = new PlayerBullet(this.X + (PLAYER_SIZE - PLAYER_BULLET_SIZE) / 2, this.Y); playerBullets.push(bullet); } } |
PlayerBulletクラスの定義
自機から発射された弾丸の状態を更新し描画するためにPlayerBulletクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class PlayerBullet{ constructor(x, y){ this.X = x; this.Y = y; this.IsDead = false; } Update(){ this.Y -= PLAYER_BULLET_SPEED; if(this.Y < -PLAYER_BULLET_SIZE) this.IsDead = true; } Draw(){ if(!this.IsDead) ctx.drawImage(playerBulletImage, this.X, this.Y, PLAYER_BULLET_SIZE, PLAYER_BULLET_SIZE); } } |
敵クラスの定義
次に敵クラスを定義します。
JavaScript 編隊攻撃を実装するのコードを少し変えて複数のタイプの敵を生成できるようにします。最初に基底クラスとなるEnemyBaseクラスを示します。
1 2 3 4 5 6 7 8 9 10 11 12 |
class EnemyBase{ constructor(delay){ this.X = 0; this.Y = 0; this.Time = -delay; this.IsDead = false; } Update(){ } Draw(){ } } |
Enemy0クラスの定義
タイプ0の敵は急接近してきてそのあと上方に撤収するタイプの敵です。コンストラクタの第一引数で最初に出現するX座標を設定します。この値で下降時の方向が左下か右下になるかが決まります。
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 Enemy0 extends EnemyBase{ constructor(initX, delay){ super(delay); this.InitX = initX; } Update(){ const max = CANVAS_HEIGHT - 100; // 下から100pxまで接近しそのあと撤収 const vx = this.InitX < CANVAS_WIDTH / 2 ? 2 : -2; // 1更新あたりのXY方向の移動量 const vy = 7; this.Time++; this.X = this.InitX + this.Time * vx; if(this.Time < max / vy) // 下に移動中 this.Y = this.Time * vy; else{ // 上に撤収中 this.Y = max - (this.Time * vy - max); if(this.Y < -ENEMY_SIZE) // canvasの外に待避したので死亡フラグをセットする this.IsDead = true; } } Draw(){ if(!this.IsDead) ctx.drawImage(enemyImages[0], this.X, this.Y, ENEMY_SIZE, ENEMY_SIZE); } } |
Enemy1クラスの定義
タイプ1の敵はジグザグに下降してくるタイプの敵です。コンストラクタの第一引数で最初に出現するX座標ともっとも左に位置する場合のX座標を設定します。
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 |
class Enemy1 extends EnemyBase{ constructor(minX, delay){ super(delay); this.minX = minX; } Update(){ // 幅200でジグザグ移動 // this.minXが一番左のX座標 const moveWidth = 200; const min = this.minX; const vx = 7; const vy = 3; this.Time++; const x = Math.abs(this.Time * vx); if(Math.floor(x / moveWidth) % 2 == 0) this.X = x % moveWidth + min; else this.X = moveWidth - x % moveWidth + min; this.Y = this.Time * vy; if(this.Y > CANVAS_HEIGHT) this.IsDead = true; } Draw(){ if(!this.IsDead) ctx.drawImage(enemyImages[1], this.X, this.Y, ENEMY_SIZE, ENEMY_SIZE); } } |
Enemy2クラスの定義
タイプ2の敵は回転しながら下降してくるタイプの敵です。コンストラクタの第一引数で回転中心になるX座標を設定します。
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 Enemy2 extends EnemyBase{ constructor(centerX, delay){ super(delay); this.CenterX = centerX; } Update(){ const radius = 80; const centerY = this.CenterX; // 回転中心になるX座標 this.Time++; this.X = radius * Math.cos(this.Time * 0.1) + centerY; this.Y = radius * Math.sin(this.Time * 0.1) + 1.5 * this.Time; // 回転させるとともに全体を1更新あたり1.5pxだけ下に移動させる if(this.Y > CANVAS_HEIGHT + radius) this.IsDead = true; } Draw(){ if(!this.IsDead) ctx.drawImage(enemyImages[2], this.X, this.Y, ENEMY_SIZE, ENEMY_SIZE); } } |
EnemyBulletクラスの定義
敵弾の状態を更新し描画するためにEnemyBulletクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class EnemyBullet{ constructor(x, y, vx, vy){ this.X = x; this.Y = y; this.VX = vx; this.VY = vy; this.IsDead = false; } Update(){ this.X += this.VX; this.Y += this.VY; if(this.X > CANVAS_WIDTH || this.X < -ENEMY_BULLET_SIZE || this.Y > CANVAS_HEIGHT || this.Y < -ENEMY_BULLET_SIZE) this.IsDead = true; } Draw(){ if(!this.IsDead) ctx.drawImage(enemyBulletImage, this.X, this.Y, ENEMY_BULLET_SIZE, ENEMY_BULLET_SIZE); } } |
次回は定義したクラスをつかってゲームを完成させます。