Unity本格入門 実装編:Chapter 9 ゲームが楽しくなる効果を付けよう
題材・出典: 技術評論社刊『作って学べる Unity本格入門[Unity 6対応版]』(賀好 昭仁 著)に基づく学習補助の解説です。書籍の代替提供を目的とせず、コード掲載は学習上必要な範囲にとどめます。本シリーズ目次
対象読者:Unity エディタの基本操作と C# のコードを追いながら読める方
この記事では、いきのこバトルのサウンド・パーティクルエフェクト・ポストプロセスを題材に、ゲームに「音と映像の演出」を加える実装を読み解きます。書籍 Chapter 9(9-1〜9-3) に対応します。本シリーズの最終回です。
記事の目次
おすすめの読み方
セクション一覧
- 題材の範囲
- プロジェクトの構成(今回のファイル・アセット)
- クラス関係(この記事で登場する範囲)
- コードを全部見てみよう
- ポイント解説(①〜⑦)
- コードの流れを整理しよう
- 自分でカスタマイズしてみよう!
- まとめ
- シリーズを振り返って
ポイント一覧
- ①
AudioSourceとAudioClipの基本設定(2D / 3D Sound) - ②
AudioMixerのグループとSetFloatで BGM ボリュームを制御する - ③
Resources.LoadAll<AudioClip>で SE を辞書に一括読み込みする - ④
DontDestroyOnLoadでシーンをまたいでAudioManagerを存続させる - ⑤
AudioManager.Instance.Play(clipName)で任意の SE を再生する - ⑥ Particle System の Shape・Emission・Lifetime でヒットエフェクトを作る
- ⑦ Post Processing Volume の Bloom・Vignette でゲームの雰囲気を仕上げる
題材の範囲
Chapter 9 のテーマは「五感に訴える演出」です。移動・攻撃・敵の死亡といった出来事に音を乗せ、映像にエフェクトとポストプロセスを加えることで、同じゲームプレイが劇的に印象を変えます。
| 書籍の節 | テーマ | IkinokoBattle での実装 |
|---|---|---|
| 9-1 | BGM・SE を追加する | AudioManager.cs・SettingManager.cs・MainAudioMixer |
| 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 = true・Loop = 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");
});
}
}① AudioSource と AudioClip の基本設定(2D / 3D Sound)
AudioClip と AudioSource の関係
| コンポーネント | 役割 | 例え |
|---|---|---|
AudioClip |
音声データそのもの | CD・音楽ファイル |
AudioSource |
音声を再生するコンポーネント | CDプレイヤー |
AudioClip を AudioSource.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(damage・hit など)は 2D Sound です。どこで当たってもプレイヤーには同じ音量で聞こえるべき UI 寄りの効果音として扱われています。
BGM の設定
メインシーンには BGM 専用の AudioSource が GameObject に配置されており、PlayOnAwake = true・Loop = 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 として表示させるための属性です。
現状の実装の注意点:
SettingManagerのUpdate()は毎フレーム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.3〜0.5
Start Speed = 2〜5
Start Size = 0.1〜0.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 をヒット位置に複製し、ParticleSystem が Play on Awake であれば自動再生されます。一定時間後に自動削除する場合は Destroy(go, lifetime) を組み合わせます。
⑦ Post Processing Volume の Bloom・Vignette でゲームの雰囲気を仕上げる
IkinokoBattle では URP(Universal Render Pipeline)の Post Processing が使われています。MainScene の Global Volume に SampleSceneProfile.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)
- MainScene に空の GameObject を作成
Volumeコンポーネントをアタッチし、Is Global = trueにするVolume Profileに新規 Profile アセットを作成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 音量を毎フレームではなく変更時だけ更新する
現在の SettingManager は Update() で毎フレーム 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 スライダーの onValueChanged に SettingManager.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 回を通じて登場した設計パターンを整理すると:
- シングルトン(
AudioManager・OwnedItemsData・LifeGaugeContainer) - テンプレートメソッド(
MobStatus→PlayerStatus・EnemyStatus) - 委譲(
CollisionDetector→EnemyMove) - コンポーネント分離(入力・移動・攻撃・状態をそれぞれ別スクリプトに)
これらは Unity 開発で繰り返し登場するパターンです。設計の詳細は OOP設計パターン編 でさらに深掘りしています。
お疲れさまでした。
← シリーズ目次に戻る
← 前の記事:Chapter 8 ユーザーインタフェースを作ってみよう
最終更新:2026年4月