2018年8月13日月曜日

[Unity]GetComponentsInChildrenとinterfaceを組み合わせる

hierarchy上の全てのオブジェクトに特定の処理を適用したい!という要望に応えてくれるのがGameObject.GetCoponentsInChildrenです。
GameObjectの再生成をせずに動的にUIの内容を差し替えたいというときや子要素の全てのコライダーを無効にしたりするなどに使えます。
image.png (120.2 kB)
例として、上のような階層構造を持つuGUIコンポーネントに対して以下のようなインターフェースを持つキャラクターを適用したいときの実装を考えます。1

Character.cs
public interface ICharacter {
    string Name { get; }
    int Hp { get; }
    int Mp { get; }
    Sprite Sprite { get; }
}

実装

各TextやImageのコンポーネントが実装すべきインターフェースの定義です。

CharacterSettable.cs
public interface ICharacterSettable {
    void SetCharacter(ICharacter character);
}

各コンポーネントはICharacter型を引数にコンポーネントの値を変更します。

NameText.cs
using UnityEngine;
using UnityEngine.UI;

 // キャラクター名
[RequireComponent(typeof(Text))]
public class NameText : MonoBehaviour, ICharacterSettable {

    public void SetCharacter(ICharacter character) {
        GetComponent<Text>().text = character.Name;
    }
}
CharacterImage.cs
using UnityEngine;
using UnityEngine.UI;

 // キャラクター画像
[RequireComponent(typeof(Image))]
public class CharacterImage : MonoBehaviour, ICharacterSettable {

    public void SetCharacter(ICharacter character) {
        GetComponent<Image>().sprite = character.Sprite;
    }
}

(スクリプト略)
...
それぞれのコンポーネント別にスクリプトを作成し、オブジェクトにスクリプトをアタッチしていきます。
image.png (13.4 kB)

親側のスクリプトでは、GetComponentsInChildrenを呼び出して、子オブジェクトを操作します。

CharacterPanel.cs
using System;
using System.Collections.Generic;
using UnityEngine;

public class CharacterPanel : MonoBehaviour{

    public void Hoge(ICharacter character) {

        //  自分と自分の子要素にアタッチされている
        //  ICharacterSettableを実装する全てのコンポーネントのSetCharacterを実行する
        GetComponentsInChildren<ICharacterSettable>().ForEach(x => {
            x.SetCharacter(character);
        });
    }
}

//  IEnumerableでForEach使えるようにする拡張
public static partial class IEnumerableExtensions {

    public static void ForEach<T>(this IEnumerable<T> e, Action<T> action) {
        foreach (T item in e) {
            action(item);
        }
    }
}

よいタイミングでHogeを呼び出すと各コンポーネントにキャラクターの情報がセットされる寸法です。

何が嬉しいか

親側のクラスが各コンポーネントのクラスや、hierarchy上の構造を知る必要が無くなります。これはとても良いことです。
GetComponentsInChildrenを利用しない方法としては以下のように親側が操作対象のコンポーネントの参照を保持しておく方法があります。

CharacterPanel.cs
public class CharacterPanel : MonoBehaviour{

    public Image characterImage;
    public Text nameText;
    public Text hpText;
    public Text mpText;

    ...

image.png (9.9 kB)

この方法はGetComponentsInChildrenを使用する場合と異なり、親側がどのコンポーネントに何を設定するかを全て知っている必要があります。この方法は階層構造の規模が大きくなるにつれて徐々にスクリプトが複雑化しやすいです。
一方でGetComponentsInChildrenを使用する方法は親側のスクリプトは成長しませんし、新しいコンポーネントを増やす際も新しいスクリプトを書くことになりますので、適切なスクリプト分割が自然となされる点が健康的で魅力的です。

何が悲しいか

スクリプトが分散します。
InspectorでオブジェクトにアタッチするMonoBehaviourは1クラス1ファイルになってしまうので、やたらとスクリプトファイルが増殖します。また、それぞれのコンポーネントクラスはコード上からは参照されないクラスになるので、使用されているスクリプトを探すときに煩わしいこともあるでしょう。
私はスクリプトファイルが増えることには特に抵抗はありませんが、増殖を嫌う人も居るようです。

今回例に挙げたuGUIの操作以外にもGetComponentsInChildrenとinterfaceの組み合わせは様々に利用することが可能です。親コンポーネントが子コンポーネントのことをよく知っているようなスクリプトがある場合は、GetComponentsInChildrenを検討してみてはいかがでしょうか。


  1. characterという名前は良くないですね。文字を意味するcharacterと被るので紛らわしくて良くないです。個人的にはキャラクターを表す命名にはactorをよく使います。