CHAPTER 10 · 全23項目
崩れるバリアの実装
小さなマスの配列で盾を表し、敵弾が触れた場所と周囲を削ります。
主な参照先:makeBarriers / barrierHit / erodeBarriers / drawBarriers
1枚の画像ではなく、マスの集まり

バリアは14列 × 10行、1マス4 pxの格子です。g[r][c] が1なら存在し、0なら穴です。生成時に上の角と下中央を切り抜き、描画時は値が残っているマスだけを塗ります(塗った結果は画像として取っておきます。最後の節を参照)。
function makeBarriers() {
barriers = [];
const cols = 14, rows = 10, cell = 4, bw = cols * cell;
for (let i = 0; i < 4; i++) {
const x = (W * (i + 1)) / 5 - bw / 2, y = H - 150;
const g = [];
for (let r = 0; r < rows; r++) {
const line = [];
for (let c = 0; c < cols; c++) {
let solid = 1;
const cut = 4 - r; // 上部の角を斜めに落としたアーチ形
if (cut > 0 && (c < cut || c >= cols - cut)) solid = 0;
if (r >= rows - 3 && c >= 4 && c < cols - 4) solid = 0; // 下の切り欠き
line.push(solid);
}
g.push(line);
}画面座標をマスの番号に変える
function barrierHit(rect, blast) {
for (const b of barriers) {
if (!hit(rect, b)) continue;
const c0 = Math.max(0, Math.floor((rect.x - b.x) / b.cell));
const c1 = Math.min(b.cols - 1, Math.floor((rect.x + rect.w - 0.01 - b.x) / b.cell));
const r0 = Math.max(0, Math.floor((rect.y - b.y) / b.cell));
const r1 = Math.min(b.rows - 1, Math.floor((rect.y + rect.h - 0.01 - b.y) / b.cell));
for (let r = r0; r <= r1; r++) {
for (let c = c0; c <= c1; c++) {
if (!b.g[r][c]) continue;まず敵弾とバリア全体の四角形を調べ、その内側で弾が重なる列・行だけを走査します。座標からバリアの原点を引き、マスの大きさで割って切り捨てると列番号になります。右端と下端の 0.01 は境界の次のマスまで含めないための調整です。
命中点の周囲を削る
実体のあるマスを見つけたら、blast の範囲を調べます。命中点と上下左右は確実に削り、周辺の一部は乱数で削るため、毎回少し違う穴になります。最初の命中を処理したら true を返し、敵弾の処理がその弾を消します。
barrierHit() を呼ぶのは敵弾の処理だけです。自機の弾は通常弾・3方向ショット・連結ビームのどれもバリアを素通りするので、自分の射撃でバリアが削れることはありません。バリアは敵弾から自機を守るための盾です。
敵弾のほかに、編隊がバリアへ下りてきた場合は erodeBarriers() が敵と重なったマスを削ります。合体レーザーとボムは barrierHit() を通らないため、バリアを壊しません。次のウェーブでは makeBarriers() で作り直します。
削れたときだけ描き直す
バリア4つのマスを毎フレーム1つずつ塗ると、400回ほどの fillRect() になります。けれどもバリアの形が変わるのは弾が当たったときだけです。そこで各バリアに専用の画像 img を持たせ、形が変わったことを示す印 dirty を付けます。
const img = barrierImgs[i] ??= prerender(bw, rows * cell, () => {});
barriers.push({ x, y, w: bw, h: rows * cell, cell, cols, rows, g, img, dirty: true });画像はウェーブをまたいで使い回すので、barrierImgs に4枚だけ作っておきます。??= は「まだ無ければ作って入れる」という書き方です。新しいバリアは dirty: true で始まるので、最初の描画で一度だけ塗られます。
function drawBarriers() {
for (const b of barriers) {
if (b.dirty) {
const g = b.img.getContext("2d");
g.clearRect(0, 0, b.w, b.h);
for (let r = 0; r < b.rows; r++) {
for (let c = 0; c < b.cols; c++) {
if (!b.g[r][c]) continue;
g.fillStyle = (r + c) % 2 ? "#2fd47e" : "#3ef08c";
g.fillRect(c * b.cell, r * b.cell, b.cell, b.cell);
}
}
b.dirty = false;
}
ctx.drawImage(b.img, b.x, b.y);
}
}
drawBarriers() は dirty のバリアだけを画像に塗り直し、印を下ろします。あとは毎フレーム、画像を drawImage() で4回貼るだけです。印を立てるのはマスを消す2か所で、barrierHit() は削ったあとに、erodeBarriers() は実際に残っていたマスを消したときだけ b.dirty = true にします。編隊がバリアに重なっている間は毎フレーム呼ばれるので、もう消えたマスで描き直しを起こさないためです。