Unityイベントシステムのソースを読む #1 EventSystem.cs編

投稿者: | 2016-12-02

前回から引き続いてUnityのイベントシステムの記事です。InputModule等を駆使してカスタムイベントを作ることができるのはわかりましたが、そもそもEventSystemオブジェクトって何してるの?というのがわからなかったので、もう少し調べてみました。

イベントシステムとは(公式マニュアルから引用)

ButtonやImageなどのUIオブジェクトをシーンに追加すると、「EventSystem」というオブジェクトも同時に追加されます。

image20161202-01

インスペクタを見てみると、「EventSystem」や「StandaloneInputModule」といったスクリプトがアタッチされていることがわかります。StandaloneInputModuleはその名前から入力に関するスクリプトだと想像できましたが、EventSystemとはいったい何なのでしょうか。

Unity公式マニュアルを参照すると、以下のようなことが書かれています。

Unityにおけるイベントシステムは、ユーザからの入力(キーボード入力やタッチ操作)に基づいて、アプリケーション内のオブジェクトにイベントを送信します。

イベントシステムの役割は以下のように書かれています。
・どのゲームオブジェクトを選択されているのかを管理する
・どのInputModuleを使うのかを管理する
・必要に応じてRaycastを管理する
・必要に応じてすべてのInputModuleの状態を更新する

公式マニュアルより

…わかるような、わからないような…
こんな時はソースを読むに限ります。

Unity UIのソース

EventSystemが持つフィールド

EventSystemがその役割を果たすにあたって、重要だと思われるフィールドを以下に示します。
(内部状態を表すフラグ等や、現在は使用が推奨されていないフィールドは省略します)

EventSystemの役割はInputModuleの管理とあることから、InputModuleの基底クラスであるBaseInputModule型のリストとBaseInputModule型の変数を持っています。後者は現在使用中扱いのInputModuleを表しています。

private List<BaseInputModule> m_SystemInputModules = new List<BaseInputModule>();
private BaseInputModule m_CurrentInputModule;

また、どのゲームオブジェクトを選択中扱いとするかを管理するのもEventSystemの役割とあるので、それ用のGameObject型の変数も用意されています。「選択中扱い」というのが少し曖昧な表現なのですが、例えばRPGの戦闘での攻撃対象だとか、メニュー画面でカーソルをあてている項目、といったものをイメージすればいいと思います。

private GameObject m_FirstSelected;   // シーン開始時に選択中扱いとするオブジェクト
private GameObject m_CurrentSelected  // 現フレームで選択中扱いとするオブジェクト

EventSystemはMonoBehaviourのサブクラスである

EventSystemはUIBehaviourを継承しています。また、UIBehaviourはMonoBehaviourを継承しています。したがって、ゲームオブジェクトにEventSystemがアタッチされていれば、フレーム毎にUpdateメソッドが呼び出されますし、ゲームオブジェクトが有効/無効になった場合はOnEnableメソッド、OnDisableメソッドが呼び出されます。

public class EventSystem : UIBehaviour
{
  protected override void OnEnable() { /* 省略 */ }
  protected override void OnDisable() { /* 省略 */ } 
  protected virtual void Update() { /* 省略 */ }
}

EventSystemはシーン内で1つしか存在しないよう設計されているため、OnEnableメソッドではシングルトンパターンを適用するための処理が実行されます。

public static EventSystem current { get; set; }

protected EventSystem() {}

protected override void OnEnable()
{
  base.OnEnable();
  if (EventSystem.current == null)
    EventSystem.current = this;
}

OnDisableメソッドはゲームオブジェクト無効化時の後始末として、各フィールドをnullに設定します。

protected override void OnDisable()
{
  if (m_CurrentInputModule != null)
  {
    m_CurrentInputModule.DeactivateModule();
    m_CurrentInputModule = null;
  }

  if (EventSystem.current == this)
    EventSystem.current = null;

    base.OnDisable();
}

InputModuleの管理

EventSystemの役割にInputModuleの内部状態の更新と、使用するInputModuleの選定がありますが、これはUpdateメソッドにて実行されます。内部状態の更新については、フレーム毎にInputModuleのUpdateModuleメソッドとProcessメソッドが呼び出されて実施されます。前の記事に書いた通り、InputModuleのProcessメソッドにはイベント発行のロジックが定義されるので、EventSystemはInputModuleを操作してイベントの発行を管理していることがわかります。

protected virtual void Update()
{
  if (current != this)
    return;

  TickModules();    // InputModuleの内部状態を更新
  
  // 中略

  m_CurrentInputModule.Process();  // InputModuleにてイベントデータの整形、イベントの発行を行う
}

private void TickModules()
{
  for (var i = 0; i < m_SystemInputModules.Count; i++)
  {
    if (m_SystemInputModules[i] != null)
      m_SystemInputModules[i].UpdateModule();
  }
}

公式リファレンスの文書からはUpdateModuleメソッドとProcessメソッドの役割の違いはよく分かりませんが、ソースを見るとUpdateModuleメソッドは管理しているInputModuleすべてにおいて実行されているのに対し、Processメソッドは現在使用しているInputModuleだけを対象に実行しているという明確な違いがあることがわかります。

UpdateModuleメソッドではInputModuleの内部状態を表すフィールドの更新だけを行い、Processメソッドは使用中扱いのInputModuleのものだけが実行されるので、クラス外へのアクション(=イベント発行)を行うという使い分けになるのでしょうか。実際にStandaloneInputModuleのUpdateModuleメソッドでは、マウスやタッチなどのポインタ座標の取得のみで、各種イベント発行などの処理はProcessメソッドで実行されています。

また、インスペクタ上でEventSystemオブジェクトに複数のInputModuleをアタッチすると、EventSystemのフィールドの項で説明したリストm_SystemInputModulesにInputModuleのインスタンスが格納されます。Updateメソッドでは使用するInputModuleのリストからの選定も行っています。

protected virtual void Update()
{
  if (current != this)
    return;

  TickModules();

  bool changedModule = false;
  for (var i = 0; i < m_SystemInputModules.Count; i++)
  {
    var module = m_SystemInputModules[i];
   
    // 現在使用中扱いとなっているInputModule以外から
    // 何らかの入力が検出された場合は、InputModuleを切り替える
    if (module.IsModuleSupported() && module.ShouldActivateModule())
    {
      if (m_CurrentInputModule != module)
      {
        ChangeEventModule(module);
        changedModule = true;
      }
      break;
    }
  }
  
  // シーン開始時等、使用中扱いとなっているInputModuleが存在しない場合は、
  // リストの先頭に入っているInputModuleを使用する
  if (m_CurrentInputModule == null)
  {
    for (var i = 0; i < m_SystemInputModules.Count; i++)
    {
      var module = m_SystemInputModules[i];
      if (module.IsModuleSupported())
      {
        ChangeEventModule(module);
        changedModule = true;
        break;
      }
    }
  }

  // 使用中扱いとなっているInputModuleのProcess()を実行し、
  // 状態に応じてイベントを発行させる
  if (!changedModule && m_CurrentInputModule != null)
    m_CurrentInputModule.Process();
}

// InputModuleの切り替え
private void ChangeEventModule(BaseInputModule module)
{
  if (m_CurrentInputModule == module)
    return;

  if (m_CurrentInputModule != null)
    m_CurrentInputModule.DeactivateModule();

  if (module != null)
    module.ActivateModule();
  
  m_CurrentInputModule = module;
}

当然のようにInputModuleのリストを操作していますが、そもそもこのリストはいつ初期化や更新がされるのでしょうか。それはEventSystemのUpdateModulesメソッドで行われます。(BaseInputModule.UpdateModuleメソッドと名前が似ているので紛らわしい)

public void UpdateModules()
{
  // EventSystemにアタッチされているBaseInputModule型のオブジェクトを検索し、リストに格納する
  GetComponents(m_SystemInputModules);
  for (int i = m_SystemInputModules.Count - 1; i >= 0; i--)
  {
    if (m_SystemInputModules[i] && m_SystemInputModules[i].IsActive())
      continue;

    m_SystemInputModules.RemoveAt(i);
}

UpdateModulesメソッドはInputModuleのOnEnableメソッドが呼び出されたとき、すなわち有効化されたときに呼び出されます。

選択中扱いとするゲームオブジェクトの管理

UnityではSelectイベントやDeSelectイベント等、マウスやタッチ操作で何かしらのゲームオブジェクトが選択されたときに発生するイベントが用意されています。これらのイベントの送信先、すなわち選択中扱いとするゲームオブジェクトはEventSystemのm_CurrentSelectedObjectフィールドにて管理されます。

例えば、Buttonオブジェクトなど、Selectableを継承したクラスや、Selectableコンポーネントをゲームオブジェクトにアタッチすると、カーソル操作等でUIオブジェクトを選択することができるようになります(Webサイトのボタンやチェックボックスをイメージしてください)が、このときの選択中扱いのゲームオブジェクトがEventSystemのm_CurrentSelectedObjectフィールドに保持され、イベントの送信先等に使われるようになります。ですので、EventSystemは各種イベント発行のために、ユーザが選択したオブジェクトを管理していることがわかります。

なお、EventSystemでは選択中扱いゲームオブジェクトを指定するためのメソッドが用意されています。

// 引数で指定したゲームオブジェクトがEventSystemにて管理される
public void SetSelectedGameObject(GameObject selected, BaseEventData pointer)
{
  if (m_SelectionGuard)
  {
    Debug.LogError("Attempting to select " + selected +  "while already selecting an object.");
    return;
  }

  m_SelectionGuard = true;
  if (selected == m_CurrentSelected)
  {
    m_SelectionGuard = false;
    return;
  }
  // 選択中扱いオブジェクトが変更された場合は各種イベントを発行する
  ExecuteEvents.Execute(m_CurrentSelected, pointer, ExecuteEvents.deselectHandler);
  m_CurrentSelected = selected;
  ExecuteEvents.Execute(m_CurrentSelected, pointer, ExecuteEvents.selectHandler);
  m_SelectionGuard = false;
}

このメソッドはUpdate等、EventSystemクラスの内部から呼び出されることはありません。Selectableコンポーネント等から呼び出されます。(Selectableコンポーネントについては公式リファレンス等を確認してください)

Raycastの管理

EventSystemの役割としてRaycastの管理があげられています。
(そもそもRayやRaycastってなに?という方は以下の記事を参照してください)

EventSystemではマウスクリックやタッチイベントの送信先を決定するためにRaycastが使われます。マウスカーソルやタッチ場所のスクリーン座標からカメラを通してRayを送信し、ヒットしたシーン内のオブジェクトをイベント送信先の候補として取り扱います。

このときシーン内にアタッチされたRaycasterをもとにRayの対象(3DオブジェクトかUIオブジェクトか等)をフィルタリングすることも可能です。(Raycasterについては別途まとめます)

EventSystemではRaycastに関連するメソッドとしてRaycastAllが用意されています。

public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults)
{
  raycastResults.Clear();
  var modules = RaycasterManager.GetRaycasters();
  for (int i = 0; i < modules.Count; ++i)
  {
    var module = modules[i];
    if (module == null || !module.IsActive())
      continue;
    // eventDataはマウス座標とVector2データが含まれているので、その座標からRayを送信する
    module.Raycast(eventData, raycastResults);
  }
  // Raycastでヒットしたオブジェクトがすべて格納されるので、手前にある順に並べ替える   
  raycastResults.Sort(s_RaycastComparer);
}

こちらのメソッドはInputModuleにて、タッチやマウスクリック等のイベント送信先を決定するために使用されます。

まとめと今後

イベントシステムの仕組みを理解するため、EventSystemのソースを読みました。EventSystemはイベント駆動によるユーザインタフェースを実現するため、Raycastやゲームオブジェクトの選択の管理を行うことによりイベント送信先を決定し、InputModuleを通してイベントを発行していることがわかりました。

次はイベント発行の実務を担っているInputModuleあたりやイベントデータ関連のソースをまとめる、かもしれません。