【Unity】StartメソッドやUpdateメソッドはなぜ実行されるのか

2024年4月10日

実は、これらのメソッドは継承されているものではないようです
Unityエンジンの特殊な挙動によって実現されています

そもそも、アクセス修飾子はプライベートですよね?
他のクラスからは呼び出せないのでは?

Unity公式の情報

MonoBehaviourを継承したクラス(スクリプト)が実行されると、Unityエンジンがそのスクリプト内にStartやUpdateなどの特別なメソッドがあるかどうかを調査し、その情報をリストで保存します
例えばスクリプトがUpdateメソッドを持っていたら「毎フレームUpdateを呼ぶべきスクリプトのリスト」に追加されるわけです。

ゲームが実行される際、Unityは単純にこのリストを順番に処理してメソッドを呼び出します。この仕組みのおかげで、メソッドのアクセス権がpublicであろうとprivateであろうと、メソッドが呼び出されることには関係ありません。

C#の継承やオーバーライドなどの仕組み文法)を使っているわけではありません

公式からの抜粋原文

任意の型のMonoBehaviourが初めてその基底のスクリプトにアクセスしたときに、スクリプティングランタイム(MonoもしくはIL2CPP)によって何かのマジックメソッドが定義されているかを調査され、この情報がキャッシュされます。もしMonoBehaviourが特定のメソッドを持っていたら所定のリストに追加されます。例えばスクリプトがUpdateメソッドを持っていたら「毎フレームUpdateを呼ぶべきスクリプトのリスト」に追加されるわけです。

ゲーム中は、Unityは単にこのリストをイテレーションしてメソッドを呼んでいきます – シンプルです。また、これがUpdateメソッドのアクセス権がpublicであろうとprivateであろうと関係ない理由でもあります。

この文章は、Unityの「マジックメソッド」の検出と実行のプロセスについて説明しています。Unityにおいて、MonoBehaviourから派生した任意のクラスが「マジックメソッド」(Unityのライフサイクルに関連した特別なメソッド、例えばUpdateStartAwakeなど)を持っているかどうかは、そのクラスのインスタンスが最初に基底スクリプト(MonoBehaviour)にアクセスした時にスクリプティングランタイム(MonoまたはIL2CPP)によって一度だけ検査されます。検査結果はキャッシュされ、特定のマジックメソッドを持っていることが判明したMonoBehaviourは、対応するメソッドを実行するべきオブジェクトのリストに追加されます。

例えば、Updateメソッドを持つスクリプトがあれば、そのスクリプトは「毎フレームUpdateを呼ぶべきスクリプトのリスト」に追加されます。そしてゲームの実行中、Unityは単にこのリストをイテレーションし、リスト内の各スクリプトに対してUpdateメソッドを呼び出します。このプロセスは非常に効率的であり、Updateメソッドがpublicであるかprivateであるかは関係ないため、開発者はアクセス修飾子を自由に選択できます。Unityが内部的にメソッドの存在を検出し、適切なタイミングで呼び出すため、メソッドの可視性は実行に影響しません。

このシステムの利点は、Unityが必要なメソッドの呼び出しを最適化し、開発者がスクリプトの可読性や整理を維持しながらも、ゲームのパフォーマンスを最大化できるようにする点にあります。また、このプロセスにより、不要なメソッドの呼び出しを避けることができ、実行効率が向上します。

用語

スクリプティングランタイム

ゲームロジックやイベントの処理をカスタマイズおよび制御するプログラム。UnityではC#スクリプティングランタイム(MonoやIL2CPP)がそれに当たります

マジックメソッド

StartメソッドやUpdateメソッドを指します
特定の名前とシグネチャ(引数の型や戻り値の型)を持つ特別なメソッドのことを指します。これらのメソッドは、Unityのスクリプティングランタイムによって自動的に呼び出されるため、「魔法のように」振る舞うことからこの名前がつけられました。これらのマジックメソッドを適切に実装することで、ゲームオブジェクトやコンポーネントに対する特定のイベントや振る舞いをカスタマイズできます。

イテレーション

リスト内の各要素を順番に処理することを指します

実際には使われていませんが、リクレクションでも実現は可能

UnityでのStartAwakeなどのライフサイクルメソッドの自動実行は、リフレクションは使われていないようですが、一般にゲームエンジンやフレームワークでは、リフレクションやその他のメタプログラミング技術を使用して、スクリプト内の特定のメソッドを自動的に検出し実行する機能を実現していることがあります。

リフレクションとは

リフレクションは、プログラム実行時にそのプログラム自身の構造(クラス、メソッド、変数など)を調べたり操作したりするプログラミング技術です。C# においては、System.Reflection 名前空間下のクラスを使用してリフレクションを実行できます。これにより、実行時にクラスのインスタンスを作成したり、メソッドを呼び出したり、属性を読み取ったりすることが可能になります。

Unityにおけるリフレクションの使用

Unityでは、エディタやシリアライズシステムでリフレクションを使用しています。たとえば、インスペクターで公開されているフィールドやSerializeField属性が付与されたプライベートフィールドは、リフレクションを使用して自動的に検出され、エディタに表示されます。

StartAwakeのようなライフサイクルメソッドがどのようにして自動的に実行されるかについては、Unityが内部でどのようにこれらのメソッドを検出し、適切なタイミングで呼び出しているかは公開されていませんが、リフレクションを利用することでこのような動作を実装することは技術的に可能です。具体的には、Unityがコンポーネントをロードする際に、リフレクションを使用してMonoBehaviourを継承するすべてのクラスのメソッドを調べ、StartAwakeなどの特定のメソッドが定義されているかをチェックし、存在する場合は適切なタイミングでそれらのメソッドを呼び出すことが考えられます。

しかし、この説明は一般的なリフレクションの使用例に基づいた推測であり、Unityの具体的な実装についてはUnity Technologiesの公式ドキュメントやソースコードを参照する必要があります。

擬似的に再現してみると・・・

この例では、MyClassStartメソッドが定義されており、プログラムの実行時にリフレクションを使ってこのStartメソッドを呼び出します。

using System;
// MyClassにはStartメソッドが定義されています。
public class MyClass
{
    // このメソッドをリフレクションを使って呼び出します
    private void Start()
    {
        Console.WriteLine("Start method has been called.");
    }
}
using System;
using System.Reflection;

public class ReflectionExample
{
    // 指定されたインスタンスのStartメソッドをリフレクションを使って呼び出すメソッド
    public static void InvokeStartMethod(object instance)
    {
        // インスタンスの型情報を取得
        Type type = instance.GetType();

        // "Start"という名前のメソッドを取得しようと試みる
        // BindingFlagsを指定して、privateメソッドも検索対象に含める
        MethodInfo startMethod = type.GetMethod("Start", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

        // Startメソッドが存在すれば、そのメソッドをインスタンスに対して呼び出す
        if (startMethod != null)
        {
            startMethod.Invoke(instance, null);
        }
        else
        {
            // Startメソッドが見つからなかった場合の処理
            Console.WriteLine("Startメソッドが見つからなかった");
        }
    }

    static void Main(string[] args)
    {
        // MyClassのインスタンスを作成
        MyClass myClassInstance = new MyClass();
        // リフレクションを使ってStartメソッドを呼び出す
        InvokeStartMethod(myClassInstance);
    }
}

このコードは、コンソールアプリケーションとして実行可能です。MyClassクラスにStartメソッドが定義されていると仮定しています。Startメソッドはprivateアクセス修飾子を持っているため、通常の方法では外部から直接呼び出すことはできません。しかし、ReflectionExampleクラスのInvokeStartMethodメソッドを使用することで、リフレクションを通じてStartメソッドを動的に見つけ出し、実行することができます。

このアプローチは、テストや動的なメソッド呼び出しなど、特定のシナリオで非常に便利です。ただし、リフレクションは実行時のオーバーヘッドが大きいため、パフォーマンスが重要なアプリケーションでは慎重に使用する必要があります。

Updateもシミュレートしてみると

UnityのUpdateメソッドをシミュレートするためのピュアなC#コンソールアプリケーションのサンプルコードに日本語でコメントを追加します。この例では、リフレクションを使ってUpdateメソッドを定期的に呼び出すシンプルなシミュレーションを行います。実際のUnity環境ではUpdateメソッドは毎フレーム呼び出されますが、ここではタイマーを使用して定期的な呼び出しを模倣します。

using System;
using System.Reflection;
using System.Threading;

public class MyClass
{
    // このUpdateメソッドはリフレクションを通じて定期的に呼び出されます
    private void Update()
    {
        Console.WriteLine("Updateメソッドが呼び出された");
    }
}

public class ReflectionExample
{
    public static void InvokeUpdateMethod(object instance)
    {
        Type type = instance.GetType();
        MethodInfo updateMethod = type.GetMethod("Update", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

        // タイマーを使用して、一定間隔でUpdateメソッドを呼び出す
        Timer timer = new Timer((e) =>
        {
            // Updateメソッドが存在する場合、そのメソッドを呼び出す
            updateMethod?.Invoke(instance, null);
        }, null, 0, 1000); // 1000msごとにUpdateメソッドを呼び出す

        // シミュレーションを少し長く実行するために、10秒間待機する
        Thread.Sleep(10000);
    }

    static void Main(string[] args)
    {
        MyClass myClassInstance = new MyClass();
        InvokeUpdateMethod(myClassInstance);
    }
}

このコードでは、MyClassクラス内に定義されたUpdateメソッドを、ReflectionExampleクラスが定期的に呼び出します。InvokeUpdateMethodメソッドは、リフレクションを使用してMyClassUpdateメソッドを検出し、System.Threading.Timerクラスを使って1秒ごと(1000ミリ秒ごと)にそのUpdateメソッドを呼び出します。Thread.Sleep(10000)は、デモンストレーションのためにプログラムがすぐに終了しないようにするためのもので、10秒間の間隔でプログラムの実行を続けます。

このシンプルなシミュレーションは、Unityのゲームループ内でUpdateメソッドがどのように機能するかを模倣するもので、リフレクションとタイマーを使用しています。ただし、実際のゲーム開発ではこのような方法でUpdateメソッドを扱うことはありません。このコードはあくまで教育的な目的でリフレクションの使い方を示すためのものです。

複数のアタッチされたスクリプトを順に実行する様子もシミュレート

Unityのような環境で複数のスクリプトがアタッチされているオブジェクトに対してUpdateメソッドを順番に実行するシナリオをピュアなC#でシミュレートする場合、以下のようなコードを考えることができます。このコードは、複数のスクリプト(ここではMyClass1MyClass2とします)がある場合に、それぞれのUpdateメソッドを定期的に順に呼び出す構造を示しています。

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;

public interface IMyClass
{
    void Update();
}

public class MyClass1 : IMyClass
{
    public void Update()
    {
        Console.WriteLine("MyClass1のUpdateメソッドが呼び出された");
    }
}

public class MyClass2 : IMyClass
{
    public void Update()
    {
        Console.WriteLine("MyClass2のUpdateメソッドが呼び出された");
    }
}

public class ReflectionExample
{
    public static void InvokeUpdateMethods(List<IMyClass> instances)
    {
        // タイマーを使用して、一定間隔で各インスタンスのUpdateメソッドを呼び出す
        Timer timer = new Timer((e) =>
        {
            foreach (var instance in instances)
            {
                // IMyClassインターフェースを実装しているため、直接Updateメソッドを呼び出せる
                instance.Update();
            }
        }, null, 0, 1000); // 1000msごとに各Updateメソッドを呼び出す

        // シミュレーションを少し長く実行するために、10秒間待機する
        Thread.Sleep(10000);
    }

    static void Main(string[] args)
    {
        // 複数のスクリプトインスタンスを作成
        List<IMyClass> myClassInstances = new List<IMyClass>()
        {
            new MyClass1(),
            new MyClass2()
        };

        InvokeUpdateMethods(myClassInstances);
    }
}

この例では、IMyClassインターフェイスを定義しています。このインターフェイスはUpdateメソッドを持ち、MyClass1MyClass2はこのインターフェイスを実装します。これにより、異なる型のインスタンスを一つのリストにまとめて管理し、同じメソッド(この場合はUpdate)を順番に呼び出すことができます。

Mainメソッドでは、MyClass1MyClass2のインスタンスを作成し、それらをリストに追加しています。その後、InvokeUpdateMethodsメソッドを呼び出して、リストに含まれる各インスタンスのUpdateメソッドを1秒ごとに順に実行します。

このコードは、UnityのゲームループにおけるUpdateメソッドの呼び出し方をシンプルな形で模倣しており、リフレクションを使わずに直接インターフェイスを通じてメソッドを呼び出しています。これにより、実行時のオーバーヘッドを減らし、シンプルかつ効率的な実装を実現しています。

参考リンク

UnityでのStartAwakeなどのライフサイクルメソッドがどのように実行されるかについて、UnityがSystem.Reflectionを使っているかどうかは、一般的に公開されていない詳細です。しかし、Stack Overflowの回答によると、Unityはリフレクションを使用して毎回「マジックメソッド」を見つけるわけではありません。代わりに、あるMonoBehaviour型が初めてアクセスされる時に、そのスクリプトがどの「マジックメソッド」を定義しているかがスクリプティングランタイム(MonoやIL2CPP)を通じて検査され、この情報はキャッシュされます​ (Stack Overflow)​。

この情報により、Unityが特定のライフサイクルメソッドを自動的に検出し実行するプロセスは、最初のアクセス時にのみリフレクションや類似のメカニズムを使用して検査を行い、その後はキャッシュされた情報を基に動作していることが示唆されます。これにより、実行時のパフォーマンスを維持しながら、開発者が特定のライフサイクルメソッドを利用できるようにしています。

Unity

Posted by hidepon