【Unity】StartメソッドやUpdateメソッドはなぜ実行されるのか
実は、これらのメソッドは継承されているものではないようです
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のライフサイクルに関連した特別なメソッド、例えばUpdate
、Start
、Awake
など)を持っているかどうかは、そのクラスのインスタンスが最初に基底スクリプト(MonoBehaviour
)にアクセスした時にスクリプティングランタイム(MonoまたはIL2CPP)によって一度だけ検査されます。検査結果はキャッシュされ、特定のマジックメソッドを持っていることが判明したMonoBehaviour
は、対応するメソッドを実行するべきオブジェクトのリストに追加されます。
例えば、Update
メソッドを持つスクリプトがあれば、そのスクリプトは「毎フレームUpdate
を呼ぶべきスクリプトのリスト」に追加されます。そしてゲームの実行中、Unityは単にこのリストをイテレーションし、リスト内の各スクリプトに対してUpdate
メソッドを呼び出します。このプロセスは非常に効率的であり、Update
メソッドがpublic
であるかprivate
であるかは関係ないため、開発者はアクセス修飾子を自由に選択できます。Unityが内部的にメソッドの存在を検出し、適切なタイミングで呼び出すため、メソッドの可視性は実行に影響しません。
このシステムの利点は、Unityが必要なメソッドの呼び出しを最適化し、開発者がスクリプトの可読性や整理を維持しながらも、ゲームのパフォーマンスを最大化できるようにする点にあります。また、このプロセスにより、不要なメソッドの呼び出しを避けることができ、実行効率が向上します。
用語
スクリプティングランタイム
ゲームロジックやイベントの処理をカスタマイズおよび制御するプログラム。UnityではC#スクリプティングランタイム(MonoやIL2CPP)がそれに当たります
マジックメソッド
StartメソッドやUpdateメソッドを指します
特定の名前とシグネチャ(引数の型や戻り値の型)を持つ特別なメソッドのことを指します。これらのメソッドは、Unityのスクリプティングランタイムによって自動的に呼び出されるため、「魔法のように」振る舞うことからこの名前がつけられました。これらのマジックメソッドを適切に実装することで、ゲームオブジェクトやコンポーネントに対する特定のイベントや振る舞いをカスタマイズできます。
イテレーション
リスト内の各要素を順番に処理することを指します
実際には使われていませんが、リクレクションでも実現は可能
UnityでのStart
、Awake
などのライフサイクルメソッドの自動実行は、リフレクションは使われていないようですが、一般にゲームエンジンやフレームワークでは、リフレクションやその他のメタプログラミング技術を使用して、スクリプト内の特定のメソッドを自動的に検出し実行する機能を実現していることがあります。
リフレクションとは
リフレクションは、プログラム実行時にそのプログラム自身の構造(クラス、メソッド、変数など)を調べたり操作したりするプログラミング技術です。C# においては、System.Reflection
名前空間下のクラスを使用してリフレクションを実行できます。これにより、実行時にクラスのインスタンスを作成したり、メソッドを呼び出したり、属性を読み取ったりすることが可能になります。
Unityにおけるリフレクションの使用
Unityでは、エディタやシリアライズシステムでリフレクションを使用しています。たとえば、インスペクターで公開されているフィールドやSerializeField
属性が付与されたプライベートフィールドは、リフレクションを使用して自動的に検出され、エディタに表示されます。
Start
やAwake
のようなライフサイクルメソッドがどのようにして自動的に実行されるかについては、Unityが内部でどのようにこれらのメソッドを検出し、適切なタイミングで呼び出しているかは公開されていませんが、リフレクションを利用することでこのような動作を実装することは技術的に可能です。具体的には、Unityがコンポーネントをロードする際に、リフレクションを使用してMonoBehaviour
を継承するすべてのクラスのメソッドを調べ、Start
やAwake
などの特定のメソッドが定義されているかをチェックし、存在する場合は適切なタイミングでそれらのメソッドを呼び出すことが考えられます。
しかし、この説明は一般的なリフレクションの使用例に基づいた推測であり、Unityの具体的な実装についてはUnity Technologiesの公式ドキュメントやソースコードを参照する必要があります。
擬似的に再現してみると・・・
この例では、MyClass
にStart
メソッドが定義されており、プログラムの実行時にリフレクションを使ってこの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
メソッドは、リフレクションを使用してMyClass
のUpdate
メソッドを検出し、System.Threading.Timer
クラスを使って1秒ごと(1000ミリ秒ごと)にそのUpdate
メソッドを呼び出します。Thread.Sleep(10000)
は、デモンストレーションのためにプログラムがすぐに終了しないようにするためのもので、10秒間の間隔でプログラムの実行を続けます。
このシンプルなシミュレーションは、Unityのゲームループ内でUpdate
メソッドがどのように機能するかを模倣するもので、リフレクションとタイマーを使用しています。ただし、実際のゲーム開発ではこのような方法でUpdate
メソッドを扱うことはありません。このコードはあくまで教育的な目的でリフレクションの使い方を示すためのものです。
複数のアタッチされたスクリプトを順に実行する様子もシミュレート
Unityのような環境で複数のスクリプトがアタッチされているオブジェクトに対してUpdate
メソッドを順番に実行するシナリオをピュアなC#でシミュレートする場合、以下のようなコードを考えることができます。このコードは、複数のスクリプト(ここではMyClass1
とMyClass2
とします)がある場合に、それぞれの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
メソッドを持ち、MyClass1
とMyClass2
はこのインターフェイスを実装します。これにより、異なる型のインスタンスを一つのリストにまとめて管理し、同じメソッド(この場合はUpdate
)を順番に呼び出すことができます。
Main
メソッドでは、MyClass1
とMyClass2
のインスタンスを作成し、それらをリストに追加しています。その後、InvokeUpdateMethods
メソッドを呼び出して、リストに含まれる各インスタンスのUpdate
メソッドを1秒ごとに順に実行します。
このコードは、UnityのゲームループにおけるUpdate
メソッドの呼び出し方をシンプルな形で模倣しており、リフレクションを使わずに直接インターフェイスを通じてメソッドを呼び出しています。これにより、実行時のオーバーヘッドを減らし、シンプルかつ効率的な実装を実現しています。
参考リンク
UnityでのStart
やAwake
などのライフサイクルメソッドがどのように実行されるかについて、UnityがSystem.Reflection
を使っているかどうかは、一般的に公開されていない詳細です。しかし、Stack Overflowの回答によると、Unityはリフレクションを使用して毎回「マジックメソッド」を見つけるわけではありません。代わりに、あるMonoBehaviour
型が初めてアクセスされる時に、そのスクリプトがどの「マジックメソッド」を定義しているかがスクリプティングランタイム(MonoやIL2CPP)を通じて検査され、この情報はキャッシュされます (Stack Overflow)。
この情報により、Unityが特定のライフサイクルメソッドを自動的に検出し実行するプロセスは、最初のアクセス時にのみリフレクションや類似のメカニズムを使用して検査を行い、その後はキャッシュされた情報を基に動作していることが示唆されます。これにより、実行時のパフォーマンスを維持しながら、開発者が特定のライフサイクルメソッドを利用できるようにしています。
ディスカッション
コメント一覧
まだ、コメントがありません