【Unity】「攻撃が当たった感じがしない」を直す:ヒットストップ・フラッシュ・揺れ・音のゲームフィール入門

作成: 2026-07-16

攻撃判定は正しく動いてHPも減るのに、遊ぶと手応えがなく「豆腐を叩いているみたい」——それはバグではなく、演出(ゲームフィール)が足りていません。ヒットフラッシュ・パーティクル・SE・カメラシェイク・ヒットストップを同じ一撃へ一枚ずつ重ね、弱・強・クリティカルの強度プリセットまで、ON/OFF比較で「どの演出が何を伝えるか」を体感しながら組み上げます。敵全員が光る・カメラが戻らない・時間が戻らないという定番事故の回避も扱います。

攻撃判定を作り、HPも減るようになった(体力・ダメージ・無敵時間の記事)。数字の上ではゲームは完成へ近づいたはず——なのに、遊んでみると何かが決定的に足りません。剣が敵をすり抜けたようにしか見えない。当たったのかどうか、HPバーを見ないと分からない。俗に言う「豆腐を叩いているみたい」な手応えのなさです。

これはバグではありません。「当たった」という事実をプレイヤーの目と耳に届ける 演出(ゲームフィール) が、まだ無いだけです。この記事では、同じ一撃へヒットフラッシュ・パーティクル・SE・カメラシェイク・ヒットストップを一枚ずつ重ね、ON/OFFで比較しながら「どの演出が何を伝えているのか」を体感できる形まで組み上げます。

剣の一撃に光・火花・揺れの演出が重なるゲームフィールのイメージ

この記事でわかること

  • 手応えのなさの正体——演出は飾りではなく 「情報」 であること
  • フラッシュ・パーティクル・SE・シェイク・ヒットストップの実装と、それぞれが運ぶ情報
  • 定番事故の回避:敵全員が光る(sharedMaterial)・カメラが戻らない(自作シェイクの競合)・時間が戻らない(timeScaleの復元漏れ)
  • 弱・強・クリティカルの3段階プリセットと、やりすぎ防止の考え方

動作確認環境: Unity 6 (6000.x) / Cinemachine 3.x

Sponsored

演出は「情報」——1発のヒットが伝える4つのこと

やみくもに派手にする前に、考え方を1つだけ入れておきます。ヒット演出は飾りではなく、 プレイヤーへ情報を運ぶチャンネル です。攻撃が当たった瞬間、プレイヤーが知りたいことは4つあります。

1発のヒットが4つの情報に分岐する図。命中したか(フラッシュ)、ど�こに当たったか(パーティクルと音)、どれくらい強いか(カメラの揺れ)、今が大事な瞬間か(ヒットストップ)
  • 命中したか——ヒットフラッシュが伝える
  • どこに・何に当たったか——パーティクルとSEが伝える
  • どれくらい強かったか——カメラシェイクが伝える
  • 今が大事な瞬間か——ヒットストップが「読む時間」を作る

この整理があると、あとで「派手すぎる」「うるさい」となった時に、 どれを削っても情報が失われないか で判断できます。逆に「当たったか分かりにくい」なら、足りない情報を運ぶ演出だけをピンポイントで強化できます。

土台:命中の瞬間を1か所に集める

演出を重ねる前に、「命中の瞬間」がコードのどこで確定するのかを決めます。体力・ダメージ記事 の構成をそのまま使うと、役割は自然に2つへ分かれます。

演出の役割分担図。受けた側のHealthはOnDamagedイベントでフラッシュを光らせ、攻撃側のHitboxは命中点でパーティクル・SE・カメラの揺れ・ヒットストップを再生する
  • 受けた側の演出(フラッシュ): HealthOnDamaged イベントを購読する。「どれがダメージを受けたか」は、受けた本人が一番よく知っています
  • 攻撃側の演出(パーティクル・SE・揺れ・ヒットストップ): 攻撃判定(Hitbox)が命中を確定した場所で再生する。「どこに・どの強さで当たったか」は、攻撃側が一番よく知っています

命中の確定が TakeDamage() の1か所に集まっている限り、演出はこの2つの入口から 後付けで何枚でも重ねられます。ここから1枚ずつ足していきましょう。

ヒットフラッシュ——「どれに」当たったか

最初に足すのは、当たった相手を一瞬白く光らせるフラッシュです。実装が軽いわりに「命中した」という最重要の情報を確実に伝える、費用対効果が最も高い演出です。

白く光らせるには、マテリアルの色を明るくするだけでは足りません。Shader Graph記事 で作れる「_FlashAmount プロパティでベース色と白をLerpするシェーダー」を用意して、スクリプトからは値だけを動かします。

ここに定番の罠があります。renderer.sharedMaterial の値を変えると、 同じマテリアルを使う敵全員が一斉に光ります。マテリアルはアセット(共有物)だからです。

sharedMaterialの罠の比較図。左はsharedMaterialを変更したため殴っていない敵まで全員が白く光る。右はMaterialPropertyBlockで命中した1体だけが光る

「このRendererだけ、この値で描いて」という上書き指定が MaterialPropertyBlock です。マテリアル本体を汚さずに、1体だけを光らせられます。

using System.Collections;
using UnityEngine;

public class HitFlash : MonoBehaviour
{
    [SerializeField] private Renderer targetRenderer;
    [SerializeField] private float duration = 0.08f;

    private static readonly int FlashAmount = Shader.PropertyToID("_FlashAmount");
    private MaterialPropertyBlock mpb;
    private Health health;
    private Coroutine current;

    private void Awake()
    {
        mpb = new MaterialPropertyBlock();
        health = GetComponent<Health>();
    }

    private void OnEnable()  { health.OnDamaged += Play; }
    private void OnDisable() { health.OnDamaged -= Play; }

    private void Play()
    {
        if (current != null) StopCoroutine(current); // 連続ヒットは光り直す
        current = StartCoroutine(Run());
    }

    private IEnumerator Run()
    {
        SetAmount(1f);
        yield return new WaitForSeconds(duration);
        SetAmount(0f);
        current = null;
    }

    private void SetAmount(float value)
    {
        targetRenderer.GetPropertyBlock(mpb);
        mpb.SetFloat(FlashAmount, value);
        targetRenderer.SetPropertyBlock(mpb);
    }
}

tips: renderer.material と書くとその場でマテリアルの複製が作られ、個別に光らせること自体はできます。ただし複製の管理が増え、SRP Batcherとの相性も考えどころです。少数ならMaterialPropertyBlockで十分、大量に並ぶ時の使い分けは Shader Graph記事 を参照してください。

パーティクルとSE——「どこに」当たったか

フラッシュだけでは、まだ「点いた・消えた」だけです。次に、命中した位置へ火花のパーティクルと打撃音を足して、「どこに・どんなものに」当たったかを伝えます。

パーティクル自体は パーティクル記事のヒットエフェクトレシピ がそのまま使えます。SEは オーディオ記事 の定石どおり PlayOneShot()——再生中の音を止めずに重ねられるので、連続ヒットでも音が途切れません。

大事なのは 再生位置 です。攻撃者の足元や原点から火花が出ると、一気に嘘くさくなります。命中点は攻撃判定が一番よく知っているので、Hitbox側で確定して渡します。

// AttackHitbox側:命中が確定した行で、接触点を求めて渡す
Vector3 hitPoint = other.ClosestPoint(transform.position);
HitFeedbackPlayer.Instance.Play(profile, hitPoint);

受け取る側は、シーンに置いたパーティクルを命中点へ動かして再生するだけです(全体のコードは実践の節でまとめます)。

// HitFeedbackPlayer側(抜粋)
hitParticle.transform.position = hitPoint;
hitParticle.Play();
seSource.PlayOneShot(profile.hitSe, profile.seVolume);

tips: パーティクルを毎回 Instantiate すると、連打で山ほど生成されます。1個をシーンに置いて使い回すのが最小構成、同時に何か所も出したくなったら オブジェクトプール の出番です。

カメラシェイク——「どれくらい強く」

画面全体がわずかに揺れると、攻撃の「重さ」が伝わります。ここで transform.position を乱数でずらして戻す自作コルーチンに手を出すと、定番の事故が待っています。 連続ヒットでコルーチンが競合し、「ずれた位置」を基準に次の揺れが始まって、カメラが元の位置へ戻らなくなる のです。

Cinemachine を使っているなら、答えはImpulse Systemです。揺れを「発生源が撃つ衝撃波」として扱うため、衝撃が何発重なっても自動で合成され、必ず減衰して元へ戻ります。競合の管理をこちらで書く必要がありません。

using Unity.Cinemachine;

[SerializeField] private CinemachineImpulseSource impulseSource;

public void Shake(float force)
{
    impulseSource.GenerateImpulse(force); // forceで揺れの強さを変えられる
}

セットアップは2つだけです。揺らす側のGameObjectに Cinemachine Impulse Source を、カメラ側(CinemachineCamera)に Cinemachine Impulse Listener を追加します。「撃つ側」と「聞く側」が分かれているので、後述の強度プリセットとも素直につながります。

ヒットストップ——「その瞬間」を読ませる

格闘ゲームやアクションゲームの、「当たった瞬間だけ、ほんの一瞬時間が止まる」あの演出です。数十ミリ秒だけ世界を止めることで、プレイヤーの脳に「今、確かに当たった」を読み取る時間を渡します。

攻撃モーションの時間軸の図。振り始めから当たる瞬間までモーションが進み、命中のフレームでフラッシュとヒットストップが数十ミリ秒だけ入り、解除後にモーションの残りとカメラの揺れの減衰が続く

実装は ポーズ記事 で学んだ Time.timeScale の応用です。ただし「止めたまま待つ」ので、待機は現実時間の WaitForSecondsRealtime でなければなりません。WaitForSeconds はゲーム内時間で数えるため、timeScale = 0 の間は 永遠に終わらない からです。

using System.Collections;
using UnityEngine;

public class HitStop : MonoBehaviour
{
    public static HitStop Instance { get; private set; }

    private Coroutine current;

    private void Awake() { Instance = this; }

    public void Play(float duration)
    {
        if (duration <= 0f) return;
        if (PauseManager.IsPaused) return;            // ポーズ中は開始しない
        if (current != null) StopCoroutine(current);  // 常に1本に絞る(再入は作り直し)
        current = StartCoroutine(Run(duration));
    }

    private IEnumerator Run(float duration)
    {
        Time.timeScale = 0f;
        yield return new WaitForSecondsRealtime(duration);
        if (!PauseManager.IsPaused) Time.timeScale = 1f; // ポーズと衝突させない
        current = null;
    }
}

短いコードですが、本体は2つのガードです。

  • コルーチンを常に1本に絞る: 連続ヒットで2本走ると、先に終わった方が先に timeScale を戻してしまい、後の停止が途中で解けます。「開始時の値を覚えて復元する」方式も、2本目が覚えるのは0なので同じように壊れます。再入時は前の1本を止めて作り直すのが確実です
  • ポーズとの取り決め: ポーズも timeScale = 0 を使うため、無防備に 1f へ戻すと ヒットストップがポーズを勝手に解除する 事故が起きます。「ポーズ中は開始しない・復元前にポーズを確認」の2行で衝突を断っています(PauseManagerポーズ記事 のものです)

tips: ヒットストップ中は、WaitForSeconds で動いているフラッシュも一緒に凍ります。これは良い方向に働きます——白く光った瞬間で時間が止まり、解除とともに消えていく。2つの演出が勝手に同期してくれるのです。

Sponsored

強度プリセット:弱・強・クリティカル

演出が一通り揃ったら、次は 強弱 です。すべての攻撃が同じ揺れ・同じ停止時間だと、通常攻撃と必殺技の区別がつきません。パラメータの束を ScriptableObject のプリセットにして、攻撃ごとに差し替えられるようにします。

using UnityEngine;

[CreateAssetMenu(menuName = "Game/Hit Feedback Profile")]
public class HitFeedbackProfile : ScriptableObject
{
    [Header("音")]
    public AudioClip hitSe;
    [Range(0f, 1f)] public float seVolume = 1f;

    [Header("カメラの揺れ")]
    public float impulseForce = 0.3f;

    [Header("ヒットストップ")]
    public float hitStopDuration = 0.05f;
}

Hit_WeakHit_StrongHit_Critical の3アセットを作って、値の目安から始めてみてください。

プリセット揺れ(force)停止(秒)使いどころ
Weak0.1〜0.20〜0.03通常攻撃・連撃の途中段
Strong0.3〜0.50.05〜0.08コンボの締め・チャージ攻撃
Critical0.6〜1.00.1〜0.15必殺技・とどめ・ボスの大技

大事なのは絶対値ではなく です。弱を控えめにするほど強が引き立ちます。全部を派手にすると、全部が普通になります。

実践:ON/OFF比較シーンで「効き」を確かめる

仕上げに、体力・ダメージ記事のアリーナ(プレイヤーと、案山子代わりの敵)へ全演出を配線し、 1つずつOFFにして「何の情報が失われるか」を体感する 比較シーンを作ります。メトロイドヴァニアの剣戟でも、ベルトスクロールのパンチでも、見下ろしARPGの一撃でも、この配線はそのまま使えます。

比較シーンの図。剣を振る人形が案山子の敵を殴り、画面脇のパネルにフラッシュ・パーティクル・SE・揺れ・ヒットストップのトグルが並んでいて、1つずつ入切して手応えを比べる
using UnityEngine;
using Unity.Cinemachine;

public class HitFeedbackPlayer : MonoBehaviour
{
    public static HitFeedbackPlayer Instance { get; private set; }

    [SerializeField] private ParticleSystem hitParticle;
    [SerializeField] private AudioSource seSource;
    [SerializeField] private CinemachineImpulseSource impulseSource;

    [Header("効果の入切(比較用)")]
    [SerializeField] private bool useParticle = true;
    [SerializeField] private bool useSe = true;
    [SerializeField] private bool useShake = true;
    [SerializeField] private bool useHitStop = true;

    private void Awake() { Instance = this; }

    public void Play(HitFeedbackProfile profile, Vector3 hitPoint)
    {
        if (useParticle)
        {
            hitParticle.transform.position = hitPoint;
            hitParticle.Play();
        }
        if (useSe) seSource.PlayOneShot(profile.hitSe, profile.seVolume);
        if (useShake) impulseSource.GenerateImpulse(profile.impulseForce);
        if (useHitStop) HitStop.Instance.Play(profile.hitStopDuration);
    }
}

攻撃側は、命中を確定した行のすぐ後で1行呼ぶだけです。フラッシュだけは受け手側の HitFlashOnDamaged から勝手に光るので、ここには登場しません。

// AttackHitboxのOnTriggerEnter内、TakeDamageの直後
health.TakeDamage(damage);
HitFeedbackPlayer.Instance.Play(profile, other.ClosestPoint(transform.position));

Playを押して、まず全部ONで数発殴ってみてください。あの「豆腐」の手応えが別物になっているはずです。そうしたら、Inspectorのチェックを1つずつ外してはまた殴ります。SEを切ると急に「当たったか不安」になる。ヒットストップを切ると攻撃が「軽く」なる。シェイクを切ると強攻撃の迫力が消える——どの演出がどの情報を運んでいたのか、体で分かります。逆に、切っても何も変わらない演出があったら、それはこのゲームには要らない演出です。最後に壊れにくさも突いておきましょう。連打してもカメラは必ず元の位置へ戻り(Impulseの仕事)、ヒットストップの最中にポーズを開いて閉じても、時間は正しく流れ続けるはずです。

ポイントは2つ。 命中の確定を1か所(TakeDamage の直後)に集めたから、演出は後から何枚でも重ねられた こと。そして 演出を情報ごとに独立させたから、1つずつ切って「効き」を確かめられる ことです。

やりすぎ防止——酔い・まぶしさ・音圧

ここまで足し算の話ばかりだったので、引き算の基準も置いておきます。

  • 揺れ酔い: シェイクは効果が大きいぶん、頻発すると画面酔いを招きます。連撃の全段で揺らさず「締めの1発だけ」に。設定でシェイク強度を下げられると理想です(設定画面の記事SettingsData に1項目足すだけです)
  • フラッシュの点滅: 高速な明滅は光過敏の引き金になり得ます。全画面のフラッシュは避け、対象だけを短く1回にとどめます
  • 音圧: 連続ヒットでSEが重なると音が割れます。同時再生数を絞るほか、pitch を毎回わずかに乱数でずらすと、重なりが濁らず自然になります(定番テクです)
  • 連続ヒットの間引き: 多段ヒット技を全段で止めると、爽快どころか鈍重になります。ヒットストップは初段と最終段だけ、のような間引きを

おまけ:先に知っておくと良いこと

  • モーションと合わせると跳ね上がる: 攻撃アニメの「当てるフレーム」で判定を開閉する話は Animation Eventの記事 で扱っています。インパクトのフレームでぴたりと止まるヒットストップは、見栄えが一段違います
  • 2Dでも道具立ては同じ: SpriteRenderer用のフラッシュシェーダー、2D用のImpulse、timeScale——この記事の内容は2D/3D共通です
  • ノックバックは物理と相談: 「押し返し」も強力なフィードバックですが、移動処理との喧嘩が起きやすい領域です。Rigidbody記事 の力の加え方を踏まえてから足すのが安全です
  • 被弾側の演出も同じ入口から: この記事は「攻撃を当てた側」の気持ちよさでした。被弾側(画面の赤縁・コントローラー振動・無敵中の点滅)も、同じ OnDamaged から同じ考え方で足せます

まとめ

  • ヒット演出は飾りではなく 情報。命中(フラッシュ)・位置(パーティクルとSE)・強さ(シェイク)・瞬間(ヒットストップ)を運ぶ
  • 命中の確定を1か所に集めれば、演出は後から一枚ずつ重ねられる
  • 定番事故は3つ——sharedMaterialで全員が光る、自作シェイクの競合、timeScaleの復元漏れ。MaterialPropertyBlock・Impulse・「1本に絞る」ガードで断つ
  • 強弱は絶対値ではなく で作る。そして時々1つずつOFFにして、「効き」を点検する

数字の上ではずっと前から動いていた「攻撃が当たる」が、今日はじめて「手応え」になりました。あなたのゲームの一撃は、どの情報をまだ伝え損ねていますか?