4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Unityのスクリプト実行順序へのアプローチ

4
Last updated at Posted at 2025-12-18

目次

はじめに

SGと申します。現在(2025/12/18)CA Tech Lounge所属しておりアドベントカレンダー書きました。
今回は UnityにおけるScriptの実行制御について、自分なりのアプローチを交えて紹介します。開発中に複数の Awake() / Start() が絡み合い、null参照が発生する問題に遭遇した経験がある方は多いと思います。本記事では、そうした問題をどう捉え、どう回避するかをテーマにしています。
なお、筆者が考えうる別アプローチはこちらにまとめています。合わせて参考にしていただければ幸いです。

筆者は設計初心者です。
間違いや独自のとらえ方も多いためなるべく疑って読んでください。

課題

Unityにおいて初期化を行う際、Awake() / Start() を利用するケースは非常に多いです。しかし、複数が存在する場合、それらの実行順は保証されません。そのため次のような問題が発生します。

  • AクラスとBクラスの両方で Start() が呼ばれる
  • Bクラスの Start() 内でAクラスを参照している
  • しかし A クラスの Start() がまだ実行されておらず null になる

このように、「どちらが先に初期化されるか」に暗黙的にした場合、開発が進むにつれて不具合の温床になります。
実際の開発中に起こりやすい例として以下のようなものが考えられます。


音ゲーム開発

Comboクラスが初期化されていない状態でScoreクラスのStart()が実行されると、コンボ数を参照できずスコア計算が正しく行えないこと

UIとInGameの開発

UI管理クラスが、まだ生成されていないPlayerStatusを参照してしまい、初期表示が崩れ、一部のUIが更新されないこと


これらはすべて、初期化順が明示されていないことが原因です。
この問題をどう捉え、どう解決するかが本記事のテーマです。

アプローチ

結論として Start() / Update() を使用するクラスを1つに集約するという方針を取ります。

  • 実行順が明示的になる
  • どこで初期化・更新が行われているか把握しやすくなる

というメリットがあります。
ただし、このままでは「プロジェクト内のすべてを管理する巨大クラス」が生まれてしまいます。そこで役割ごとに責務を分割します。
以下の3つのクラスを用意します。
zu1.png

  • GameBuilder : GameInitializerとGameExecutorの2つを扱うためのクラス、 Start() / Update() を唯一使用します。
  • GameInitializer : プロジェクト内の初期化を担います。
  • GameExecutor : プロジェクト内の毎フレーム更新による機能を担います。

このクラス図のようにGameBuilderはGameInitializerとGameExecutorを呼び出す以下のようなコードになります。

.cs
class GameBuilder:MonoBehaviour{
    [SerializeField] private GameInitializer gameInitializer;
    [SerializeField] private GameExecutor gameExecutor;
    private void Start(){
        gameInitializer.InitializeAll();
    } 
    private void Update(){
        gameExecutor.ExecuteAll();
    }
}

またUnityでは Start() は Update() より必ず先に実行されます。
これによりGameInitializerにGameSetUperのような初期構築の役割を持たせたりした場合でも「初期化が終わってから更新が始まる」ことで保証できるようになります

さらにGameInitializer、GameExecutorについても焦点を当てます。

GameInitializerの実装

GameInitializerをどのように設計・実装するかを説明します。
またこのためにインターフェースを用意します。

.cs
interface IInitializeable{
    public void Initialize();
}

さらに以下のような依存関係を示します。

zu2.png
これによりGameInitializerはインターフェースのIInitializeableに依存して初期化を行うことができます。
そしてGameInitializerのコードを以下に示します。

.cs
class GameInitializer:MonoBehaviour{
    [SerializeField] private List<IInitializeable> initializeables;
    public void InitializeAll(){
        foreach(var init in initializeables){
            init.Initialize();
        }
    }
}

Unityではinterfaceを直接アタッチすることができないため、「Serializable Interface」や「Odin」の利用などで実装を考えます。
また筆者はList<MonoBehaviour> monoInitializeablesでアタッチしてからinitializeablesにinterfaceのみ渡す形で実装を行っていました。

この実装を行うことでUnityのInspector上で初期化処理の各Initializeの実行順序の制御が行えるようになります。
これによって以下のようなことができます。

  • 初期化処理の一元管理ができること
  • 初期化順が視覚的に分かりやすいこと
  • 後から順序変更・追加が容易なこと

同様の方針でGameExecutorを設計、実装をします。

GameExecutorの実装

同様の方針で以下のように設計します。
zu3.png

それぞれのインターフェース、クラスのコードを以下に示します。
インターフェースのIExecuteableを用意します。

.cs
interface IExecuteable{
    public void Execute();
}

さらにGameExecutorの実装をします。

.cs
class GameExecutor:MonoBehaviour{
    [SerializeField] private List<IExecuteable> executeables;
    public void ExecuteAll(){
        foreach(var exec in executeables){
            exec.Execute();
        }
    }
}

これらによって毎フレーム行う処理も実行順序の制御が行えるようになります。

こちらで課題へのアプローチは以上になります。
他のやり方では本アプローチ以外の筆者が考えうるアプローチを記述しており、またまとめでは本アプローチのまとめを行っています。

他のやり方

本アプローチ以外にも、同様の課題に対していくつかの解決方法が考えられます。
筆者が検討した代表的な方法を具体例を通して紹介します。

Script Execution Orderの使用

Script Execution OrderとはUnityエディタから直接、スクリプトの実行順序を設定する方法です。特定のスクリプトが他のスクリプトより先に、または後に実行されるよう手動で設定することができます。
ゲームスタート時のセーブデータのロードとプレイヤーのステータス取得では以下のようになります。

.cs
[DefaultExecutionOrder(-100)]
public class SaveDataLoader : MonoBehaviour
{
    public static SaveData Data;

    void Start()
    {
        Data = Load();
    }
}

[DefaultExecutionOrder(0)]
public class PlayerStatus : MonoBehaviour
{
    void Start()
    {
        // セーブデータ前提
        hp = SaveDataLoader.Data.hp;
        level = SaveDataLoader.Data.level;
    }
}

このコードではSaveDataLoaderが既にロードされている前提でPlayerStatusは Start() を呼んでいます。Script Execution Orderにより[DefaultExecutionOrder(0)]の値が小さければ小さいほど先に実行されます。そのため上の実装ではPlayerStatusは値を取得できます。

whileループによる待機

必要なデータが取得できるまで while ループで待機するというものです。
以下のコードから同様の具体例について考えます。

.cs
public class SaveDataLoader : MonoBehaviour
{
    public static SaveData Data;
    void Start()
    {
        Data = Load();
    }
}

public class PlayerStatus : MonoBehaviour
{
    private void Start()
    {
        // セーブデータの読み込み完了を待機
        while (SaveDataLoader.Data == null)
        {
            // 待機
        }

        // セーブデータ前提の初期化
        hp = SaveDataLoader.Data.hp;
        level = SaveDataLoader.Data.level;
    }
}

whlieループを使うことでセーブデータからデータを取得できます。
このようにwhileループを使えばを取得でき、進めることができます。

Awake() / Start() の使い分け

Unityにおいて Awake() / Start() では実行順序が異なります。 Awake() が先に実行し Start() が次に実行されます。そのため同様の具体例に対しても有効であり以下のようなコードです。

.cs
public class SaveDataLoader : MonoBehaviour
{
    public static SaveData Data;

    void Awake()
    {
        Data = Load();
    }
}

public class PlayerStatus : MonoBehaviour
{
    void Start()
    {
        // セーブデータ前提
        hp = SaveDataLoader.Data.hp;
        level = SaveDataLoader.Data.level;
    }
}

このコードではSaveDataLoaderが Awake() しているためPlayerStatusよりも先にデータがロードされるためセーブデータの取得ができます。

まとめ

今回はUnityのスクリプト実行順序について私自身のアプローチと他のやり方から考えました。
私自身のアプローチでは Start() / Update() を1つにすることで制御しようという考えです。そのために以下のような工夫を行いました。

  • Start() / Update() をGameBuilderに集約する
  • 初期化処理と更新処理を明確に分離する
  • interfaceを用いて疎結合な構造を保つ

このような構造にしました。
また他のやり方も挙げましたが私個人の考えでは「Whileループによる待機」だけは避けた方がいいと思っており、他は開発規模と開発チームのスタイルだと考えています。

これは実行順序の問題を構造ではなく実行時の力技で解決しており、規模が大きくなるほど破綻しやすいためです。

またこのような開発、設計においては開発規模に合わせたコーディングをすべきと考えていますのでこれが正しいという最適解はないと考えています。

さいごに

色々とここまで書きましたが正直今回提案した手法も欠点かなりあると思ってます。結局このままでは多くの参照は持つためです。そのためこれの解決のためにさらにこのアプローチを階層構造のように使います。Aの順序制御機能このAを制御するBの順序制御機能みたいな感じです。具体的に言えば神マネージャー(全てのマネージャーのためのマネージャー)の下にバトルマネージャーがいる感じです。
またマネージャーは順序制御機能を備えています。この内容の記事も書こうとは思ったのですが、多分先になります。
またUnityの開発をPureC#にすることでこの問題もより解決しやすいと思っています。そのため今はPureな世界を目指そうと考えています。
しかしながら最近見た記事で「PureC#こそ至高!」という思想は正直「Unity使ってるのに本末転倒では?」と書かれており「たしかに....」と考えている今日この頃です。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?