攻撃判定を作り、HPも減るようになった(体力・ダメージ・無敵時間の記事)。数字の上ではゲームは完成へ近づいたはず——なのに、遊んでみると何かが決定的に足りません。剣が敵をすり抜けたようにしか見えない。当たったのかどうか、HPバーを見ないと分からない。俗に言う「豆腐を叩いているみたい」な手応えのなさです。
これはバグではありません。「当たった」という事実をプレイヤーの目と耳に届ける 演出(ゲームフィール) が、まだ無い だけです。この記事では、同じ一撃へヒットフラッシュ・パーティクル・SE・カメラシェイク・ヒットストップを一枚ずつ重ね、ON/OFFで比較しながら「どの演出が何を伝えているのか」を体感できる形まで組み上げます。
この記事でわかること
- 手応えのなさの正体——演出は飾りではなく 「情報」 であること
- フラッシュ・パーティクル・SE・シェイク・ヒットストップの実装と、それぞれが運ぶ情報
- 定番事故の回避:敵全員が光る(sharedMaterial)・カメラが戻らない(自作シェイクの競合)・時間が戻らない(timeScaleの復元漏れ)
- 弱・強・クリティカルの3段階プリセットと、やりすぎ防止の考え方
動作確認環境: Unity 6 (6000.x) / Cinemachine 3.x
演出は「情報」——1発のヒットが伝える4つのこと
やみくもに派手にする前に、考え方を1つだけ入れておきます。ヒット演出は飾りではなく、 プレイヤーへ情報を運ぶチャンネル です。攻撃が当たった瞬間、プレイヤーが知りたいことは4つあります。

- 命中したか——ヒットフラッシュが伝える
- どこに・何に当たったか——パーティクルとSEが伝える
- どれくらい強かったか——カメラシェイクが伝える
- 今が大事な瞬間か——ヒットストップが「読む時間」を作る
この整理があると、あとで「派手すぎる」「うるさい」となった時に、 どれを削っても情報が失われないか で判断できます。逆に「当たったか分かりにくい」なら、足りない情報を運ぶ演出だけをピンポイントで強化できます。
土台:命中の瞬間を1か所に集める
演出を重ねる前に、「命中の瞬間」がコードのどこで確定するのかを決めます。体力・ダメージ記事 の構成をそのまま使うと、役割は自然に2つへ分かれます。

- 受けた側の演出(フラッシュ):
HealthのOnDamagedイベントを購読する。「どれがダメージを受けたか」は、受けた本人が一番よく知っています - 攻撃側の演出(パーティクル・SE・揺れ・ヒットストップ): 攻撃判定(Hitbox)が命中を確定した場所で再生する。「どこに・どの強さで当たったか」は、攻撃側が一番よく知っています
命中の確定が TakeDamage() の1か所に集まっている限り、演出はこの2つの入口から 後付けで何枚でも重ねられ ます。ここから1枚ずつ足していきましょう。
ヒットフラッシュ——「どれに」当たったか
最初に足すのは、当たった相手を一瞬白く光らせるフラッシュです。実装が軽いわりに「命中した」という最重要の情報を確実に伝える、費用対効果が最も高い演出です。
白く光らせるには、マテリアルの色を明るくするだけでは足りません。Shader Graph記事 で作れる「_FlashAmount プロパティでベース色と白をLerpするシェーダー」を用意して、スクリプトからは値だけを動かします。
ここに定番の罠があります。renderer.sharedMaterial の値を変えると、 同じマテリアルを使う敵全員が一斉に光ります。マテリアルはアセット(共有物)だからです。

「この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つの演出が勝手に同期してくれるのです。
強度プリセット:弱・強・クリティカル
演出が一通り揃ったら、次は 強弱 です。すべての攻撃が同じ揺れ・同じ停止時間だと、通常攻撃と必殺技の区別がつきません。パラメータの束を 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_Weak・Hit_Strong・Hit_Critical の3アセットを作って、値の目安から始めてみてください。
| プリセット | 揺れ(force) | 停止(秒) | 使いどころ |
|---|---|---|---|
| Weak | 0.1〜0.2 | 0〜0.03 | 通常攻撃・連撃の途中段 |
| Strong | 0.3〜0.5 | 0.05〜0.08 | コンボの締め・チャージ攻撃 |
| Critical | 0.6〜1.0 | 0.1〜0.15 | 必殺技・とどめ・ボスの大技 |
大事なのは絶対値ではなく 差 です。弱を控えめにするほど強が引き立ちます。全部を派手にすると、全部が普通になります。
実践:ON/OFF比較シーンで「効き」を確かめる
仕上げに、体力・ダメージ記事のアリーナ(プレイヤーと、案山子代わりの敵)へ全演出を配線し、 1つずつOFFにして「何の情報が失われるか」を体感する 比較シーンを作ります。メトロイドヴァニアの剣戟でも、ベルトスクロールのパンチでも、見下ろしARPGの一撃でも、この配線はそのまま使えます。

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行呼ぶだけです。フラッシュだけは受け手側の HitFlash が OnDamaged から勝手に光るので、ここには登場しません。
// 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にして、「効き」を点検する
数字の上ではずっと前から動いていた「攻撃が当たる」が、今日はじめて「手応え」になりました。あなたのゲームの一撃は、どの情報をまだ伝え損ねていますか?