🐇

【Unity】 Prefabを動的生成してからAwakeが実行されるまでの間に任意の処理を行う方法

に公開

はじめに

皆さんこんにちは、ambrでCTO / クライアントエンジニアを務めているampです!
この記事ではUnityでAwake()を任意タイミングで実行する方法を紹介します。

Awake()の挙動

UnityのAwake()は、MonoBehaviourのライフサイクルメソッドの1つで、オブジェクトの初期化時に自動的に呼び出されます。その挙動は、オブジェクトの生成方法によって異なります。

Sceneに配置済みのオブジェクト

Sceneに配置済みのGameObjectの場合、Sceneがロードされた時点で、GameObject および ComponentがActiveであればAwake()が呼び出されます。

動的にInstantiate()した場合

Instantiate()で動的に生成した場合、Awake()は生成処理の完了直後、Instantiate()の呼び出しが完了する前に同期的に実行されます:

var obj = Instantiate(prefab); // この行の実行中にAwake()が呼ばれる

// ここに到達する時点で、既にAwake()は完了している

つまり、Instantiate()を呼び出した直後には、すでにAwake()の処理が完了しているため、生成後にパラメータを設定してからAwake()で使用する、といった処理ができません。

このような挙動のため、以下の要件を満たしたい場合、Awake()で初期化処理を完結させることができません。

  • Sceneに配置したPrefabのSerializeFieldの値を調整したい
    • 後述の例では、配置したモンスターのHP係数を調整したいとき等
  • 動的にPrefabを生成した場合にもパラメータを指定したい
  • 初期化処理をAwake()に統一したい
  • 初期化時点(=Componentが生成された時点)で同期的に当該パラメータを使った処理が完了していてほしい
※ 補足:後述に似た別解はあるものの、運用上課題があります

Prefabを非Active状態で保存し、Instantiate()後に任意処理を実行してからSetActive(true)することで実現できますが、
Prefab編集時に非Activeに設定し忘れてしまうと処理結果が変わってしまうため事故が怖い方法になります。

しかし、この問題を汎用的に解決する方法があります。

Awake()を任意タイミングで実行する方法

前提

Prefabにアタッチされているコンポーネントで、
動的に生成されるComponentではAwake()の前にSetMaxHpRate()を呼び出したいものとします。

例として最大HP 50のスライムのPrefabと、最大HP 100のゴブリンのPrefabを用意します。
alt text
alt text

Sceneには一部のHP係数を調整して、以下の設定でPrefabを配置しました。

  • Slime MaxHpRate: 1.0f
  • Goblin MaxHpRate: 1.0f
  • Goblin MaxHpRate: 2.0f
using UnityEngine;

public class Monster : MonoBehaviour
{
    [SerializeField]
    private float _maxHpRate = 1.0f;

    [SerializeField]
    private float _baseMaxHp = 10f;

    private float _maxHp = 10f;
    private float _hp = 10f;

    private void Awake()
    {
        // maxHpRateを使用した処理
        UpdateMaxHp();

        _hp = _maxHp;

        Debug.Log($@"Monster Awake() _maxHp: {_maxHp}, _maxHpRate: {_maxHpRate}");
    }

    public void SetMaxHpRate(float maxHpRate)
    {
        this._maxHpRate = maxHpRate;

        UpdateMaxHp();
    }

    private void UpdateMaxHp()
    {
        _maxHp = _baseMaxHp * _maxHpRate;
    }
}

実装

Sceneに予め非Activeな状態のGameObjectを用意します。
ここではInactiveParentと命名します。

MonsterFactoryをSceneの適当なGameObjectにアタッチしてください。
Prefabと、最終的に配置したいアクティブな親Transformをアタッチします。
加えてInactiveParentをMonsterFactoryの_inactiveParentにアタッチします。

alt text
Hierarchy設定例

alt text
Inspector設定例

MonsterFactoryのCreate()を呼び出すと、Awake()の前にSetMaxHpRate()の呼び出しを実現できます。

using UnityEngine;

public enum MonsterType
{
    Slime,
    Goblin,
}

public class MonsterFactory : MonoBehaviour
{
    [SerializeField]
    private Monster _slimePrefab;
    [SerializeField]
    private Monster _goblinPrefab;
    [SerializeField, Header("生成したオブジェクトの親")]
    private Transform _monsterParent;
    [SerializeField, Header("非ActiveのGameObject。生成処理用に用いる")]
    private Transform _inactiveParent;

    private void Start()
    {
        // テスト生成
        Create(MonsterType.Slime, new Vector3(2f, 0f, 0f), 1.25f);
        Create(MonsterType.Goblin, new Vector3(4f, 0f, 0f), 1.5f);
    }

    public Monster Create(MonsterType monsterType, Vector3 position, float maxHpRate = 1.0f)
    {
        Monster prefab = GetPrefab(monsterType);
        if (prefab == null) return null;

        // 非Activeの状態で生成される
        Monster monster = Instantiate(prefab, _inactiveParent);

        // 任意の処理。このタイミングではAwake()は実行されない
        monster.SetMaxHpRate(maxHpRate);

        // 本来の親にSetParent()する。このタイミングでAwake()が実行される
        monster.transform.SetParent(_monsterParent);

        monster.transform.position = position;

        return monster;
    }

    private Monster GetPrefab(MonsterType monsterType)
    {
        switch (monsterType)
        {
            case MonsterType.Slime:
                return _slimePrefab;
            case MonsterType.Goblin:
                return _goblinPrefab;
            default:
                return null;
        }
    }
}

実行

以下はMonster.Awake()で出力される実行ログです。

上から3行がSceneに配置されていたログで、残り2行がMonsterFactoryで生成されたオブジェクトです。
Scene上で設定したMaxHpRate、プログラムから指定したMaxHpRateいずれもAwake()の時点で反映されていることが確認できます。

Monster Awake() _maxHp: 200, _maxHpRate: 2
Monster Awake() _maxHp: 50, _maxHpRate: 1
Monster Awake() _maxHp: 100, _maxHpRate: 1
Monster Awake() _maxHp: 62.5, _maxHpRate: 1.25
Monster Awake() _maxHp: 150, _maxHpRate: 1.5

解説

Instantiate()の第二引数に親となるTransformを指定できますが、
上記の方法では非Activeの親を指定する点がポイントで、生成したGameObjectを非Activeの状態で生成しています。
これにより、Instantiate()と共に実行されるAwake()の発火を防いでいます。

生成後に任意処理を行い、本来の親の配下に移動させています。
Prefab側に特別な下準備が不要な点がこの方法の良い点といえます。

補足
なお、以下のコードは上記と等価ではなく、SetMaxHpRate()よりAwake()が先に実行されてしまう結果になってしまいます。

    // ✕ うまくいかない例
    public Monster Create(MonsterType monsterType, Vector3 position, float maxHpRate = 1.0f)
    {
        Monster prefab = GetPrefab(monsterType);
        if (prefab == null) return null;

        Monster monster = Instantiate(prefab);  // このタイミングでAwakeが実行される
        monster.transform.SetParent(_inactiveParent);

        monster.SetMaxHpRate(maxHpRate);

        monster.transform.SetParent(_monsterParent);

        return monster;
    }


実行ログ

// Awakeが先に実行されてしまい、下2行のMaxHpRateが1のままになっている

Monster Awake() _maxHp: 200, _maxHpRate: 2
Monster Awake() _maxHp: 50, _maxHpRate: 1
Monster Awake() _maxHp: 100, _maxHpRate: 1
Monster Awake() _maxHp: 50, _maxHpRate: 1
Monster Awake() _maxHp: 100, _maxHpRate: 1

おわりに

Prefabの動的生成のみを考慮する場合は、単に初期化関数を呼べば良いのでこの方法は不要です。
しかし、Sceneに配置済みのオブジェクトと動的生成を両対応させたい場合には、この方法が役に立ちます。

ぜひ活用してみてください!

ambr Tech Blog

Discussion