ゲームオブジェクトを使い回す(1)

2023年5月16日

シューティングなどで弾をたくさん作ってしばらくしたら消滅させるような時、InstantiateメソッドとDestroyメソッドを使っていますよね。これらの処理は、新しくメモリを割り当てる処理が必要なので負荷がかかります(といっても、弾は3つしか出てこないくらいでは影響ないです)

パフォーマンスを上げる(負荷を減らす)テクニックとしてオブジェクトプール(object pool)というのがあります


仕組み

遊んでいる人から見ると、新規で作成され、消滅するように見えるようにするのですが、実際には、画面に表示しなくなるだけです。SetActiveメソッドをtrueにするかfalseにするかで実現します

インスペクターでは次のところがそうです

基本

プールを作るスクリプト

最初にオブジェクトをプールする仕組みを作成していきましょう

保管用のリストを作成

使い回すゲームオブジェクトを保管しておくために、GameObject型でリストを作っておきます

List<GameObject> gameObjects = new List<GameObject>();

プレファブ登録用のフィールドを宣言

プレファブをもとに生成しますので、インスペクターから登録できるようにします
[SerializeField]のところは、Public GameObject prefab;とすることもあります

[SerializeField]
GameObject prefab;

インスタンスの生成と無効化

プレファブからインスタンスを生成します
実行時にヒエラルキー(シーン)に出現します
その際、SetActiveをfalseにしていますので、無効化され画面(Gameビュー)には見えません

var obj = Instantiate(prefab);

// 無効化しておく
obj.SetActive(false);

リストに追加

作ったゲームオブジェクトは、リストに追加しておきます

gameObjects.Add(obj);

全てのコード

必要な数だけ作って、リストに保管していきます
まとめると次のようになります

using System.Collections;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;

public class PoolSample : MonoBehaviour
{
    // ゲームオブジェクトを使い回すためにリストに保存しておく
    List<GameObject> gameObjects = new List<GameObject>();

    [SerializeField]
    GameObject prefab;

    void Awake()
    {
        // 最初にゲームオブジェクトを作っておく
        for (int i = 0; i < 5; i++)
        {
            var obj = Instantiate(prefab);

            // 無効化しておく
            obj.SetActive(false);

            // リストに追加
            gameObjects.Add(obj);
        }
    }

プールを使うスクリプト

作ったオブジェクトプールを使ってみましょう

使われていないゲームオブジェクトを取得

使われていない(無効)オブジェクトをリストから探して取得します
登録されている一覧から順番にアクティブであるかをif文で確認していきます
もし、アクティブでなければ(無効ならば)、見つけたことになります

// リストの中から探す
foreach (var obj in gameObjects)
{
    // !がついているのでアクティブでなければと読む
    if (!obj.activeInHierarchy)
    {
        // objゲームオブジェクトは未使用!!
    }
}

上記をメソッドにしておきましょう

// 現在未使用(無効)なゲームオブジェクトを取得
// 戻り値の型は、GameObject型。見つからなければnullを返す
GameObject GetFreeObject()
{
    // リストの中から探す
    foreach (var obj in gameObjects)
    {
        // !がついているのでアクティブでなければと読む
        if (!obj.activeInHierarchy)
        {
            return obj;
        }
    }
    return null;
}

スペースキーを押下したら有効化

スペースを押せば、有効状態に変更されるようにします
SetActive(true)でアクティブ(有効)になります

void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))
    {
        GameObject obj = GetFreeObject();
        obj.SetActive(true);
    }
}

実行して確認

スペースを押すたびにSphere(球)が生成されています
同じ場所に生成されるのでよくわかりませんが、移動させれば重なっていたことがわかります
ヒエラルキーを見ると、オブジェクトが最初無効になっていて、スペースを押すたびに有効になっていくのがわかります
もちろん、登録されているオブジェクトの数以上を作ろうとするとエラーになります
ダメというわけではなく、「もう作れないよ」とか、「登録数を増やす(自動も含め)」などの対応を取ります
今回は、シンプルを目指しているのであえて実装していません

ここまでの全てのコード

using System.Collections.Generic;
using UnityEngine;

public class PoolSample : MonoBehaviour
{
    // ゲームオブジェクトを使い回すためにリストに保存しておく
    List<GameObject> gameObjects = new List<GameObject>();

    [SerializeField]
    GameObject prefab;

    void Awake()
    {
        // 最初にゲームオブジェクトを作っておく
        for (int i = 0; i < 5; i++)
        {
            var obj = Instantiate(prefab);

            // 無効化しておく
            obj.SetActive(false);

            // リストに追加
            gameObjects.Add(obj);
        }
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            GameObject obj = GetFreeObject();
            obj.SetActive(true);
        }
    }

    // 現在未使用(無効)なゲームオブジェクトを取得
    // 戻り値の型は、GameObject型
    GameObject GetFreeObject()
    {
        // リストの中から探す
        foreach (var obj in gameObjects)
        {
            // !がついているのでアクティブでなければと読む
            if (!obj.activeInHierarchy)
            {
                return obj;
            }
        }
        return null;
    }
}

オブジェクトの消滅

1秒後に消えるようにする

今度は、Destroyのようなのを実装しましょう

OnEnableイベントはStartやUpdateと同じくUnityで用意されているものです
有効になった時に実行されます。Startは、新しくインスタンスが作成されたときのみ実行されるので、今回は適切でありません
StartCoroutineは難しいですよね
引数にメソッドが入っています。このメソッドとセットで使います
時間待ちによく利用されます

private void OnEnable()
{
    StartCoroutine(DelayInActive());
}

上記引数のメソッドになります。戻り値はIEnumerator型になります。ここでは深く触れませんが、途中で他に処理を明け渡すと思ってください

yield return new WaitForSeconds(1); では、1秒待つことを指示しています。この次の行は1秒後に実行されます

IEnumerator DelayInActive()
{
    // 1秒待って、その次のコード(SetActiveメソッド)が実行される
    yield return new WaitForSeconds(1);
    gameObject.SetActive(false);
}

まとめると次のようになります

using System.Collections;
using UnityEngine;

public class PrefabControl : MonoBehaviour
{
    private void OnEnable()
    {
        StartCoroutine(DelayInActive());
    }

    IEnumerator DelayInActive()
    {
        // 1秒待って、その次のコード(SetActiveメソッド)が実行される
        yield return new WaitForSeconds(1);
        gameObject.SetActive(false);
    }
}

プレファブにスクリプトをアタッチします

実行してみましょう
1秒後に消えるはずです

表示がわかりにくいので、ランダムに出現するように見せます

// 出現させる座標をランダムに設定
int x = Random.Range(-5, 6);
int y = Random.Range(-5, 6);
int z = Random.Range(-5, 6);

obj.transform.position = new Vector3(x, y, z);

これをUpdateメソッドに追記します

void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))
    {
        GameObject obj = GetFreeObject();
        obj.SetActive(true);
        // 出現させる座標をランダムに設定
        int x = Random.Range(-5, 6);
        int y = Random.Range(-5, 6);
        int z = Random.Range(-5, 6);

        obj.transform.position = new Vector3(x, y, z);
    }
}

カメラは(0, 0, -15)くらいにしておくと見やすいでしょう

実行して確認

実用的なコードを作る

基本は以上です
もう少し実用性があるものに更新してみましょう