学習記事一覧 · Unity

Unity本格入門 実装編:Chapter 9 ゲームが楽しくなる効果を付けよう

題材・出典: 技術評論社刊『作って学べる Unity本格入門[Unity 6対応版]』(賀好 昭仁 著)に基づく学習補助の解説です。書籍の代替提供を目的とせず、コード掲載は学習上必要な範囲にとどめます。本シリーズ目次

対象読者:Unity エディタの基本操作と C# のコードを追いながら読める方
この記事では、いきのこバトルのサウンド・パーティクルエフェクト・ポストプロセスを題材に、ゲームに「音と映像の演出」を加える実装を読み解きます。書籍 Chapter 9(9-1〜9-3) に対応します。本シリーズの最終回です。


記事の目次

おすすめの読み方

セクション一覧

ポイント一覧


題材の範囲

Chapter 9 のテーマは「五感に訴える演出」です。移動・攻撃・敵の死亡といった出来事に音を乗せ、映像にエフェクトとポストプロセスを加えることで、同じゲームプレイが劇的に印象を変えます。

書籍の節 テーマ IkinokoBattle での実装
9-1 BGM・SE を追加する AudioManager.csSettingManager.csMainAudioMixer
9-2 パーティクルエフェクトを作成する HitEffect(ParticleSystem Prefab)
9-3 画面にエフェクトをかける URP の Post Processing(Bloom・Vignette・Tonemapping)

IkinokoBattle のサウンドファイル一覧

ファイル 種別 用途
arata.mp3 BGM メインゲームのBGM
damage.wav SE ダメージを受けたとき
die.wav SE キャラクターが死亡したとき
hit.wav SE 攻撃がヒットしたとき
pick_item.wav SE アイテムを拾ったとき
swing.wav SE 攻撃(剣を振る)したとき

プロジェクトの構成(今回のファイル・アセット)

スクリプト

ファイル 役割
AudioManager.cs SE を辞書管理し、名前で再生するシングルトン
SettingManager.cs AudioMixer を通じて BGM 音量をリアルタイム制御
CancelButton.cs ボタン押下時に "cancel" SE を再生
OKButton.cs ボタン押下時に "ok" SE を再生

Unity アセット(エディタ上で設定)

アセット 役割
MainAudioMixer.mixer BGM グループの音量を管理する AudioMixer
BGM 用 AudioSource MainScene の GameObject に配置、PlayOnAwake = trueLoop = true
SE 用 AudioSource AudioManager の GameObject にアタッチ
HitEffect(ParticleSystem) 攻撃ヒット時に再生するパーティクルエフェクト
SampleSceneProfile.asset Bloom・Vignette・Tonemapping のポストプロセス設定

クラス関係(この記事で登場する範囲)

【サウンド】
  SettingManager
    └─ AudioMixer.SetFloat("BGMVolume", value)   ← BGM 音量をリアルタイム制御
  AudioManager(DontDestroyOnLoad シングルトン)
    ├─ Awake: Resources.LoadAll<AudioClip>("2D_SE") → Dictionary にキャッシュ
    └─ Play(clipName): AudioSource で SE を再生
  CancelButton / OKButton
    └─ AudioManager.Instance.Play("cancel" / "ok")
【パーティクル】
  MobAttack.OnHitAttack
    └─ Instantiate(HitEffect Prefab, ヒット座標)  ← エフェクト生成
【ポストプロセス】
  Global Volume(MainScene の GameObject)
    └─ SampleSceneProfile.asset
         ├─ Bloom(発光演出)
         ├─ Vignette(画面周辺の暗化)
         └─ Tonemapping(色調整・HDR圧縮)

コードを全部見てみよう

AudioManager.cs

using System;
using System.Collections.Generic;
using UnityEngine;
public class AudioManager : MonoBehaviour
{
    private static AudioManager instance;
    [SerializeField] private AudioSource _audioSource;
    private readonly Dictionary<string, AudioClip> _clips
        = new Dictionary<string, AudioClip>();
    public static AudioManager Instance
    {
        get { return instance; }
    }
    private void Awake()
    {
        if (null != instance)
        {
            Destroy(gameObject);   // 重複インスタンスは自分を削除
            return;
        }
        DontDestroyOnLoad(gameObject);   // シーン遷移後も存続
        instance = this;
        // Resources/2D_SE/ フォルダのクリップをすべて辞書に登録
        AudioClip[] audioClips = Resources.LoadAll<AudioClip>("2D_SE");
        foreach (var clip in audioClips)
        {
            _clips.Add(clip.name, clip);
        }
    }
    public void Play(string clipName)
    {
        if (!_clips.ContainsKey(clipName))
            throw new Exception("Sound " + clipName + " is not defined");
        _audioSource.clip = _clips[clipName];
        _audioSource.Play();
    }
}

SettingManager.cs

using UnityEngine;
using UnityEngine.Audio;
public class SettingManager : MonoBehaviour
{
    [Range(-80, 0)]
    public float BgmVolume = -80f;
    public AudioMixer audioMixer;
    void Update()
    {
        audioMixer.SetFloat("BGMVolume", BgmVolume);
    }
}

CancelButton.cs / OKButton.cs

// CancelButton.cs
[RequireComponent(typeof(Button))]
public class CancelButton : MonoBehaviour
{
    private void Start()
    {
        GetComponent<Button>().onClick.AddListener(() =>
        {
            AudioManager.Instance.Play("cancel");
        });
    }
}
// OKButton.cs
[RequireComponent(typeof(Button))]
public class OKButton : MonoBehaviour
{
    private void Start()
    {
        GetComponent<Button>().onClick.AddListener(() =>
        {
            AudioManager.Instance.Play("ok");
        });
    }
}

AudioSourceAudioClip の基本設定(2D / 3D Sound)

AudioClip と AudioSource の関係

コンポーネント 役割 例え
AudioClip 音声データそのもの CD・音楽ファイル
AudioSource 音声を再生するコンポーネント CDプレイヤー

AudioClipAudioSource.clip にセットして Play() を呼ぶことで音が鳴ります。

2D Sound と 3D Sound の違い

AudioSource の Inspector の Spatial Blend スライダーで切り替えます。

設定 Spatial Blend 特徴 用途
2D Sound 0(完全 2D) 距離・方向に関係なく一定音量 BGM・UI SE・ナレーション
3D Sound 1(完全 3D) 音源から遠いほど小さく、方向から聞こえる 敵の足音・爆発・環境音

IkinokoBattle の AudioManager が管理する SE(damagehit など)は 2D Sound です。どこで当たってもプレイヤーには同じ音量で聞こえるべき UI 寄りの効果音として扱われています。

BGM の設定

メインシーンには BGM 専用の AudioSource が GameObject に配置されており、PlayOnAwake = trueLoop = true に設定されています。シーン起動と同時に BGM が自動ループ再生される最もシンプルな設定です。


AudioMixer のグループと SetFloat で BGM ボリュームを制御する

public class SettingManager : MonoBehaviour
{
    [Range(-80, 0)]
    public float BgmVolume = -80f;
    public AudioMixer audioMixer;
    void Update()
    {
        audioMixer.SetFloat("BGMVolume", BgmVolume);
    }
}

AudioMixer とは

AudioMixer は複数の AudioSource をグループ化して音量・エフェクトを一括管理するアセットです。IkinokoBattle では MainAudioMixer.mixer に「BGMVolume」という Exposed Parameter(公開パラメータ)が設定されています。

Exposed Parameter の仕組み

Window → Audio → Audio Mixer でミキサーを開き、BGM グループの Volume を右クリック → Expose "Volume" to script で公開名(BGMVolume)を付けます。これで audioMixer.SetFloat("BGMVolume", value) でコードから音量を操作できるようになります。

単位はデシベル(dB)

AudioMixer の音量は dB(デシベル) で管理します。0 が最大音量、-80 が事実上の無音です。線形の 0〜1 スライダーを dB に変換するには Mathf.Log10 を使います。

// 例:0〜1 のスライダー値を dB に変換
float dB = Mathf.Log10(Mathf.Max(sliderValue, 0.0001f)) * 20f;
audioMixer.SetFloat("BGMVolume", dB);

[Range(-80, 0)] の属性はエディタ上でスライダー UI として表示させるための属性です。

現状の実装の注意点SettingManagerUpdate() は毎フレーム SetFloat を呼んでいます。実用上は BgmVolume が変わったときだけ呼ぶ(OnValueChanged やプロパティのセッターを使う)方が効率的です。課題 A で改善します。


Resources.LoadAll<AudioClip> で SE を辞書に一括読み込みする

AudioClip[] audioClips = Resources.LoadAll<AudioClip>("2D_SE");
foreach (var clip in audioClips)
{
    _clips.Add(clip.name, clip);
}

Resources.LoadAll とは

Assets/Resources/ フォルダ以下の指定パスにあるアセットをすべてロードします。"2D_SE" を指定すると Assets/Resources/2D_SE/ フォルダ内の AudioClip がすべて配列で返ります。

なぜ辞書(Dictionary)に入れるか

SE を再生するたびに Resources.Load("2D_SE/damage") を毎回呼ぶと、都度ファイルを読み込む処理が発生します。Awake() でまとめてロードして Dictionary<string, AudioClip> にキャッシュしておくと、再生のたびにキー検索するだけで済みます。

Awake 時(1回だけ)
  Resources.LoadAll → damage, die, hit, pick_item, swing を読み込み
  Dictionary に登録: { "damage" → clip, "die" → clip, ... }
Play("damage") 時(毎回)
  Dictionary["damage"] → 即座にクリップ取得 → 再生

Resources フォルダの制約

Resources.Load を使うためにはアセットを必ず Assets/Resources/ 以下に置く必要があります。プロジェクト規模が大きくなると Resources フォルダは管理しにくくなるため、Addressables(アセットバンドル管理システム)への移行が推奨されることもありますが、小〜中規模では Resources が手軽です。


DontDestroyOnLoad でシーンをまたいで AudioManager を存続させる

private void Awake()
{
    if (null != instance)
    {
        Destroy(gameObject);
        return;
    }
    DontDestroyOnLoad(gameObject);
    instance = this;
    // クリップの辞書構築…
}

シーン遷移時に GameObject が破棄される問題

通常、SceneManager.LoadScene() でシーンを切り替えると、前のシーンにあった GameObject はすべて破棄されます。AudioManager が TitleScene で初期化されても、MainScene に遷移した瞬間に消えてしまいます。

DontDestroyOnLoad(gameObject) を呼ぶと、そのオブジェクトはシーン遷移時に破棄されず「シーンをまたぐ特別なオブジェクト」として扱われます。

重複防止との組み合わせが必須

DontDestroyOnLoad だけでは不十分です。MainScene を再ロードすると再び Awake() が走り、2 つ目の AudioManager が誕生しようとします。

if (null != instance)
{
    Destroy(gameObject);   // 後から来た方を自分で削除
    return;
}

この重複チェックと組み合わせることで「どのシーンに遷移しても、常に最初に作られた 1 つの AudioManager だけが生き続ける」シングルトンが完成します(詳しくは OOP設計パターン編・第1回 も参照)。


AudioManager.Instance.Play(clipName) で任意の SE を再生する

SE の再生は AudioManager.Instance.Play("クリップ名") の 1 行で完結します。

// CancelButton.cs
AudioManager.Instance.Play("cancel");
// OKButton.cs
AudioManager.Instance.Play("ok");

例外で「音の設定ミス」を早期発見

public void Play(string clipName)
{
    if (!_clips.ContainsKey(clipName))
        throw new Exception("Sound " + clipName + " is not defined");
    _audioSource.clip = _clips[clipName];
    _audioSource.Play();
}

存在しないクリップ名を渡すと即座に例外を投げます。「SE が鳴らないまま気づかない」ことを防ぐための設計です。開発中にコンソールに赤いエラーが出れば、すぐに誤ったクリップ名の修正に気づけます。

MobAttack.OnAttackStart での SE 再生との比較

MobAttack では AudioSource を直接フィールドに持ち、swingSound.Play() でローカルに再生しています。

方法 特徴 使いどころ
AudioManager.Instance.Play どこからでも呼べる。管理が一元化される UI SE・汎用 SE
AudioSource.Play() 直接呼び出し 3D 空間内の位置で音を鳴らせる。ピッチのランダム化など細かい制御 キャラ固有の SE・3D 音声

⑥ Particle System の Shape・Emission・Lifetime でヒットエフェクトを作る

IkinokoBattle の MainScene には HitEffect という ParticleSystem GameObject が存在し、攻撃がヒットしたときに Instantiate で複製・再生されます。

Particle System の主要モジュール

モジュール 役割 主なパラメータ
Main パーティクル全体の基本設定 Duration(再生時間)・Start Lifetime(粒の寿命)・Start Speed(初速)・Start Color
Emission パーティクルの発生数と頻度 Rate over Time(毎秒発生数)・Bursts(一瞬に大量発生)
Shape パーティクルの発生形状 Sphere(球状)・Cone(円錐)・Box(直方体)
Color over Lifetime 生存中に色を変化させる グラデーションで透明に消えていく
Size over Lifetime 生存中にサイズを変化させる 大きくなる・小さくなる
Renderer 描画設定 Material・Render Mode(Billboard など)

ヒットエフェクトの典型的な設定

Main:
  Duration = 0.5(短時間の爆発)
  Start Lifetime = 0.30.5
  Start Speed = 25
  Start Size = 0.10.3
  Start Color = 白〜オレンジのランダム
Emission:
  Bursts: Count = 20(瞬間的に 20 粒発生)
Shape:
  Shape = Sphere, Radius = 0.1(小さい球から全方向に飛び散る)
Color over Lifetime:
  白 → 透明(フェードアウト)

コードからエフェクトを発生させる

// MobAttack.OnHitAttack 内(実装のイメージ)
public void OnHitAttack(Collider collider)
{
    var targetMob = collider.GetComponent<MobStatus>();
    if (null == targetMob) return;
    targetMob.Damage(1);
    // ヒット位置にエフェクトを生成
    // Instantiate(hitEffectPrefab, collider.transform.position, Quaternion.identity);
}

Instantiate でエフェクト Prefab をヒット位置に複製し、ParticleSystemPlay on Awake であれば自動再生されます。一定時間後に自動削除する場合は Destroy(go, lifetime) を組み合わせます。


⑦ Post Processing Volume の Bloom・Vignette でゲームの雰囲気を仕上げる

IkinokoBattle では URP(Universal Render Pipeline)の Post Processing が使われています。MainScene の Global VolumeSampleSceneProfile.asset が設定されており、以下の 3 つのエフェクトが有効です。

設定値の一覧

エフェクト 設定値 効果
Bloom Threshold: 1, Intensity: 0.25, Scatter: 0.5 明るい部分がじんわり光る
Vignette Intensity: 0.2 画面周辺を暗くする(集中感)
Tonemapping Mode: Neutral HDR カラーを SDR 画面に自然に圧縮

Bloom

輝度が閾値(Threshold)を超えたピクセルを周囲にぼかして広げます。陽光・炎・魔法など「光っているもの」を表現するのに効果的です。Intensity が高いほど広がりが大きくなり、過剰だとゲーム全体がぼやけた印象になるので注意が必要です。

Vignette

画面の四隅を暗くするエフェクトです。映画のような没入感を生み、プレイヤーの視線を画面中央に自然に誘導します。Intensity: 0.2 は控えめな設定で、意識せずに見ると気づかない程度の演出です。

Tonemapping

HDR(High Dynamic Range)描画時の明るさを SDR(Standard Dynamic Range)モニターで表示できる範囲に圧縮します。Neutral モードはコントラストを自然に保ちます。

Post Processing の設定方法(URP)

  1. MainScene に空の GameObject を作成
  2. Volume コンポーネントをアタッチし、Is Global = true にする
  3. Volume Profile に新規 Profile アセットを作成
  4. Add Override から Bloom・Vignette 等を追加してパラメータを調整

カメラの設定も必要:URP では Camera の Post Processing チェックを ON にしておかないとエフェクトが適用されません。


コードの流れを整理しよう

サウンドシステム全体の流れ

TitleScene 起動
  └─ AudioManager(GameObject) Awake()
       ├─ DontDestroyOnLoad → 以降のシーン遷移でも存続
       ├─ instance = this(シングルトン登録)
       └─ Resources.LoadAll<AudioClip>("2D_SE")
            └─ Dictionary に { "cancel","ok","damage","hit","die","swing","pick_item" } 登録
メインシーン起動
  └─ BGM 用 AudioSource(PlayOnAwake)→ arata.mp3 ループ再生
  └─ SettingManager.Update() 毎フレーム
       └─ audioMixer.SetFloat("BGMVolume", BgmVolume)
任意のタイミングで SE 再生
  ├─ CancelButton クリック → AudioManager.Instance.Play("cancel")
  ├─ OKButton クリック    → AudioManager.Instance.Play("ok")
  └─ MobAttack.OnHitAttack → (hitEffectPrefab の Instantiate)

パーティクルエフェクトの発生フロー

MobAttack.OnHitAttack(collider)
  └─ targetMob.Damage(1)
  └─ Instantiate(hitEffectPrefab, ヒット座標, Quaternion.identity)
       └─ ParticleSystem(Play on Awake)
            └─ Burst 発生 → 粒が全方向に飛散
            └─ Color over Lifetime でフェードアウト
            └─ Destroy(gameObject, lifetime) で自動削除

ポストプロセスの適用フロー

MainScene(シーン起動時に 1 回)
  └─ Global Volume(Is Global = true
       └─ SampleSceneProfile.asset
            ├─ Bloom → 輝度 > 1 の部分を発光させる
            ├─ Vignette → 画面四隅を暗化
            └─ Tonemapping → HDR → SDR 変換
カメラ(毎フレームのレンダリング)
  └─ Post Processing = ON → Profile のエフェクトを適用して描画

自分でカスタマイズしてみよう!

課題 A:BGM 音量を毎フレームではなく変更時だけ更新する

現在の SettingManagerUpdate() で毎フレーム SetFloat を呼んでいます。プロパティのセッターで変更時だけ更新するように改善します。

public class SettingManager : MonoBehaviour
{
    public AudioMixer audioMixer;
    private float _bgmVolume = -80f;
    public float BgmVolume
    {
        get => _bgmVolume;
        set
        {
            _bgmVolume = Mathf.Clamp(value, -80f, 0f);
            audioMixer.SetFloat("BGMVolume", _bgmVolume);
        }
    }
    // Update() は不要になるので削除
}

UI スライダーの onValueChangedSettingManager.BgmVolume を紐付ければ、スライダーを動かした瞬間だけ音量が変わります。

課題 B:BGM のフェードイン・フェードアウトを追加する

シーン遷移時に BGM が突然始まる/止まるのを滑らかにします。

using System.Collections;
using UnityEngine;
public class AudioManager : MonoBehaviour
{
    // 既存コードは省略
    public IEnumerator FadeIn(float duration)
    {
        _audioSource.volume = 0f;
        _audioSource.Play();
        float elapsed = 0f;
        while (elapsed < duration)
        {
            _audioSource.volume = Mathf.Lerp(0f, 1f, elapsed / duration);
            elapsed += Time.deltaTime;
            yield return null;
        }
        _audioSource.volume = 1f;
    }
    public IEnumerator FadeOut(float duration)
    {
        float startVolume = _audioSource.volume;
        float elapsed = 0f;
        while (elapsed < duration)
        {
            _audioSource.volume = Mathf.Lerp(startVolume, 0f, elapsed / duration);
            elapsed += Time.deltaTime;
            yield return null;
        }
        _audioSource.Stop();
    }
}

課題 C:ダメージを受けたときに画面を赤くフラッシュさせる

Vignette の強度を一時的に上げることで被ダメージ時の画面演出を追加できます。

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using System.Collections;
public class DamageFlash : MonoBehaviour
{
    [SerializeField] private Volume postProcessVolume;
    private Vignette _vignette;
    private void Start()
    {
        postProcessVolume.profile.TryGet(out _vignette);
    }
    public IEnumerator Flash()
    {
        _vignette.color.Override(Color.red);
        _vignette.intensity.Override(0.6f);
        yield return new WaitForSeconds(0.2f);
        _vignette.color.Override(Color.black);
        _vignette.intensity.Override(0.2f);
    }
}

まとめ

ポイント キーワード
Spatial Blend で 2D Sound(UI SE)と 3D Sound(環境音)を使い分ける AudioSource
AudioMixer の Exposed Parameter に SetFloat で dB 単位の音量を渡す AudioMixer・SetFloat
Resources.LoadAll で一括ロードし Dictionary にキャッシュして高速再生 Resources・GC 回避
DontDestroyOnLoad + 重複チェックでシーンをまたぐシングルトンを実現 MonoBehaviour シングルトン
Play(clipName) の例外で「鳴らない SE」を開発中に即発見 フェイルファスト設計
⑥ Burst Emission + Sphere Shape で衝撃的なヒットエフェクトを作る Particle System
⑦ Bloom・Vignette・Tonemapping の 3 つで画面の「映像品質感」を決める URP Post Processing

Chapter 9 の演出は「プレイヤーが意識せずに感じるもの」です。SE がなければ攻撃の手応えが消え、Bloom を切れば昼光の鮮やかさが失われます。コード量は少なくても、ゲームの体験を左右する重要な要素です。


シリーズを振り返って

このシリーズ「Unity本格入門 実装編」では、いきのこバトルを Chapter 5〜9 の書籍構成に沿って読み解いてきました。

Chapter 学んだ核心
第1回 Ch.5 transform.Rotate * Time.deltaTime で昼夜サイクル。フレームレート非依存の基本
第2回 Ch.6 CharacterController + PlayerInput + Cinemachine。入力・物理・カメラの分離
第3回 Ch.7 NavMesh・Raycast・CollisionDetector・Spawner。1 クラス 1 責任の徹底
第4回 Ch.8 Canvas・DOTween・JSON保存・LifeGauge。UI は「見せ方」と「データ」を切り離す
第5回 Ch.9 AudioManager・Particle System・Post Processing。演出はゲームプレイを際立たせる

5 回を通じて登場した設計パターンを整理すると:

  • シングルトンAudioManagerOwnedItemsDataLifeGaugeContainer
  • テンプレートメソッドMobStatusPlayerStatusEnemyStatus
  • 委譲CollisionDetectorEnemyMove
  • コンポーネント分離(入力・移動・攻撃・状態をそれぞれ別スクリプトに)

これらは Unity 開発で繰り返し登場するパターンです。設計の詳細は OOP設計パターン編 でさらに深掘りしています。

お疲れさまでした。


← シリーズ目次に戻る
← 前の記事:Chapter 8 ユーザーインタフェースを作ってみよう


最終更新:2026年4月