弾幕シューティングは、プレイヤーが大量の弾を避けながら敵を倒すシューティングゲームです。このゲームを作ることで、Canvas APIを使った描画処理や、三角関数を使った弾の動きの計算など、ゲーム開発で欠かせない技術が身につきます。
弾幕シューティングはHTMLの <canvas> 要素を使って描画します。まずキャンバスを取得して、ゲームの基本ループを作りましょう。ゲームループとは、毎フレーム(1秒間に60回)「画面を消す→更新する→描く」を繰り返す仕組みです。
var BASE_W = 400;
var BASE_H = 600;
var canvas = document.getElementById('game-canvas');
var ctx = canvas.getContext('2d');
// スマホの高解像度ディスプレイに対応するためデバイスピクセル比を使う
function resizeCanvas() {
var wrapper = canvas.parentElement;
var rect = wrapper.getBoundingClientRect();
var w = rect.width;
var h = rect.height;
var dpr = window.devicePixelRatio || 1;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
// BASE_W = 400 を基準にすべての座標を拡大縮小する
scale = canvas.width / BASE_W;
}
// ゲームループ: requestAnimationFrame で毎フレーム呼ばれる
function gameLoop() {
update(); // 位置・状態を更新
draw(); // 画面に描画
requestAnimationFrame(gameLoop);
}
scale という変数がポイントです。ゲームの座標はすべて BASE_W = 400 を基準に計算しておき、描画するときだけ scale 倍して実際のピクセル数に変換します。こうすることで画面サイズが変わっても同じゲームロジックが使えます。
宇宙の雰囲気を出すために、流れる星を作りましょう。星を配列で管理して、毎フレーム少しずつ下に移動させます。
var STAR_COUNT = 60;
var stars = [];
function initStars() {
stars = [];
for (var i = 0; i < STAR_COUNT; i++) {
stars.push({
x: Math.random() * BASE_W,
y: Math.random() * BASE_H,
size: Math.random() * 1.5 + 0.5,
speed: Math.random() * 0.4 + 0.1,
brightness: Math.random() * 0.7 + 0.3
});
}
}
function updateStars() {
for (var i = 0; i < stars.length; i++) {
var s = stars[i];
s.y += s.speed; // 下に流れる
// 画面下に出たら上からまた降ってくる
if (s.y > BASE_H) {
s.y = 0;
s.x = Math.random() * BASE_W;
}
}
}
星を画面下まで動かしたら上に戻すことで、無限にスクロールしているように見えます。speed と size を変えることで遠近感も演出できます。
弾幕シューティングの醍醐味は多彩な弾のパターンです。三角関数(Math.sin・Math.cos・Math.atan2)を使うと、円形・スパイラル・自機狙いなど様々な弾パターンが作れます。
// 自機狙い弾: 敵からプレイヤーへの角度を計算して発射
function fireAimed(x, y, speed) {
// Math.atan2 で「敵→プレイヤー」の角度(ラジアン)を求める
var angle = Math.atan2(player.y - y, player.x - x);
spawnEnemyBullet(x, y,
Math.cos(angle) * speed, // X方向の速度
Math.sin(angle) * speed // Y方向の速度
);
}
// 円形弾: 360度を等分して全方向に発射
function fireCircle(x, y, count, speed) {
for (var i = 0; i < count; i++) {
// count 個の弾を等間隔で並べる
var angle = (Math.PI * 2 / count) * i;
spawnEnemyBullet(x, y,
Math.cos(angle) * speed,
Math.sin(angle) * speed
);
}
}
// スパイラル弾: 毎フレーム角度をずらして渦巻きを作る
function fireSpiral(x, y, count, speed, baseAngle) {
for (var i = 0; i < count; i++) {
var angle = baseAngle + (Math.PI * 2 / count) * i;
spawnEnemyBullet(x, y,
Math.cos(angle) * speed,
Math.sin(angle) * speed
);
}
}
// boss.spiralAngle += 0.08; // フレームごとに少しずつ角度を増やす
Math.atan2(dy, dx) は「dx・dyで表される方向の角度」を返します。これで「敵がプレイヤーの方向を向く」計算ができます。スパイラルはフレームごとに baseAngle を少しずつ増やすことで、弾が螺旋状に広がります。
弾はオブジェクトの配列で管理します。毎フレームすべての弾を移動させ、画面外に出たら削除します。
var enemyBullets = [];
var MAX_ENEMY_BULLETS = 300; // 弾数の上限(パフォーマンス対策)
function spawnEnemyBullet(x, y, vx, vy, color) {
// 上限を超えたら生成しない(処理が重くなるのを防ぐ)
if (enemyBullets.length >= MAX_ENEMY_BULLETS) return;
enemyBullets.push({
x: x, y: y,
vx: vx, vy: vy, // 速度(ベクトル)
color: color,
radius: 3
});
}
function updateEnemyBullets() {
// 後ろから削除するのがポイント(添字がずれないように)
for (var i = enemyBullets.length - 1; i >= 0; i--) {
var b = enemyBullets[i];
b.x += b.vx;
b.y += b.vy;
// 画面外に出たら削除
if (b.x < -20 || b.x > BASE_W + 20 ||
b.y < -20 || b.y > BASE_H + 20) {
enemyBullets.splice(i, 1);
}
}
}
配列から要素を削除するとき、前から削除すると添字がずれてバグになります。for ループを後ろから回すことで安全に削除できます。これはゲーム開発でとてもよく使うテクニックです。
弾幕シューティングでは「プレイヤーの当たり判定を小さくする」のがゲームデザインの重要なポイントです。見た目より小さな当たり判定(ヒットボックス)にすることで、弾をギリギリかすらせる爽快感が生まれます。
var PLAYER_HITBOX_R = 2; // 自機の当たり判定の半径(とても小さい!)
var GRAZE_RADIUS = 15; // かすり判定の半径
function updateEnemyBullets() {
for (var i = enemyBullets.length - 1; i >= 0; i--) {
var b = enemyBullets[i];
b.x += b.vx;
b.y += b.vy;
if (player.alive && player.invincible <= 0) {
// 円同士の距離で当たり判定(三平方の定理)
var dx = b.x - player.x;
var dy = b.y - player.y;
var d = Math.sqrt(dx * dx + dy * dy);
if (d < PLAYER_HITBOX_R + b.radius) {
// 当たった!
playerHit();
enemyBullets.splice(i, 1);
continue;
}
// かすり判定: 当たらなかったが近くを通った場合はスコア加算
if (!b.grazed && d < GRAZE_RADIUS) {
b.grazed = true;
score += 10; // かすりボーナス
}
}
}
}
当たり判定に Math.sqrt(dx*dx + dy*dy)(三平方の定理)を使います。2つの円の中心間の距離が、半径の合計より小さければ衝突です。シンプルですが、ゲームで最もよく使う当たり判定の一つです。
被弾後の無敵時間を実装することで、連続ダメージを防いでゲームのリズムが生まれます。
var PLAYER_INVINCIBLE_TIME = 120; // 120フレーム = 約2秒間無敵
var MAX_LIVES = 3;
var lives = MAX_LIVES;
function playerHit() {
lives--;
if (lives <= 0) {
gameOver();
return;
}
// 無敵時間をセット
player.invincible = PLAYER_INVINCIBLE_TIME;
// 爆発エフェクトを生成
for (var k = 0; k < 15; k++) {
particles.push(createParticle(player.x, player.y, '#ff8844'));
}
}
function drawPlayer() {
// 無敵中は点滅(4フレームごとに表示/非表示を切り替え)
if (player.invincible > 0 && Math.floor(player.invincible / 4) % 2 === 0) {
return; // このフレームは描画しない
}
// ... 通常の描画処理
}
Math.floor(player.invincible / 4) % 2 という計算で、4フレームごとに0と1が交互に切り替わります。これを使って点滅表現を実現しています。無敵中は当たり判定のチェック自体をスキップすることで、完全に安全な時間を作ります。
おつかれさまでした!ここまでで弾幕シューティングの基本が完成しました。Canvas APIでゲーム画面を作り、三角関数で多彩な弾パターンを実装し、当たり判定とライフ管理でゲームとして遊べるようにしました。さらに発展させるなら、ボスのHPに応じて攻撃パターンを変えるフェーズ制や、爆発パーティクルエフェクト、ステージ構成などに挑戦してみましょう。
A: 大丈夫です!使うのは Math.sin・Math.cos・Math.atan2 の3つだけです。「角度を指定したら、その方向への進み具合をxとyで教えてくれる」くらいの理解で始められます。使いながら自然と身についていきますよ。
A: 基本部分は約30分〜1時間で作れます。弾パターンを増やしたりボスを作ったりするとさらに楽しく発展させられます。ひなテックの教室でサポートを受けながら進めることもできます。