Chapter 05

パターン (Patterns)

sh1ch
sh1ch
2020.09.23に更新

パターン (Patterns)

パターンとは、頻繁に発生する問題を標準的な方法で解決する方法のことです。

Bob Nystrom氏の著書「Game Programming Patterns」(オンライン上で無料で読める)は、ゲームプログラミングで発生する問題にパータンがどのように適用されるのかを知るために便利な資料です。

Unity もこれらのパターンの多くを使っています。Instantiate() は prototype パターンの一例だし、MonoBehaviour は template パターンに従っているし、UI とアニメーションは observer パターンを使っているし、新しいアニメーションのエンジンは state machine パターンを使用しています。

これら(ここで)の Tips は、特に Unity で使用するパターンを紹介します。

37. 便利に singleton を使うこと

つぎのクラスは、継承しているクラスを自動的に singleton にします。

public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
   protected static T instance;
 
   //Returns the instance of this singleton.
   public static T Instance
   {
      get
      {
         if(instance == null)
         {
            instance = (T) FindObjectOfType(typeof(T));
 
            if (instance == null)
            {
               Debug.LogError("An instance of " + typeof(T) + 
                  " is needed in the scene, but there is none.");
            }
         }
 
         return instance;
      }
   }
}

singleton は ParticleManager、AudioManager、GUIManager などのようなマネージャー(クラス)のために役にたちます。

多くのプログラマーは、クラスの名前を XManager のように曖昧なものにすることに対して警告していますが、それは名前のつけ方が悪かったか、関連性のないタスクが多すぎるクラスになっていることを示しています。一般的には、私もこれに同意します。しかし、どんなゲームにもちょっとはマネージャークラスが存在していて、どんなゲームでも同じことをしていて、これらのクラスは実際のところイディオムです。

  • マネージャーではない Prefab のユニークなインスタンスに singleton を利用することは避けてください。(Player など)
    この原則に従わないと、継承が複雑になって、変更が難しくなります。マネージャーで管理したいなら、GameManager のようなもの(適切なマネージャークラス)にこれらの参照を追加してください。
  • クラスの外から頻繁に使用されるように(しやすいように)、static プロパティ、メソッドを定義します。こうすることで、GameMAnager.Instance.Player とする代わりに GameManager.Player と書くことができる。

他の Tips で説明したように、singleton は、オブジェクトのデフォルト生成位置を作成したり、シーンやロードの間を超えて永続しているオブジェクトを持つのにも便利です。

旧 Tips 29 と同じ内容だと思います。

38. state machine(パターン)を使用して、複数の振る舞い (state) をつくり、状態遷移をしてコードを実行すること

簡易な state machine はいくつかの state を持ち、それぞれの state に対して、その state に入る・存在しているときに実行・更新するアクションが決まります。

これによって、コードを綺麗な状態にして、エラーを発生しづらくすることができます。state machine の恩恵を受けることができる兆候は、Update() メソッドのコードに if 文や switch 文があってそれらがなにかを変更する場合や、hasShownGameOverMessage のような変数がある場合です。

public void Update()
{
   if(health <= 0)
   {
      if(!hasShownGameOverMessage) 
      {
         ShowGameOverMessage();
         hasShownGameOverMessage = true; //Respawning resets this to false
      }
   }
   else
   {
      HandleInput();
   }
}

state の数が増えてくると、この問題は非常にやっかいになります。state machine パターンを適用することで、よりすっきりしたコードにすることができます。

39. UnityEvent 型のフィールドを使用して、インスペクターに observer パターンを使えるようにしておくこと

UnityEvent クラスを使用すると、インスペクターで、ボタンのイベントと同じ UI インターフェースを使用して、(最大4つの引数を使える)メソッドをリンクすることができます。この機能は、特に入力を扱う場合に便利です。

40. observer パターンを使って、フィールドの値が変化したタイミングを検出する

変数の値が変化したときだけ、コードを実行したいというケースは、ゲームの中で頻繁に発生します。この問題の一般的な解決策として、値が変わるたびにイベント登録できる汎用クラスを準備します。

ここでは health を例に示します。以下のようになります:

/*ObservedValue*/ health = new ObservedValue(100);
health.OnValueChanged += () => { if(health.Value <= 0) Die(); };

これで、例えばこんな感じで、チェックする場所ごとにやらなくても、どこでもチェックできるように(通知してくれるように)なりました。

if(hit) health.Value -= 10;

health の値が0以下になれば Die() メソッドが呼び出されます。これについて、さらなる議論・実装等については、こちらの「記事」を参照してください。

これも、個人的に気になったので訳をした「記事」をあげました。

41. Prefab に actor パターンを使用する

このパターンは標準的なパターンではありません。基本的な考え方は、Kieran Load の「プレゼンテーション」からのものになります。

actor とは、Prefab のメインコンポーネントであり、通常は Prefab の「アイデンティティ」を決めるコンポーネントであり、上位レベルのコードが最も頻繁に相互作用するものになります。actor は、同じオブジェクト(子オブジェクト上)から他のコンポーネントに「ヘルパー」を使って作用します。

Unity でメニューからボタンオブジェクトを作成すると、Sprite と Button コンポーネントを持つ Game Object が生成されます。(Text コンポーネントを持つ子要素も生成されます)この場合、ボタンは actor コンポーネントです。

同様に、メインカメラも通常は Camera コンポーネントが付属しているだけではなくて、いくつかのコンポーネント(GUI and Flare layer、Audio Listener)が付属しています。Camera も actor です。

actor が正しく動作するためには、他のコンポーネントが必要になる場合があります。actor コンポーネントに以下の属性を与えることで、Prefab をより堅牢で有用なものにすることができます。

  • RequiredComponent を使用して、actor が同じゲームオブジェクト上で必要とするすべてのコンポーネントを指定します。(これによって、actor は常に安全に GetComponent を呼び出すことができ、返された値が null かどうかをチェックしなくてもよくなります)
  • 複数の同じコンポーネントがアタッチされるのを防ぐためには、DisallowMultipleComponent を使用します。これによって、同じコンポーネントが複数存在しないことになるので、actor は常に GetComponent を呼び出すことができます。
  • actor オブジェクトに子要素がある場合は、SelectionBase を使用します。これによって、シーンビューでの選択がやりやすくなります。
[RequiredComponent(typeof(HelperComponent))]
[DisallowMultipleComponent]
[SelectionBase]
public class Actor : MonoBehaviour
{
   ...//
}

42. ランダムなものとパターンのあるデータストリームには generator を使用すること

これは標準的なパターンではないけれど、非常に有用であることがわかりました。

generator は random generator に似ています。これは、Next() メソッドを持つオブジェクトで、特定の方の新しいアイテムを取得するために呼び出すことができます。

generator はそのアイテムを構築する間に操作して、多様なパターン、異なるタイプのアイテムとして生成することができます。

generator は、新しいアイテムを生成するロジックをアイテムが必要な場所と別のところに(generator 自身のロジックとして)配置することができるので、コードをよりすっきりさせることができるので、便利です。

いくつかの例を挙げます:

var generator = Generator
   .RamdomUniformInt(500)
   .Select(x => 2*x); //Generates random even numbers between 0 and 998
 
var generator = Generator
   .RandomUniformInt(1000)
   .Where(n => n % 2 == 0); //Same as above
 
var generator = Generator
    .Iterate(0, 0, (m, n) => m + n); //Fibonacci numbers
 
var generator = Generator
   .RandomUniformInt(2)
   .Select(n => 2*n - 1)
   .Aggregate((m, n) => m + n); //Random walk using steps of 1 or -1 one randomly
 
var generator = Generator
   .Iterate(0, Generator.RandomUniformInt(4), (m, n) => m + n - 1)
   .Where(n >= 0); //A random sequence that increases on average

私たちは、障害物の生成、背景色の変更、手続き的な音楽、単語ゲームの単語の文字列生成などに generator を使用してきました。また、generator の機能は、一定ではない間隔で繰り返すコルーチンを制御することもうまく機能します。

while (true)
{
   //Do stuff
   
   yield return new WaitForSeconds(timeIntervalGenerator.Next());
}

generator について詳しく知りたいなら、これちらの「記事」を確認ください。