【ゲーム制作】画面フラッシュ・衝撃波エフェクトとは?|被弾と爆発の「効いてる感」を作る演出

画面フラッシュ・衝撃波エフェクトとは?被弾と爆発の効いてる感を作る演出 ゲーム制作

敵を倒したときに、画面がパッと白く光る。爆発の衝撃波が、輪になって広がっていく。どちらも、「効いてる感」を作る、定番の演出です。

この記事では、被弾時に画面全体を一瞬光らせる画面フラッシュと、爆発の中心から輪が広がっていく衝撃波エフェクトについて、実際にC++でクラスを書いて動かしながら解説します。以前紹介したヒットストップやカメラシェイクと同じく、「効いてる感」を作る演出の仲間です。

画面フラッシュ・衝撃波エフェクトとは?

画面フラッシュ・衝撃波エフェクトとは、

・被弾時に画面全体を一瞬明るく光らせて減衰させる「フラッシュ」と、爆発の中心から輪が広がりながら弱まっていく「衝撃波」をまとめたエフェクトシステム

です。どちらも派手な3Dモデルやパーティクルを使わず、数値を1つ持ってフレームごとに減衰させるだけで実装できるのが特徴です。ポストエフェクトのシェーダーに渡す係数として使うイメージです。

実装の中身

フラッシュと衝撃波、それぞれ「発生させる関数」「毎フレーム更新する関数」「今の値を取り出す関数」の3点セットで作ります。

class ScreenFX {
public:
    // ---- 画面フラッシュ ----
    static void TriggerFlash(float intensity, float duration) {
        s_FlashValue = intensity;
        s_FlashDecay = intensity / duration; // durationで0まで落ちる速度
    }

    // ---- 衝撃波(中心から輪が広がる) ----
    static void TriggerShock(float duration, float maxRadius, float strength) {
        s_ShockTimer = duration;
        s_ShockDuration = duration;
        s_ShockMaxRadius = maxRadius;
        s_ShockStrength = strength;
    }

    static void Update(float dt) {
        // フラッシュは実時間で線形に減衰させる
        if (s_FlashValue > 0.0f) {
            s_FlashValue -= s_FlashDecay * dt;
            if (s_FlashValue < 0.0f) s_FlashValue = 0.0f;
        }

        // 衝撃波はタイマーを減らしつつ、輪の半径を広げる
        if (s_ShockTimer > 0.0f) {
            s_ShockTimer -= dt;
            if (s_ShockTimer < 0.0f) s_ShockTimer = 0.0f;
        }
    }

    static float GetFlash() { return s_FlashValue; }

    static float GetShockRadius() {
        if (s_ShockDuration <= 0.0f) return 0.0f;
        float t = 1.0f - (s_ShockTimer / s_ShockDuration); // 0->1で進む
        return s_ShockMaxRadius * t; // 時間とともに輪が広がる
    }

    static float GetShockStrength() {
        if (s_ShockDuration <= 0.0f) return 0.0f;
        float t = s_ShockTimer / s_ShockDuration; // 1->0で減っていく
        return s_ShockStrength * t; // 広がるほど効果は弱まる
    }

private:
    static float s_FlashValue;
    static float s_FlashDecay;
    static float s_ShockTimer;
    static float s_ShockDuration;
    static float s_ShockMaxRadius;
    static float s_ShockStrength;
};

フラッシュはintensity / durationで「1フレームあたりどれだけ減衰させるか」を計算し、毎フレーム引いていくだけのシンプルな線形減衰です。衝撃波は、経過時間の割合tを使って半径は0→最大まで広がり、強さは1→0まで弱まるという、2つの値を逆方向に動かしているのがポイントです。実際に60FPSでシミュレーションした値がこちらです。

=== 被弾フラッシュ(0.3秒で減衰) ===
frame  4: Flash=0.778
frame  8: Flash=0.556
frame 12: Flash=0.333
frame 16: Flash=0.111
frame 20: Flash=0.000

=== 爆発の衝撃波(0.25秒で半径5まで広がる) ===
frame  4: Radius=1.33 Strength=0.0587
frame  8: Radius=2.67 Strength=0.0373
frame 12: Radius=4.00 Strength=0.0160
frame 16: Radius=5.00 Strength=0.0000
frame 20: Radius=5.00 Strength=0.0000

フラッシュは0.3秒(18フレーム)できれいに0まで落ちています。衝撃波は半径が最大の5まで広がりきる(frame16以降)のと同時に、強さもちょうど0になっているのが分かります。輪が広がりきった頃には効果も消えているように調整してあるので、輪だけが取り残されて不自然に見えることがありません。

【重要】実際に踏んだ罠:duration=0で「瞬間フラッシュ」を作ろうとした

この記事用に検証していて、「即座に画面を真っ白にしたいから、減衰時間は0でいいのでは?」と考えて、TriggerFlash(1.0f, 0.0f)のようにdurationに0を渡してみたことがあります。

// NG: duration=0で「瞬間フラッシュ」のつもりで呼んだ場合
ScreenFX::TriggerFlash(1.0f, 0.0f); // 0除算(intensity / duration)

実際に動かしてみると、フラッシュが一瞬も見えないまま終わってしまいました。

=== NG: duration=0で「瞬間フラッシュ」のつもりで呼んだ場合 ===
frame  4: Flash=0.000
frame  8: Flash=0.000
frame 12: Flash=0.000

原因はs_FlashDecay = intensity / durationの0除算です。C++のfloatでは、正の数を0で割るとクラッシュはせず+∞(無限大)になります。次のUpdate()でs_FlashValue -= s_FlashDecay * dtを計算すると、1.0 - 無限大で結果は-∞になり、直後のif (s_FlashValue < 0.0f)の判定に引っかかって即座に0へクランプされます。つまり「瞬間的に真っ白になる」つもりが、実質的に一度も光らないのと同じになっていたのです。

対処法は、duration=0を特別扱いすることでした。「瞬間フラッシュ」を作りたいときは、durationに0を渡すのではなく、ごく短い値(例:1フレーム分の1.0f / 60.0fなど)を渡すか、TriggerFlash側でduration <= 0.0fのときは減衰させず1フレームだけ表示してすぐ0に戻す、といった専用の分岐を用意するのが安全です。

画面フラッシュ・衝撃波のよくある失敗例と対処法

①durationに0を渡して0除算してしまう

上で実演した通りです。intensity / durationのように時間で割る計算をするときは、durationが0になり得ないか必ず確認しましょう。

②連続でTriggerShockを呼んで輪が不自然にリセットされる

今回の実装は、TriggerShockを呼ぶたびにs_ShockTimerを無条件で上書きします。輪が半分くらい広がったところで別の爆発が起きて再度呼ばれると、広がりかけの輪が半径0からいきなりやり直しになり、パッと縮んだように見えてしまいます。同時に複数の衝撃波を扱いたい場合は、単一の変数ではなく配列やリストで複数インスタンスを管理する設計に変える必要があります。

③Update()を呼ぶ順序を間違えて1フレーム遅れる

TriggerFlashを呼んだ同じフレーム内でUpdate()や描画用の値取得(GetFlash()など)を行わないと、「発生させたのに1フレーム目は真っ暗のまま」という違和感につながります。入力判定→エフェクト発生→Update()→描画、の順序を毎フレーム揃えておくのが安全です。

注意点

  • フラッシュはintensity / durationの線形減衰。durationを0にしない
  • 衝撃波は半径0→最大と強さ1→0を同じ経過時間tから逆方向に計算する
  • 輪が広がりきるタイミングと効果が0になるタイミングを揃えておくと不自然さが出ない
  • 複数の衝撃波を同時に扱いたいなら単一変数ではなく配列で管理する

まとめ

  • 画面フラッシュは1つの数値を持って毎フレーム減衰させるだけで実装できる
  • 衝撃波は半径を広げながら強さを弱めるという2方向の変化を同じタイマーから作る
  • duration=0の0除算で「瞬間フラッシュ」のつもりが一度も光らなくなる(実際に踏んだ罠)
  • ヒットストップやカメラシェイクと組み合わせると「効いてる感」がより強まる

画面フラッシュは、1つの数値を持って、毎フレーム減衰させるだけで実装できます。衝撃波は、半径を広げながら、強さを弱めるという2方向の変化を、同じタイマーから作ります。duration=0の0除算で、「瞬間フラッシュ」のつもりが、一度も光らなくなる罠がありました(実際に踏みました)。迷ったら、実行結果を印字して確認してください。

関連記事