ソースコード解説・目次

CHAPTER 10 · 全23項目

崩れるバリアの実装

小さなマスの配列で盾を表し、敵弾が触れた場所と周囲を削ります。

主な参照先:makeBarriers / barrierHit / erodeBarriers / drawBarriers

1枚の画像ではなく、マスの集まり

4つのバリア。小さなマスが消えることで穴が開く。
4つのバリア。小さなマスが消えることで穴が開く。

バリアは14列 × 10行、1マス4 pxの格子です。g[r][c] が1なら存在し、0なら穴です。生成時に上の角と下中央を切り抜き、描画時は値が残っているマスだけを塗ります(塗った結果は画像として取っておきます。最後の節を参照)。

game.html · L913–929ソースを見る ↗
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);
    }

画面座標をマスの番号に変える

game.html · L1398–1407ソースを見る ↗
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 を付けます。

game.html · L930–931ソースを見る ↗
    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 で始まるので、最初の描画で一度だけ塗られます。

game.html · L1846–1863ソースを見る ↗
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 にします。編隊がバリアに重なっている間は毎フレーム呼ばれるので、もう消えたマスで描き直しを起こさないためです。