HTML・CSS・JavaScriptだけで、ブラウザで動く本格的な横スクロールアクションゲームを完成させるチュートリアルです。
ジャンプ・チャージショット・ステージ・ボス戦まで実装して、ロックマン級の手応えがある作品に仕上げよう。
⏱ 読了:約30分 🎯 対象:プログラミング初心者〜
📋 もくじ
- 完成するとどうなる?
- 準備するもの
- ファイル構成を作ろう
- HTMLを書こう
- CSSでデザインしよう
- JavaScriptの基本構造を書こう
- ゲームループを作ろう
- ステージを作ろう(タイルマップ)
- キー入力を管理しよう
- プレイヤーを動かそう(物理&衝突判定)
- ジャンプを気持ちよくしよう(プロの3テク)
- ショットとチャージを実装しよう
- 敵を作ろう(4種類)
- ボス戦を実装しよう
- エフェクトを追加しよう
- サウンドをコードだけで作ろう
- 完成コード全文
- 動かしてみよう
- 次のステップ
完成するとどうなる?
この記事を最後まで読むと、ブラウザ上で動く本格的な横スクロールアクションゲームが完成します。ただのアクションゲームじゃなくて、こんな機能がついています:
- プレイヤーが左右に走る・ジャンプする・はしごを登る
- 「押した時間」で3段階に強くなるチャージショット(通常弾・半チャージ・フルチャージ)
- メット敵・フライ敵・グラウンド敵・砲台の4種類の敵が登場
- コヨーテタイム・ジャンプバッファ・可変ジャンプ高さで操作感バツグン
- はしご・動く床・トゲ・シャッターなど多彩なギミック
- ボス部屋突入演出(シャッターが開いて自動歩行→ボス降下→HPゲージ充填)
- ボス撃破時の連続爆発+画面フラッシュの派手な演出
- 爆発パーティクル・画面シェイク・被弾フラッシュのジューシーな演出
- 音声ファイルなし!Web Audio APIでコードだけで効果音を生成
使うのは HTML・CSS・JavaScript の3つだけ。特別なライブラリやフレームワークは使いません。約3,000行でアクションゲーム級の作品が作れます!
準備するもの
以下の2つだけ用意してください。
- テキストエディタ(Web版のVS Codeだとインストール不要!)
- Webブラウザ(Chrome、Edge、Safariなど何でもOK)
ファイル構成を作ろう
パソコンのどこかにフォルダを1つ作ります。名前は rockman にしましょう。その中に3つのファイルを作ります。
rockman/
index.html ─ 画面の構造
style.css ─ 見た目のデザイン
script.js ─ ゲームの動き(約3,000行)
Webページは基本的にこの3つの役割分担でできています。HTMLが骨組み、CSSが見た目、JavaScriptが動き。シューティングの記事でもやった構成と同じです。
HTMLを書こう
index.html を開いて、以下のコードを書いてください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ロックマン風ゲーム</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<canvas id="gameCanvas" width="800" height="480"></canvas>
<script src="script.js"></script>
</body>
</html>
HTMLのポイント解説
シューティングと同じく、<canvas> タグが1つあるだけのシンプルな構成です。HPバーもスコアもすべてJavaScriptのCanvas描画で行うので、HTML側には余計な要素を置きません。
今回は横スクロールアクションなので、キャンバスサイズは横長の 800 × 480 にしています。これがゲーム画面の「見える範囲」で、実際のステージはこれよりずっと広い(横3,200px分)です。
CSSでデザインしよう
style.css にゲームの見た目を書きます。
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #000;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
overflow: hidden;
}
#gameCanvas {
display: block;
border: 2px solid #333;
image-rendering: pixelated;
}
CSSのポイント解説
背景を真っ黒にして、Canvasを画面のど真ん中に配置しています。
注目ポイントは image-rendering: pixelated; の1行。これがあると、ブラウザがCanvas画像を拡大表示するときにぼかさずカクカクのまま描画してくれます。ドット絵っぽい雰囲気を出したいときの必須プロパティです。
JavaScriptの基本構造を書こう
ここからがメイン! script.js に書いていくコードを、セクションごとに解説していきます。
定数の設定
// 画面・マップ
const CANVAS_W = 800;
const CANVAS_H = 480;
const TILE = 32; // マップ1マスの大きさ
const FPS = 60;
// 物理
const GRAVITY = 0.45;
const PLAYER_SPEED = 3;
const JUMP_MIN = -6; // 短押しジャンプ
const JUMP_MAX = -10; // 長押し最大
const JUMP_HOLD_MAX = 0.2; // 長押しで伸びる最大秒数
const JUMP_BUFFER_FRAMES = 6; // ジャンプ入力の先行受付
const COYOTE_FRAMES = 8; // 崖を踏み外した後の猶予
ゲームバランスに関わる数字は冒頭にまとめて定数化しておくのが鉄則です。「重力を強くしたい」「ジャンプを高くしたい」と思ったとき、ここの数字を変えるだけで済みます。コードの中に直接 0.45 と書いていると、あとで探すのが大変です。
Canvasの初期化とステートマシン
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// ゲームの状態(ステートマシン)
const STATE_TITLE = 'title';
const STATE_PLAYING = 'playing';
const STATE_BOSS_ENTER = 'boss_enter'; // ボス部屋突入演出中
const STATE_BOSS = 'boss'; // ボス戦中
const STATE_PLAYER_DEAD = 'player_dead';
const STATE_BOSS_DEAD = 'boss_dead';
const STATE_VICTORY = 'victory';
const STATE_GAMEOVER = 'gameover';
let gameState = STATE_TITLE;
シューティングは3状態でしたが、アクションは演出が多いので8状態あります。ボス部屋に入る演出、ボスを倒す演出、勝利演出……と、それぞれが独立した状態として扱えると、コードが整理しやすくなります。これがステートマシン(状態機械)の威力です。
ゲームループを作ろう
ゲームの心臓部、ゲームループを作ります。
let lastTime = 0;
function gameLoop(timestamp) {
// 前フレームからの経過秒数(最大0.05秒でクランプ)
const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
lastTime = timestamp;
try {
update(dt); // ロジック更新
render(); // 画面描画
} catch (e) {
console.error('Game error:', e);
}
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(ts => {
lastTime = ts;
requestAnimationFrame(gameLoop);
});
ここで何をしてる?
requestAnimationFrame は「次の画面更新のタイミングで関数を呼んでね」とブラウザにお願いする関数です。これを繰り返すことで、毎秒約60回画面が更新されるループが完成します。
dt に上限をつけているのがポイント。Math.min(..., 0.05) で0.05秒(50ms)より大きくならないようにしています。なぜかというと、タブを切り替えていた間にゲームが止まっていて、戻ってきたときに「3秒分」の時間差でいきなり動いたら、プレイヤーが画面外までワープしてしまうから。フレーム間隔が極端に空いたときの保険です。
try...catch でエラーを捕まえているのも地味に大事。どこかでバグっても、ループ自体は止まらないので画面が固まりません。
ステージを作ろう(タイルマップ)
シューティングと違って、アクションゲームにはステージがあります。ステージをどう表現するか? 答えは 2次元配列 です。
タイルの種類を定義
const T_AIR = 0; // 空気(なにもない)
const T_BLOCK = 1; // 通常ブロック(四方から壁)
const T_PLATFORM = 2; // 薄い床
const T_MOVING = 3; // 動く床
const T_SPIKE = 4; // トゲ(触れたら即死)
const T_LADDER = 5; // はしご
const MAP_W = 100; // ステージの横マス数
const MAP_H = 15; // ステージの縦マス数
ステージ全体を 横100マス × 縦15マス の格子で表現します。各マスは「何が置いてあるか」を表す番号(T_AIR、T_BLOCKなど)を持ちます。
マップの中身を作る
function buildMap() {
// 全マスを「空気」で初期化
const map = [];
for (let row = 0; row < MAP_H; row++) {
map.push(new Array(MAP_W).fill(T_AIR));
}
// ヘルパー:row行のcol1〜col2をtileで埋める
const fillRow = (row, col1, col2, tile) => {
for (let col = col1; col <= col2; col++) map[row][col] = tile;
};
// 地面
fillRow(14, 0, 99, T_BLOCK);
// 階段
fillRow(13, 15, 17, T_BLOCK);
fillRow(11, 18, 20, T_BLOCK);
fillRow(9, 21, 23, T_BLOCK);
// はしご
for (let row = 8; row <= 12; row++) map[row][47] = T_LADDER;
// トゲ地獄
for (let col = 65; col <= 75; col += 2) map[12][col] = T_SPIKE;
return map;
}
const MAP = buildMap();
💡 座標の変換を覚えよう
プレイヤーの位置はピクセル(px)単位で管理しますが、マップは「マス目」単位です。この2つを変換する式が頻出します。
- ピクセル → マス目:
Math.floor(x / TILE) - マス目 → ピクセル:
col * TILE
たとえば、プレイヤーの足元のタイルを調べたいときは、プレイヤーのX座標を TILE(32px)で割れば、どのマス目にいるか計算できます。
function getTile(col, row) {
if (col < 0 || col >= MAP_W || row < 0 || row >= MAP_H) return T_BLOCK;
return MAP[row][col];
}
マップ外は自動的に「壁」扱いにしているのもコツ。プレイヤーが画面外に飛び出していかないようにする簡単な工夫です。
キー入力を管理しよう
アクションゲームでは「押している間」と「押した瞬間」を区別する必要があります。
const keys = {};
window.addEventListener('keydown', e => {
if (!keys[e.code]) {
keys[e.code] = { pressed: true, justPressed: true };
} else {
keys[e.code].pressed = true;
keys[e.code].justPressed = true;
}
e.preventDefault();
});
window.addEventListener('keyup', e => {
if (keys[e.code]) keys[e.code].pressed = false;
});
function keyDown(code) {
return keys[code] && keys[code].pressed;
}
function keyJust(code) {
if (keys[code] && keys[code].justPressed) {
keys[code].justPressed = false; // 一度読んだらクリア
return true;
}
return false;
}
なぜ2種類必要?
|
| |
|---|---|---|
左右移動 | ✅ 押しっぱなしで走り続ける | ❌ 一歩ずつしか動けない |
ジャンプ | ❌ 離すまで連続ジャンプしちゃう | ✅ 1回押したら1回跳ぶ |
チャージショット | ✅ 押している間にチャージが溜まる | ❌ 溜まらない |
この使い分けを間違えると、「ジャンプが連射されて制御できない」「連続攻撃しても1発しか撃てない」みたいなバグになります。アクションゲーム作りの第一歩です。
プレイヤーを動かそう(物理&衝突判定)
プレイヤークラスの骨格
class Player {
constructor(x, y) {
this.x = x;
this.y = y;
this.w = 20;
this.h = 24;
this.vx = 0; // 水平速度
this.vy = 0; // 垂直速度
this.onGround = false;
this.facing = 1; // 1=右, -1=左
this.hp = PLAYER_HP_MAX;
this.invincible = 0;
// ... ジャンプ・チャージ用の変数も
}
}
プレイヤーは「位置(x, y)」と「速度(vx, vy)」を持ちます。速度を使って位置を動かし、重力で vy をだんだん増やし、壁にぶつかったら止める……これが物理の基本です。
基本の移動処理
_updateNormalMove(input, dt) {
// 左右移動
if (input.left) { this.vx = -PLAYER_SPEED; this.facing = -1; }
else if (input.right) { this.vx = PLAYER_SPEED; this.facing = 1; }
else this.vx = 0;
// 重力(空中なら vy を増やす)
if (!this.onGround) this.vy += GRAVITY;
}
左右キーで水平速度を決めて、空中なら重力で垂直速度を増やす。たったこれだけで跳んで落ちる動きができます。
📐 AABB:四角vs四角の当たり判定
キャラクターと壁の衝突判定には AABB (Axis-Aligned Bounding Box) を使います。これは「縦横に揃った四角形同士が重なっているか」を調べる、一番シンプルな判定方法です。
function rectsOverlap(a, b) {
return a.x < b.x + b.w && a.x + a.w > b.x &&
a.y < b.y + b.h && a.y + a.h > b.y;
}
たった4行の式ですが、これを使い回してプレイヤーvs敵、弾vs敵、プレイヤーvsアイテム……と、ゲーム中の衝突判定をほぼすべて処理しています。
🔑 ここがアクションゲーム最大のコツ:X/Y を分けて処理する
キャラクターとタイルマップの衝突判定は、ちょっとだけ工夫が必要です。
function collideTiles(obj) {
// ① X方向だけ動かす
obj.x += obj.vx;
// → 壁にめり込んでいたら押し戻す
// ② Y方向だけ動かす
obj.y += obj.vy;
// → 床や天井にめり込んでいたら押し戻す
return { onGround, blockedLeft, blockedRight, ... };
}
X方向とY方向を同時に動かしてはいけません。同時にやると、ブロックの「角」に引っかかったときに押し出す方向がわからなくなり、プレイヤーが床に埋まったり壁をすり抜けたりする悪夢のバグが出ます。
X方向だけ先に処理 → 壁判定 → 次にY方向だけ処理 → 床判定、という順番が大事。これはタイルベースゲーム全般のお約束です。
ジャンプを気持ちよくしよう(プロの3テク)
ここが今回の記事で一番伝えたいところです。ジャンプが気持ちいいゲームと気持ち悪いゲームの差は、物理の正しさではなく「小さなズルの積み重ね」で決まります。
テク①:可変ジャンプ高さ
ボタンを長押しするほど高く跳び、軽く押すとちょっとだけ跳ぶ。これがあるだけで操作の自由度が大きく広がります。
// ジャンプ発動(短押しの高さ)
if (canJump) {
this.vy = JUMP_MIN; // -6(弱い上向き)
this.jumping = true;
this.jumpTimer = 0;
}
// ボタンを押し続けている間、最大0.2秒まで vy を強くしていく
if (this.jumping && input.jumpKey) {
this.jumpTimer += dt;
if (this.jumpTimer < JUMP_HOLD_MAX) {
const t = this.jumpTimer / JUMP_HOLD_MAX;
this.vy = JUMP_MIN + (JUMP_MAX - JUMP_MIN) * t; // 線形補間
}
}
// ボタンを途中で離したら上昇を半分カット
if (!input.jumpKey && this.jumping && this.vy < 0) {
this.vy *= 0.5;
}
テク②:コヨーテタイム
崖から落ちた直後、ほんの少しの時間だけジャンプを受け付けてくれる仕組み。名前の由来はアニメの「コヨーテ」が崖を踏み外しても一瞬だけ空中に立っているアレです。
// 地面にいる間は猶予をリセット
if (this.onGround) {
this.coyoteTimer = COYOTE_FRAMES; // 8
} else if (this.coyoteTimer > 0) {
this.coyoteTimer--;
}
// 地面にいる or コヨーテ中 ならジャンプできる
const canJump = this.jumpBuffer > 0
&& (this.onGround || this.coyoteTimer > 0)
&& !this.jumping;
これがないと、「崖ギリギリでジャンプ!」と思ったのに1フレーム遅くて落ちた……という悲しい事故が頻発します。プレイヤーは「押したつもり」が「実は遅かった」ことに気づかないので、ゲームが悪いと感じてしまいます。
テク③:ジャンプバッファ(入力先行受付)
着地する少し前にジャンプボタンを押しても、着地と同時にジャンプしてくれる仕組み。
// ジャンプボタンが押された瞬間、6フレームのバッファを設定
if (input.jumpJust) this.jumpBuffer = JUMP_BUFFER_FRAMES;
// バッファ中 かつ ジャンプできる状態なら発動
if (this.jumpBuffer > 0 && (this.onGround || this.coyoteTimer > 0)) {
// ジャンプ処理
this.jumpBuffer = 0;
}
if (this.jumpBuffer > 0) this.jumpBuffer--;
これがないと、「連続ジャンプしようとして早く押しすぎた → 着地時には入力が消えていて跳ばない」という現象が起きます。プレイヤーは「なんで今のジャンプ反応しなかったの?」と不満を感じます。
💡 この3つは有名なアクションゲームはほぼ全部入れてる
マリオ、ホロウナイト、Celeste……操作感を褒められているアクションゲームには、この3つが必ずと言っていいほど実装されています。プレイヤーは気づかないけれど、ゲームがこっそり助けてくれているのです。
ショットとチャージを実装しよう
ロックマンといえばチャージショット。Zキーを押した時間によって弾の強さが3段階に変わる仕組みを作ります。
const CHARGE_HALF = 0.5; // 半チャージまでの秒数
const CHARGE_FULL = 1.5; // フルチャージまでの秒数
_updateCharge(shootKey, bullets, dt) {
if (shootKey) {
// 押している間はチャージ時間を蓄積
if (!this.charging) {
this.charging = true;
this.chargeTime = 0;
}
this.chargeTime += dt;
} else if (this.charging) {
// ボタンを離した瞬間に発射
this.charging = false;
this.fire(bullets, this.getChargeLevel());
this.chargeTime = 0;
}
}
getChargeLevel() {
if (this.chargeTime >= CHARGE_FULL) return 2; // フル
if (this.chargeTime >= CHARGE_HALF) return 1; // 半
return 0; // 通常
}
3段階の弾
レベル | チャージ時間 | 見た目 | ダメージ | 特徴 |
|---|---|---|---|---|
0(通常) | 〜0.5秒 | 小さな黄色い球 | 1 | 最大3発まで同時発射 |
1(半) | 0.5〜1.5秒 | 青い球+電撃スパーク | 2 | 発射制限なし |
2(フル) | 1.5秒〜 | 脈打つ楕円エネルギー弾 | 3 | 敵を貫通する |
フルチャージだけが貫通するのがポイント。通常弾や半チャージは敵に当たると消えますが、フルチャージは突き抜けて後ろの敵にも当たります。
// 弾vs敵の当たり判定
if (bullet.level < 2) {
bullet.alive = false; // 通常・半チャージは当たると消える
} else {
// フルチャージは貫通(消えない)
spawnExplosion(bullet.x, bullet.y, 6);
}
チャージ中の演出
ただチャージ時間を測るだけじゃなく、見た目と音で分かるようにするのが大事です。
- プレイヤーの周りに青い粒子が集まってくる(チャージパーティクル)
- プレイヤー自身が発光する(
ctx.shadowBlurでグロウエフェクト) - 「ウィィィン」という持続音が鳴る(Web Audio APIのオシレーター)
- フルチャージに達した瞬間に「ピンッ!」と完了音が鳴る
これらがあると、プレイヤーは「今どれくらい溜まってるか」を直感的に理解できます。
敵を作ろう(4種類)
このゲームには4種類の敵がいます。それぞれクラスとして実装します。
メット敵(守備→攻撃の切替)
ヘルメットをかぶって守備中は無敵。3秒ごとに顔を出して3方向に弾を撃ってきます。
class MetEnemy {
constructor(x, y) {
this.hp = 3;
this.phase = 'guard'; // 'guard' or 'attack'
this.timer = 0;
}
update(dt, player, enemyBullets) {
this.timer += dt;
if (this.phase === 'guard') {
if (this.timer >= 3) {
this.timer = 0;
this.phase = 'attack';
}
} else {
// 攻撃モード:3方向に弾を撃つ
if (/* 発射タイミング */) {
const dir = player.x > this.x ? 1 : -1;
enemyBullets.push(new EnemyBullet(this.x, this.y, 0, -3));
enemyBullets.push(new EnemyBullet(this.x, this.y, dir * 3, 0));
enemyBullets.push(new EnemyBullet(this.x, this.y, dir * 2.1, 2.1));
}
}
}
// ガード中は通常弾を跳ね返す!
canHit() {
return this.phase === 'attack';
}
}
ここにもステートマシンが登場しています。ゲーム全体の状態と同じ仕組みを、敵ひとりひとりも持っているわけです。
フライ敵(サイン波で飛ぶ)
プロペラで空を飛び回り、上下にサイン波で揺れる敵。
// サイン波で上下に揺れる
this.y = this.baseY + Math.sin(this.time * 2) * 30;
シューティングの編隊敵と同じ Math.sin() のテクニックです。
グラウンド敵(往復巡回)
地面を左右に走る敵。段差や壁にぶつかったら折り返します。
// 前方に床がなかったら折り返し(穴に落ちない)
const aheadCol = Math.floor((this.x + this.w / 2 + this.vx * 10) / TILE);
const aheadRow = Math.floor((this.y + this.h) / TILE);
if (!isSolid(getTile(aheadCol, aheadRow))) {
this.vx *= -1; // Uターン
}
この「穴を検知して引き返す」ロジック、名前は地味ですが、マリオのクリボーにも同じ仕組みが入っています。
砲台(倒せない固定敵)
画面の天井や壁に固定されていて、周期的に弾を撃つだけの敵。倒せないので避けて通るしかありません。
4種類の敵を実装するだけで、ステージの難易度に大きな幅が生まれます。
ボス戦を実装しよう
ステージ奥にはボスが待ち構えています。ボスのコードは敵の中で一番複雑で、演出と攻撃パターンが盛りだくさんです。
ボス部屋突入の演出
ロックマンといえば、ボス部屋の前のシャッターですね。ここは3段階の演出で見せます。
function updateBossEntry(dt) {
// 1) シャッターを0.1秒ごとに1段ずつ開ける
if (shutterState === 'opening') {
shutterAnimTimer += dt;
if (shutterAnimTimer >= 0.1) {
shutterAnimTimer = 0;
shutterAnimRow++;
soundShutter(); // シャキーン!
if (shutterAnimRow >= SHUTTER_ROWS.length) {
shutterOpen = true;
}
}
}
// 2) シャッターが全開になったらプレイヤーを自動で歩かせる
if (playerAutoWalk && shutterOpen) {
player.vx = PLAYER_SPEED;
player.x += player.vx;
if (player.x > BOSS_ROOM_X + TILE) {
playerAutoWalk = false;
boss = new Boss(BOSS_ROOM_X + 5 * TILE, 4 * TILE);
}
}
// 3) ボスが降下 → HPゲージがピッピッと充填 → 戦闘開始
}
シャッターを1マスずつずらして開けることで、上から下に開いていくアニメーションが作れます。演出中はプレイヤーの操作を奪って自動で歩かせるのもポイント。映画のワンシーンを観ているような気分になります。
ボスのフェーズ切り替え
ボスはHPが半分を切ると攻撃パターンが変わるという、アクションゲームの定番仕様。
class Boss {
constructor(x, y) {
this.hp = BOSS_HP_MAX;
this.phase = 1; // 1 → 2
}
takeDamage(dmg) {
this.hp -= dmg;
// HPが半分を切ったらphase2へ
if (this.hp <= BOSS_HP_MAX / 2 && this.phase === 1) {
this.phase = 2;
// phase2の攻撃パターンに切り替え
}
}
}
phase1では単発の弾、phase2ではレーザー照射も加わる、みたいにゲームが激しくなっていきます。
撃破演出
ボスを倒した瞬間の演出が、このゲームの一番派手な見せ場です。
if (gameState === STATE_BOSS_DEAD) {
bossDeathTimer--;
// 12フレームごとに、だんだん大きくなる爆発を追加
if (bossDeathExpCount < 12 && bossDeathExpTimer % 12 === 0) {
spawnPopExplosions(boss.x, boss.y, boss.w, boss.h, 8 + bossDeathExpCount);
shake(4 + bossDeathExpCount, 10); // 画面揺れも徐々に強く
soundEnemyDie();
bossDeathExpCount++;
}
// クライマックスで画面全体を白くフラッシュ
if (bossDeathTimer === 30) {
flash(255, 255, 255, 1, 30);
}
}
時間経過とともに演出が盛り上がるのがポイント。最初は小さな爆発、徐々に大きく、最後は画面全体が真っ白になる。2秒ちょっとの間ですが、この演出があるかないかで「倒した!」の達成感が全然違います。
エフェクトを追加しよう
このゲームが「本格的に見える」のは演出のおかげです。
パーティクルシステム
爆発・チャージの光・死亡演出、すべてパーティクル(小さな粒子)で表現します。
const particles = [];
function addParticle(x, y, vx, vy, r, color, life, gravity = false) {
particles.push({ x, y, vx, vy, r, color, life, maxLife: life, gravity });
}
function spawnExplosion(x, y, count = 10, colors = ['#fff', '#ff0', '#f80']) {
for (let i = 0; i < count; i++) {
const angle = (Math.PI * 2 * i) / count + Math.random() * 0.3;
const speed = 2 + Math.random() * 3;
const color = colors[Math.floor(Math.random() * colors.length)];
addParticle(x, y,
Math.cos(angle) * speed, Math.sin(angle) * speed,
3 + Math.random() * 3, color,
20 + Math.random() * 15, true);
}
}
粒子を配列に入れておいて、毎フレーム位置を更新して描画する。寿命(life)を減らしていって0になったら消すという単純な仕組みですが、これだけで派手な爆発が作れます。
画面シェイクとフラッシュ
被弾やボス着地の瞬間に画面を揺らす「画面シェイク」、被弾時に一瞬だけ赤く光らせる「フラッシュ」。
// 画面揺れ
function shake(intensity, life) {
screenShake = { intensity, life, maxLife: life, x: 0, y: 0 };
}
// 毎フレームの描画前に、Canvas全体をランダムにずらす
ctx.save();
ctx.translate(screenShake.x, screenShake.y);
// ... 通常の描画 ...
ctx.restore();
たった数行ですが、これがあると被弾の痛みが伝わってくるゲームになります。
サウンドをコードだけで作ろう
シューティングの記事でも触れた Web Audio API 。このゲームでも効果音はすべてコードで生成しています。
単音を鳴らす
function playTone(freq, type, duration, vol = 0.3) {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = type; // 'sine' | 'square' | 'sawtooth' | 'triangle'
osc.frequency.value = freq;
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
// 使い方
function soundShoot() { playTone(880, 'square', 0.05, 0.2); } // ピッ!
function soundJump() { playTone(500, 'square', 0.06, 0.15, 400, 600); }
function soundLadder() { playTone(800, 'square', 0.03, 0.1); }
ロックマンらしい音の作り方
効果音 | 波形 | 周波数 | 特徴 |
|---|---|---|---|
通常ショット | 矩形波(square) | 880Hz | ファミコン風の「ピッ」 |
ジャンプ | 矩形波 | 400→600Hz | 音程が上がる |
被ダメ | 矩形波 | 400→100Hz | 音程が下がる |
チャージ持続音 | のこぎり波 | 80〜240Hz | 「ウィィィン」 |
フルチャージ完了 | サイン波 | 1200+1600Hz | 「ピロン♪」 |
勝利ファンファーレ | 矩形波 | ドミソド↑ | 523,659,784,1047Hz |
矩形波(square)はファミコン音源の音。ロックマン・マリオ・ドラクエ……懐かしいあの音の正体は、矩形波(8bit音源)です。Web Audio APIでも osc.type = 'square' と書くだけで、あの音が作れます。
爆発音はノイズで作る
function playNoise(duration, vol = 0.2) {
const bufferSize = audioCtx.sampleRate * duration;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1; // -1〜+1のランダム
}
// ... バッファを音として再生
}
「ザッ」「バシャッ」という爆発音は、ランダムな数値の波(ノイズ)です。音量を減衰させるだけで、それっぽい爆発音になります。
完成コード全文
以下が3ファイルの完成コードです。そのままコピペして使ってOK! HTMLとCSSは上で書いた通り。JavaScriptは約3,000行あるので、ファイルに直接コピーしてください。
script.js(完成版)
コード全文が非常に長い(約3,000行)ため、以下のリンクからダウンロードしてください。
全コードをコピーして script.js という名前で保存すればOKです。
記事の中で解説した各セクションが、コメント付きで整理されています:
script.js の構成
├── 定数(1-60行)
├── マップデータ(61-155行)
├── キーボード入力(156-200行)
├── 効果音:Web Audio API(201-340行)
├── パーティクルシステム(341-505行)
├── 画面エフェクト:フラッシュ・揺れ(506-545行)
├── タイル当たり判定(546-665行)
├── プレイヤークラス(666-1065行)
├── 弾クラス(1066-1215行)
├── 敵弾クラス(1216-1250行)
├── 敵キャラ4種類(1251-1800行)
├── ボス戦(1801-2400行)
├── カメラ・動く床・アイテム(2401-2600行)
├── ボス部屋突入演出(2601-2695行)
└── ゲームループ・衝突判定・描画(2696-3065行)
動かしてみよう
3つのファイルを保存したら、index.html をダブルクリックしてブラウザで開いてみましょう。
操作キー:
- ←→ キー:左右移動
- ↑ キー / SPACE:ジャンプ(長押しで高く跳ぶ)
- ↑↓ キー:はしごの登り降り
- Z キー:ショット(長押しでチャージ)
もし画面が真っ黒のままだったり、エラーが出る場合は以下をチェックしてみてください:
- 3つのファイルが同じフォルダに入っているか?
- ファイル名は
index.html、style.css、script.jsになっているか? - ブラウザの開発者ツール(F12キー)のConsoleタブにエラーが出ていないか?
次のステップ
アクションゲームが動いたら、次はカスタマイズに挑戦してみましょう!自分でコードをいじって遊ぶのが、プログラミング上達の一番の近道です。
チャレンジアイデア
- ジャンプ力を変えてみる:
JUMP_MAXやGRAVITYをいじって月面みたいな重力にしてみる - ステージを自作する:
buildMap()の中身を書き換えて、自分だけのステージを作る - 新しい敵を追加する:既存の敵クラス(MetEnemy など)を参考に、動きの違う敵を作ってみる
- チャージの段階を増やす:CHARGE_FULL の先に「オーバーチャージ(4段階目)」を追加する
- 武器をパワーアップさせる:倒した敵から武器が手に入る仕組み(ロックマンの「特殊武器」)
- セーブ機能を追加する:
localStorageを使えば、クリア状況をブラウザに保存できる - マップエディタを作る:クリックでタイルを配置できるエディタを別ページで作る
- ボスを2匹に増やす:ステージ中ボスを挟んでみる
わからないことがあったら「JavaScript Canvas ○○」や「JavaScript ゲーム ○○」で検索してみよう。HTML5 Canvasの描画APIはWebのゲーム開発で超よく使うので、覚えておくと他のゲームも作れるようになるよ!
ロックマン風のアクションゲームが作れたら、もうあなたは「ゲームを遊ぶ側」から「ゲームを作る側」に回れています。次は2Dプラットフォーマー、ローグライク、RPG……世界はずっと広いです。