Appearance
第07回 アイテムとオブジェクトプール
前回: 第06回 Enemyの状態制御とゲームフィール / 次回: 第08回 GameManagerとゲーム状態 / 発展: AnimatorとPlayerState
今回の授業内容
- Triggerで回復アイテムの取得を検出する
DamageReceiver.ReceiveHeal()を使ってPlayerのHPを回復する- オブジェクトの生成と破棄の負荷を理解する
- Enemyが倒れた位置へオブジェクトプールからアイテムを貸し出す
- 取得後のアイテムをオブジェクトプールに返却する
今回の配布コード
第06回基本版に、回復アイテム、固定容量プール、敵ドロップ処理を追加した第07回版です。全23ファイルを第07回配布コードとしてまとめています。主な変更ファイルは次の通りです。
配布ファイル一覧
| ファイル | 今回の変更 |
|---|---|
| ItemCollector.cs | Player側でTriggerに入ったアイテムを見つける |
| HealItem.cs | 取得条件、回復処理、プールへの返却を扱う |
| HealItemPool.cs | 回復アイテムの事前生成、貸し出し、返却を管理する |
| ItemDropSystem.cs | 敵からのアイテムドロップ依頼を受け付ける |
| EnemyItemDropper.cs | Enemyの死亡時に回復アイテムのドロップを依頼する |
| DamageReceiver.cs | ReceiveHeal()を回復アイテムから利用する |
ヒエラルキーに新規オブジェクトを登録

回復アイテムをプールするHealItemPoolと、Enemyの死亡時にアイテムを出すItemDropSystemが必要です。ヒエラルキー上にHeal Item PoolとItem Drop Systemを作り、各コンポーネントを追加してください。
INFO
システムやマネージャーのような重要なオブジェクトは、Hierarchy上で見つけやすいようにまとめておくことをおすすめします。例えばルートにSystemsやManagersなどの空オブジェクトを作り、その子階層にまとめましょう。
回復アイテムの実装
第04回では、HPの計算をHealth、外部からダメージや回復を受け取る処理をDamageReceiverに実装しました。DamageReceiverには、今後の拡張に備えてReceiveHeal()も実装済みです。
今回は回復アイテムを追加し、既存の回復処理へ接続します。アイテム自身がHPの値を直接変更するのは避け、アイテムオブジェクトとしての責務を果たすことだけに注力します。
| 部品 | 担当すること |
|---|---|
ItemCollector | TriggerでHealItemを検出し、取得を試みる |
HealItem | 取得時の回復処理とプールへの返却を行う |
DamageReceiver | Health.Heal()へ回復計算を任せ、HPが変化したときだけHealedイベントを発行する |
Player | Healedイベントを受け取り、HealthGaugeを更新する |
アイテムドロップと生成破棄のコスト
今回は、Enemyが倒れた位置に回復アイテムを出す処理を実装します。例えば以下のような実装が考えられます。
cs
// アイテムの生成
var item = Instantiate(itemPrefab, dropPosition, Quaternion.identity);
// 取得時
Destroy(item.gameObject);InstantiateはGameObjectとComponentを新しく作り、Destroyは不要になったオブジェクトを破棄します。少数をときどき生成する程度であれば、この方法でもなんの問題もありません。
ですが弾、敵、エフェクト、アイテムのように同じPrefabを何度も生成・破棄する場合は、次の処理が繰り返されます。
GameObjectとComponentの生成Awake()やOnEnable()などの初期化- メモリの確保
- 破棄されたオブジェクトとメモリの回収
短時間に大量の生成と破棄が集中すると処理負荷が高まり、フレーム時間が不安定になる原因になります。こうした場合、あらかじめ作成したオブジェクトを使い回すオブジェクトプールを使うことで、生成と破棄の負荷を回避できます。
オブジェクトプール
オブジェクトプールは、あらかじめ準備された利用可能なオブジェクトを保管し、必要なときに貸し出す仕組みです。使用済みのオブジェクトは破棄せず、無効化してプールに戻します。戻されたオブジェクトは再び貸し出されるまで待機します。
今回作るオブジェクトプールは、ゲーム開始時に必要数をまとめて作成しておく固定容量のプールです。ゲーム中に数が足りなくなっても追加生成せず、利用可能なアイテムがない場合は出現を見送ります。
ゲームとオブジェクト数の管理
ゲームを動作させるコンピューターは有限の機械です。無制限にオブジェクトを出現させていては処理速度が落ち、フレーム時間が不安定になり、最悪の場合はメモリ不足でクラッシュします。オブジェクトの最大出現数を固定することで、フレーム時間の安定化とメモリ使用量の上限を確保するのは定番の手法です。
今回はオブジェクトの最大数管理まで含めてオブジェクトプールが責任を担います。
INFO
オブジェクトプールが同時生成数の管理まで行うかはゲームの仕様次第です。ここでは固定容量のプールを作ることで同時に出現する回復アイテムの数を制御していますが、プールは単に処理負荷の削減に注力した方がよい場合もあります。
オブジェクトプールの実装
今回のオブジェクトプールでは、次の2つのコレクションを使います。
| コレクション | 管理する内容 | 使う理由 |
|---|---|---|
Queue<HealItem> | 利用可能なアイテム | 先に戻したアイテムから順番に再利用する |
HashSet<HealItem> | 現在Scene上へ貸し出しているアイテム | 貸し出し中のアイテムを記録し、同じアイテムの二重返却を防ぐ |
Queueは、先に追加した要素から順番に取り出すコレクションです。利用可能なアイテムを順番に再利用する用途に適しています。
この回で扱うプール
Unityにはより汎用的なプールを作るためのAPIもあります。実務では、プールを固定容量にするか、足りないときに追加生成するか、返却時にどこまで状態を戻すかなどを、ゲームの仕様に合わせて判断実装しなければなりません。
今回のプールは参考のため、あくまで簡素で直接的に、固定容量で実装しています。より汎用的なプールの自作にも挑戦してみてください。
敵のドロップアイテムの実装
今回は、Enemyが倒れたときに回復アイテムを落とす処理を実装します。ドロップされたHealItemのライフサイクルは以下の通りです。
| タイミング | HealItemの状態 |
|---|---|
| ゲーム開始時 | Prefabから必要数を事前生成し、無効化してプールへ保管する |
| Enemyが倒れたとき | プールから1つ取り出し、Enemyの位置へ移動して有効化する |
| 取得されたとき | 無効化してプールへ返し、次の利用を待つ |
アイテムドロップ処理の担当
Enemyが倒れたときにアイテムを落とすとしても、EnemySpawnerにアイテム管理まで管理させる必要はありません。EnemySpawnerはEnemyを生成する役割に留め、アイテムドロップは別のコンポーネントで扱います。
| 部品 | 担当すること |
|---|---|
Enemy | HPと死亡処理を扱う |
EnemyItemDropper | Enemyの死亡を検出し、アイテムドロップを依頼する |
ItemDropSystem | Scene上のアイテムドロップ処理を受け付ける |
HealItemPool | HealItemの事前生成、貸し出し、返却を管理する |
今後アイテム出現の仕様が増えても、EnemyItemDropper相当の処理を増やすことで対応可能となります。
ItemCollectorでアイテムを回収する
回復アイテムがPlayerクラスを直接操作する実装では、例によってPlayerの責務が増えてしまいます。今回は、Player側に「アイテムを取得する役割」を持つItemCollectorを追加します。ItemCollectorがTriggerでアイテムを見つけ、取得を試みます。
cs
using UnityEngine;
// アイテムを取得する役割を持つ
[RequireComponent(typeof(DamageReceiver))]
public class ItemCollector : MonoBehaviour
{
public DamageReceiver DamageReceiver { get; private set; }
private void Awake()
{
DamageReceiver = GetComponent<DamageReceiver>();
}
private void OnTriggerEnter(Collider other)
{
// Triggerに入った相手が回復アイテムかどうかを確認する
var item = other.GetComponentInParent<HealItem>();
if (item == null)
return;
// 取得できるかどうかの判定は、アイテム側に任せる
item.TryCollect(this);
}
}ItemCollectorは、同じGameObjectにあるDamageReceiverを取得し、アイテム取得時に使えるようにします。RequireComponentによって、ItemCollectorを追加したGameObjectにはDamageReceiverも必要であることを明示しています。回復アイテムはItemCollectorから渡された情報を使えば、取得した相手と回復先の両方を判断できます。
回復アイテムの取得
HealItemにはTrigger Colliderを設定します。Playerが範囲内に入ると、Player側のItemCollectorがHealItemを見つけ、TryCollect()を呼びます。
cs
using UnityEngine;
// 取得した相手のHPを回復し、使用後はプールへ戻る
public class HealItem : MonoBehaviour
{
[Tooltip("取得した相手を回復する量")]
[SerializeField] private int healAmount = 3;
private HealItemPool _pool;
private bool _isCollected;
// このアイテムを管理するプールを記録する
public void Initialize(HealItemPool pool)
{
_pool = pool;
}
// プールから貸し出される前に取得可能な状態へ戻す
public void PrepareForSpawn()
{
_isCollected = false;
}
public bool TryCollect(ItemCollector collector)
{
if (_isCollected)
return false;
// 回復先は、取得した相手のItemCollectorから受け取る
var receiver = collector.DamageReceiver;
// 死亡中やHPが満タンの場合は取得しない
if (receiver.IsDead ||
receiver.CurrentHealth >= receiver.MaxHealth)
return false;
_isCollected = true;
receiver.ReceiveHeal(healAmount);
// Destroyせず、管理元のプールへ返す
_pool.Release(this);
return true;
}
}HPが満タンなら取得しない
HPが満タンの相手へReceiveHeal()を呼んでも、Health.Heal()によって最大HPを超えないように制限されます。今回は回復が必要な場合だけ取得できるようにします。
cs
if (receiver.IsDead ||
receiver.CurrentHealth >= receiver.MaxHealth)
return false;ここでは回復量を計算しているわけではなく「アイテムを取得できるかどうか」を判定しています。今回はHPが最大の時はアイテムを回収しないようにしていますが、取得条件は好みで変えてみてください。
消滅時にDestroyしない
取得後はDestroy(gameObject)を呼ばず、管理元のHealItemPoolへ自分自身を返します。アイテムは無効化されてプールへ戻り、次に必要なときに再利用されます。これで破棄と再生成の負荷を回避できます。
cs
receiver.ReceiveHeal(healAmount);
_pool.Release(this);HealItemPoolを実装する
cs
using System.Collections.Generic;
using UnityEngine;
// HealItemの事前生成、貸し出し、返却を管理する
public class HealItemPool : MonoBehaviour
{
[Tooltip("事前生成する回復アイテムのPrefab")]
[SerializeField] private HealItem itemPrefab;
[Tooltip("ゲーム開始時に用意する個数")]
[SerializeField] private int poolSize = 5;
private readonly Queue<HealItem> _availableItems = new();
private readonly HashSet<HealItem> _activeItems = new();
// ゲーム開始時に必要な個数をまとめて生成する
private void Awake()
{
for (var i = 0; i < poolSize; i++)
CreateItem();
}
// Prefabから生成し、利用可能なアイテムとして保管する
private void CreateItem()
{
var item = Instantiate(itemPrefab, transform);
item.Initialize(this);
item.gameObject.SetActive(false);
_availableItems.Enqueue(item);
}
// 利用可能なアイテムを指定位置へ配置する
public bool TrySpawn(Vector3 position, Quaternion rotation)
{
// 固定容量のプールなので、足りない場合は追加生成しない
if (_availableItems.Count == 0)
return false;
var item = _availableItems.Dequeue();
// 貸し出し中のアイテムはScene直下へ出し、指定位置に置く
item.transform.SetParent(null);
item.transform.SetPositionAndRotation(
position,
rotation);
item.PrepareForSpawn();
_activeItems.Add(item);
item.gameObject.SetActive(true);
return true;
}
// 使用済みのアイテムを無効化してプールへ戻す
public void Release(HealItem item)
{
// 貸し出し中ではないアイテムの二重返却を防ぐ
if (!_activeItems.Remove(item))
return;
item.gameObject.SetActive(false);
item.transform.SetParent(transform);
_availableItems.Enqueue(item);
}
}ゲーム開始時に事前生成する
Awake()で、poolSizeの数だけHealItemを作成します。
INFO
こうした事前準備の処理をPrewarmと呼ぶことがあります。
cs
private void Awake()
{
for (var i = 0; i < poolSize; i++)
CreateItem();
}生成直後のアイテムはSetActive(false)で無効化し、Queueへ追加します。非アクティブなGameObjectは表示や物理判定の対象から外れ、Update()なども呼び出されません。Triggerイベントも発生しません。
Enemyの位置へアイテムを貸し出す
TrySpawn()は、指定された位置を受け取り、利用可能なアイテムを1つ貸し出します。固定容量のプールなので、利用可能なアイテムがない場合は何も生成せず、falseを返します。
cs
var item = _availableItems.Dequeue();取り出したアイテムはプールの子階層から外し、Enemyが倒れた位置へ移動します。その後、PrepareForSpawn()で再利用に必要な状態を初期化し、_activeItemsへ追加してから有効化します。
取得されたアイテムを返却する
Release()は、取得されたアイテムを_activeItemsから外します。その後、GameObjectを無効化してプールの子階層へ戻し、利用可能なQueueの末尾へ追加します。
cs
item.gameObject.SetActive(false);
item.transform.SetParent(transform);
_availableItems.Enqueue(item);同じアイテムを誤って2回返却するとQueueへ重複登録されるため、HashSet.Remove()に成功した場合だけ返却処理を続けます。
ItemDropSystemで敵ドロップを実装する
回復アイテムは一定時間ごとに出すのではなく、Enemyが倒れたときに落とします。Enemy本体にはHPと死亡処理の責務を残し、ドロップ処理はEnemyItemDropperへ分けます。
ItemDropSystemでドロップ依頼を受け取る
Scene上にItemDropSystemを1つ置き、アイテムドロップの窓口にします。今回は、ItemDropSystemがHealItemPoolを持ち、指定位置へ回復アイテムを出します。
cs
using UnityEngine;
// Scene上のアイテムドロップ処理を受け付ける
public class ItemDropSystem : MonoBehaviour
{
[Tooltip("回復アイテムを貸し出すプール")]
[SerializeField] private HealItemPool healItemPool;
public void DropHealItem(Vector3 position)
{
// プールが空の場合、TrySpawnはfalseを返して何も出さない
healItemPool.TrySpawn(position, Quaternion.identity);
}
}ItemDropSystemは、どのプールを使ってアイテムを出すかを管理します。EnemyやEnemySpawnerは、プールの中身や固定容量の管理を知る必要がありません。
EnemyItemDropperで死亡時に依頼する
EnemyItemDropperはDamageReceiverのダメージイベントを受け取り、Enemyが死亡したときだけItemDropSystemへドロップを依頼します。Enemy Prefabへ追加しておけば、EnemySpawner以外の方法で生成されたEnemyでも同じように動作します。
cs
using UnityEngine;
// Enemyが倒れたときに回復アイテムのドロップを依頼する
[RequireComponent(typeof(DamageReceiver))]
public class EnemyItemDropper : MonoBehaviour
{
[Tooltip("回復アイテムを落とす位置の補正")]
[SerializeField] private Vector3 itemDropOffset = Vector3.up * 0.5f;
private DamageReceiver _damageReceiver;
private ItemDropSystem _itemDropSystem;
private bool _dropped;
// 死亡判定に使うDamageReceiverと、Scene上のドロップ管理を取得する
private void Awake()
{
_damageReceiver =
GetComponent<DamageReceiver>();
_itemDropSystem =
FindAnyObjectByType<ItemDropSystem>();
if (_itemDropSystem == null)
Debug.LogWarning("SceneにItemDropSystemがありません");
}
// 有効になったときにダメージイベントの購読を開始する
private void OnEnable()
{
_dropped = false;
_damageReceiver.Damaged += OnDamaged;
}
// 無効になったときにダメージイベントの購読を解除する
private void OnDisable()
{
_damageReceiver.Damaged -= OnDamaged;
}
// 死亡した瞬間に1回だけ回復アイテムのドロップを依頼する
private void OnDamaged(int damage, int maxHealth)
{
// 死亡していない場合と、すでにドロップ済みの場合は何もしない
if (!_damageReceiver.IsDead || _dropped)
return;
_dropped = true;
_itemDropSystem?.DropHealItem(
transform.position + itemDropOffset);
}
}FindAnyObjectByType<ItemDropSystem>()は、Scene上にあるItemDropSystemを探します。この回では、Sceneに1つだけItemDropSystemを置く前提にします。見つからない場合は、Consoleへ警告を表示します。
次回のGameManagerとの関係
第08回でGameManagerを追加した後は、GameManagerからItemDropSystemを参照し、ゲーム中だけアイテムを落とす、ゲームオーバー中は落とさない、といった制御へ拡張できます。GameManagerにプール処理を直接書くのではなく、ItemDropSystemを管理対象として扱うと責務を分けたまま拡張できます。
プール実装を汎用化する場合
今回のHealItemPoolは、回復アイテム専用の固定容量プールです。授業では、プールの中身と動作を追いやすくするために、Queue<HealItem>とHashSet<HealItem>を直接扱っています。
より多くのPrefabやエフェクトへ同じ仕組みを適用する場合は、より高度な実装が必要になるかもしれません。
| 要求 | 例 |
|---|---|
| 容量 | 固定数にするか、足りないときに追加生成するか |
| 再利用時の初期化 | Coroutine、Rigidbody、Particle System、Materialの停止とリセットの方法 |
| 所有関係 | プールが生成したオブジェクトだけ返却を受け付けるための機構 |
| 失敗時の扱い | プールを消費しきっている場合の対応 |
Unityには専用のオブジェクトプールコレクションUnityEngine.Pool.ObjectPool<T>も用意されています。ただし、それを使っても「取得時に何を初期化するか」「返却時に何を戻すか」「容量をどのように扱うか」という設計判断は必要です。
今回の実装はあくまで最小構成です。自身のゲーム制作で何が必要かを考えてみてください。
再利用時に状態をリセットする
プールされたオブジェクトは、新しく作り直されるわけではありません。前回利用したときのフィールド、Transform、Coroutine、Materialなどの状態が残ります。
今回のHealItemでは、取得済みであることを表す_isCollectedが残っていると、再び取得できません。そこで、プールから貸し出す直前にPrepareForSpawn()を呼び、初期状態へ戻します。
cs
public void PrepareForSpawn()
{
_isCollected = false;
}初期化を貸し出し処理の一部として明示することで、どのタイミングで状態を戻すのかが分かりやすくなります。再利用するオブジェクトでは、どの状態を返却時または再貸し出し時に戻す必要があるかを必ず確認します。
| 状態 | リセット例 |
|---|---|
| 取得済みフラグ | falseへ戻す |
| Rigidbodyの速度 | Vector3.zeroへ戻す |
| Particle System | 停止して先頭へ戻す |
| Coroutine | 停止する |
| HP | 最大値へ戻す |
| Material | 初期値へ戻す |
HealItem Prefabを設定する
- Sphereや任意のModelを使って
Heal Itemを作ります - Colliderを追加し、
Is Triggerを有効にします HealItemを追加し、healAmountを設定しますHeal ItemをPrefab化します- Scene上の元オブジェクトは削除します
PlayerにはItemCollectorを追加します。Playerには第03回からRigidbodyとDamageReceiverがあるため、Triggerイベントが発生する条件も満たしています。
HealItemPoolとItemDropSystemを設定する
- 空の
GameObjectを作り、Heal Item Poolと名付けます HealItemPoolを追加しますitemPrefabへHeal Item Prefabを割り当てますpoolSizeを設定します- 空の
GameObjectを作り、Item Drop Systemと名付けます ItemDropSystemを追加しますhealItemPoolへHeal Item Poolを割り当てます- Enemy Prefabへ
EnemyItemDropperを追加します
poolSizeより多くのアイテムを同時にScene上へ出すことはできません。今回は、ゲーム中に追加生成を行わない固定容量のプールとして実装しています。
Play Modeで貸し出しと返却を確認する
HierarchyでHeal Item PoolとScene直下を確認し、アイテムの有効状態と親子関係を観察します。
| 操作・時間経過 | 確認する内容 |
|---|---|
| Play開始直後 | poolSize個の非アクティブなHeal Itemがプールの子にある |
| Enemyを倒す | 1個がプールの子階層から外れ、Enemyが倒れた位置で有効になる |
| HP満タンで触れる | アイテムは取得されず、その場に残る |
| ダメージ後に触れる | HPとHPゲージが回復し、アイテムが無効化されてプールの子階層へ戻る |
| 別のEnemyを倒す | 同じInstanceが別のEnemyの位置で再利用される |
| アイテムを取らずにEnemyを倒し続ける | poolSizeを超える数のHeal Itemは表示されない |
Play Mode中にInstance IDを確認すると、新しいオブジェクトを増やさず、同じInstanceを再利用していることを確認できます。
実習:回復アイテムをプールから再利用する
ItemCollectorを作り、Playerへ追加しますHealItemを作り、Trigger Colliderを設定しますHealItemPoolを作り、ゲーム開始時にアイテムを事前生成しますItemDropSystemを作り、HealItemPoolを割り当てますEnemyItemDropperをEnemy Prefabへ追加します- PlayerのHPが満タンの場合は取得できないことを確認します
- ダメージを受けた後に取得し、HPとHPゲージが回復することを確認します
- 取得したアイテムが破棄されず、プールから再利用されることを確認します
動作しない場合のチェックリスト
アイテムが出現しない
itemPrefabへHeal Item Prefabが割り当てられているか- Scene上に
ItemDropSystemがあるか ItemDropSystemのhealItemPoolへHeal Item Poolが割り当てられているか- Enemy Prefabに
EnemyItemDropperが追加されているか - Enemyが倒れているか
poolSizeが0になっていないか- すべてのHeal Itemが貸し出し中になっていないか
アイテムを取得できない
- Heal ItemのColliderで
Is Triggerが有効になっているか - Playerに
ItemCollectorとDamageReceiverがあるか - PlayerまたはHeal Itemに
Rigidbodyがあるか - PlayerのHPが最大値まで回復済みではないか
ItemCollector.OnTriggerEnter()からHealItem.TryCollect()が呼ばれているか- Heal ItemをSceneへ直接置かず、
HealItemPoolから出現させているか
取得後に再出現しない
HealItem.Initialize()でプールが設定されているか- 取得時に
HealItemPool.Release()が呼ばれているか - 貸し出し時に
PrepareForSpawn()が呼ばれているか ItemDropSystemのhealItemPoolが外れていないかpoolSizeが同時に残るアイテム数に対して小さすぎないか
完成条件
- 回復アイテムを取得するとPlayerのHPとHPゲージが回復する
- HPが満タンのときは回復アイテムを取得しない
- ゲーム開始時に指定数のHeal Itemを事前生成する
- Enemyが倒れた位置へ利用可能なHeal Itemを配置する
- 取得後のHeal Itemを破棄せずプールへ返す
- 再利用時に取得済み状態をリセットする
Instantiate/Destroyとオブジェクトプールの違いを説明できる
まとめ
ItemCollectorはTriggerでHealItemを見つけ、取得を試みる- 回復アイテムは取得時に
DamageReceiver.ReceiveHeal()を呼ぶ - HPの回復計算は既存の
Healthへ任せる - 同じPrefabを繰り返し使う場合は、事前生成したInstanceを再利用できる
- Enemyが倒れたときに、プールから回復アイテムを貸し出せる
- プールは利用可能なオブジェクトと貸し出し中のオブジェクトを分けて管理する
- 使用済みのオブジェクトは無効化してプールへ返す
- 再利用するオブジェクトは、前回の状態を明示的にリセットする
次回へ向けて
第08回ではGameManagerを追加し、ゲーム全体の状態を管理します。プレイ中、ゲームクリア、ゲームオーバーといったゲームの状態はゲームの細かな構成要素に任せず、GameManagerが一元的に管理するのが基本的な手法です。