横スクロールシューティングは、画面が右から左へと自動スクロールし、右から現れる敵を倒しながら進むゲームです。グラディウスを参考にしたこのゲームを作ることで、スクロール処理・パワーアップシステム・プロシージャル地形生成など、ゲーム開発の実践的な技術が身につきます。
横スクロールシューティングでは、ゲーム全体が毎フレーム少しずつ左にスクロールします。地形(上下の壁)はランダムに生成し、スクロールに合わせて描画します。
var SCROLL_SPEED = 1.0; // 1フレームあたりのスクロール量
var TERRAIN_SEGMENT_W = 20; // 地形の1ブロックの幅
var terrainTop = []; // 上壁の高さの配列
var terrainBottom = []; // 下壁の高さの配列
var terrainOffset = 0; // スクロール量の累積
// ゲームの状態更新: 毎フレーム呼ばれる
function update() {
// スクロール量を増やす
terrainOffset += SCROLL_SPEED;
// 新しい地形が必要になったら追加生成する
extendTerrain();
}
// 地形を伸ばす(プロシージャル生成)
function extendTerrain() {
var needed = Math.ceil((terrainOffset + BASE_W) / TERRAIN_SEGMENT_W) + 5;
while (terrainTop.length < needed) {
var last = terrainTop[terrainTop.length - 1];
// 前のブロックから少しランダムにずれた高さを追加
terrainTop.push(clamp(
last + rand(-4, 4), // ±4ピクセルの変動
10, // 最小の高さ
40 // 最大の高さ
));
}
// terrainBottom も同様に生成
}
terrainOffset は累積スクロール量です。描画するときにこのオフセットを引くことで「画面が右から左へ動いている」ように見せます。地形は先の分まで先読みして生成しておくことで、スクロール中もなめらかに表示できます。
地形は座標計算して折れ線グラフのように描きます。スクロール量 terrainOffset を引くことで、正しい位置に表示できます。
function drawTerrain() {
ctx.fillStyle = '#334455';
ctx.beginPath();
// 現在の表示開始セグメントを計算
var startSeg = Math.floor(terrainOffset / TERRAIN_SEGMENT_W);
// 上壁を描画
ctx.moveTo(0, 0);
for (var i = 0; i <= Math.ceil(BASE_W / TERRAIN_SEGMENT_W) + 1; i++) {
var seg = startSeg + i;
// スクロール位置からのX座標を計算
var x = i * TERRAIN_SEGMENT_W - (terrainOffset % TERRAIN_SEGMENT_W);
var topH = terrainTop[seg] || 10;
ctx.lineTo(x * S, topH * S);
}
ctx.lineTo(BASE_W * S, 0);
ctx.closePath();
ctx.fill();
// 下壁も同様に描画
}
terrainOffset % TERRAIN_SEGMENT_W(余り算)を使うのがコツです。1ブロック分スクロールするごとに startSeg が1増えるので、どこまでスクロールしても正しい地形が表示されます。
グラディウス風のパワーアップシステムの特徴は「カプセルを取るとゲージが進み、好きなタイミングで発動できる」点です。スピード・ミサイル・ダブル・レーザー・オプション・バリアの6種類を管理します。
// パワーアップの種類ラベル
var GAUGE_LABELS = ['SPD', 'MSL', 'DBL', 'LSR', 'OPT', 'BRR'];
var gaugeLevel = 0; // 現在のゲージ位置(0〜5)
var speedLevel = 0; // スピードアップの段階(最大3)
var hasMissile = false;
var hasDouble = false;
var hasLaser = false;
var options = []; // オプション(分身)の配列
var barrierHP = 0; // バリアの残りHP
// カプセルを取ったとき: ゲージを1つ進める
function activateCapsule() {
gaugeLevel = Math.min(gaugeLevel + 1, GAUGE_LABELS.length - 1);
}
// Enterキーを押したとき: 現在のゲージ位置のパワーアップを発動
function activatePowerUp() {
if (gaugeLevel === 0) {
// スピードアップ
speedLevel = Math.min(speedLevel + 1, MAX_SPEED_LEVEL);
} else if (gaugeLevel === 1) {
hasMissile = true;
} else if (gaugeLevel === 2) {
hasDouble = true;
} else if (gaugeLevel === 3) {
hasLaser = true;
} else if (gaugeLevel === 4 && options.length < MAX_OPTIONS) {
options.push({ x: 0, y: 0 }); // オプション追加
} else if (gaugeLevel === 5) {
barrierHP = BARRIER_HITS; // バリア発動
}
gaugeLevel = 0; // 発動したらゲージをリセット
}
パワーアップの状態はすべてフラグ(true/false)や数値で管理します。発射処理のときにこれらのフラグを確認して、装備に応じた弾を追加します。シンプルな設計ですが、ゲームの幅が大きく広がります。
function firePlayerWeapon(fx, fy) {
// 基本弾(常に発射)
playerBullets.push({
x: fx, y: fy,
vx: BULLET_SPEED, vy: 0,
type: 'normal'
});
// ダブル: 斜め上にも弾を追加
if (hasDouble) {
playerBullets.push({
x: fx, y: fy,
vx: BULLET_SPEED * Math.cos(-Math.PI / 6),
vy: BULLET_SPEED * Math.sin(-Math.PI / 6),
type: 'normal'
});
}
// ミサイル: 下向きにミサイルを追加
if (hasMissile) {
playerBullets.push({
x: fx, y: fy,
vx: MISSILE_SPEED * 0.7,
vy: MISSILE_SPEED * 0.7, // 右下方向
type: 'missile'
});
}
}
ミサイルは毎フレーム vy += 0.08 で重力を加えることで、放物線を描いて落ちていきます。三角関数で角度を指定しているので、自由な方向への弾も簡単に追加できます。
グラディウスの特徴的な要素「オプション(分身)」は、プレイヤーの動きを少し遅れてついてきます。これを実現するには、プレイヤーの過去の位置を配列に記録しておく「移動履歴」の仕組みを使います。
var OPTION_TRAIL_DELAY = 15; // オプションが何フレーム遅れてついてくるか
var MAX_OPTIONS = 2;
// プレイヤーの更新処理の中で、位置履歴を記録する
function updatePlayer() {
// ... 移動処理 ...
// 毎フレーム現在位置を配列の先頭に追加
player.posHistory.unshift({ x: player.x, y: player.y });
// 配列が長くなりすぎないよう古い履歴を削除
var maxLen = OPTION_TRAIL_DELAY * (MAX_OPTIONS + 1);
if (player.posHistory.length > maxLen) {
player.posHistory.length = maxLen;
}
// オプションの位置を履歴から取得
for (var oi = 0; oi < options.length; oi++) {
// オプション1は15フレーム前の位置、オプション2は30フレーム前
var trailIdx = OPTION_TRAIL_DELAY * (oi + 1);
if (trailIdx < player.posHistory.length) {
options[oi].x = player.posHistory[trailIdx].x;
options[oi].y = player.posHistory[trailIdx].y;
}
}
}
posHistory は「プレイヤーが通ってきた座標の記録」です。unshift で先頭に追加することで、インデックス0が最新の位置、インデックスが大きいほど過去の位置になります。オプションは OPTION_TRAIL_DELAY 個前の位置に置くだけで、自動的に追従します。
バリアはプレイヤーの周囲を回転するシールドです。角度を毎フレーム増やすだけで実現できます。
var BARRIER_HITS = 3; // バリアが耐えられる被弾数
var barrierAngle = 0; // バリアの現在の回転角度
function updateBarrier() {
// 毎フレーム少しずつ回転
barrierAngle += 0.03;
}
function drawBarrier() {
if (barrierHP <= 0) return;
var px = sc(player.x);
var py = sc(player.y);
var radius = sc(player.w * BARRIER_RADIUS_RATIO);
// バリアの軌道(円)を描画
ctx.beginPath();
ctx.arc(px, py, radius, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(68, 170, 255, 0.4)';
ctx.stroke();
// 残りHP数だけバリアの「粒」を等間隔に配置
for (var i = 0; i < barrierHP; i++) {
var angle = barrierAngle + (i * Math.PI * 2 / BARRIER_HITS);
var bx = px + Math.cos(angle) * radius;
var by = py + Math.sin(angle) * radius;
ctx.beginPath();
ctx.arc(bx, by, sc(3), 0, Math.PI * 2);
ctx.fillStyle = '#44aaff';
ctx.fill();
}
}
バリアの「粒」の位置も三角関数で計算しています。cos(angle) と sin(angle) で円周上の座標が得られるので、HPが減るにつれて粒の数が減る表現も自然に実現できます。
おつかれさまでした!ここまでで横スクロールシューティングの基本が完成しました。スクロール処理でゲームに奥行きを作り、パワーアップシステムでゲームの幅を広げ、移動履歴を使ってオプションを実装しました。さらに発展させるなら、ボスの攻撃パターンを増やしたり、地形の種類を増やしたり、BGMや効果音を追加したりしてみましょう。
A: 主な原因は弾や敵の数が増えすぎることです。このゲームでは MAX_ENEMY_BULLETS のような上限を設けて対処しています。また、画面外に出た弾や敵は配列から削除することも大切です。削除しないとメモリが増え続けてしまいます。
A: 基本部分は約30分〜1時間で作れます。パワーアップの種類を増やしたりステージを追加したりすると、さらに楽しく発展させられます。