型を継承する以上はis-aであるべき
ではコルーチンを使おうが何だろうが、java.lang.Iterable(C#ならSystem.Collections.Generic.IEnumerator)であるからには
その型としての機能性を全うすべきだと主張しました。
では、本題のタスクシステムでコルーチンを扱うケースでどのように設計するべきかを考えてみましょう。
そもそもタスクシステムって?
ゲームにオブジェクト指向の考えを部分的に取り入れたのがタスクシステムと言えるでしょう。
タスクシステムでやっていることは、
- キャラクタを表現する構造体を作る
- その構造体には関数ポインタによって挙動の違いを表現できる機能性を持たせる
- 構造体は双方向リストなどの構造で複数のデータを管理できるようにする
といったところです。
これは、非オブジェクト指向であるC言語では「システム」と称するだけのものかもしれませんが、
JavaやC#といったオブジェクト指向を前提としたモダンな言語ではごく日常的な表現にすぎません。
キャラクタを表現するのは抽象クラスかinterfaceを用います。
関数ポインタによる挙動の違いは、抽象クラスもしくはinterfaceの実装クラスのポリモフィズムによって
行うことができますから、わざわざ関数ポインタなどを用いる必要はありません。
双方向リストにするためにクラスに前後へのポインタを持たせることもできますが、
現在のプログラミング言語であれば外部のコレクションAPIを用いる方が自然でしょう。
このように、C言語で作られるタスクシステムがオブジェクト指向言語と極めて相性が良いのは、
タスクシステム自体がオブジェクト指向の考え方を取り入れたシステムだからに他なりません。
C#で大雑把なイメージを表現すると、
// ゲーム中のキャラクタを表現するインターフェース
interface GameCharacter
{
// 1フレーム分の処理を行う
public GameCharacterStatus update();
// キャラクタの描画
public void draw();
}
class TaskSystem
{
private List<GameCharacter> list;
public void GameMainLoop()
{
while (true)
{
foreach(GameCharacter gchar in list)
{
GameCharacterStatus status = gchar.update;
// ...
// キャラクタのステータスによって削除などを行う
// 描画処理
gchar.draw();
}
}
}
}
この例では、敵が倒されて除去されるような部分はタスクシステム側の責務としています。
そのため、キャラクタが返すステータスを見て、倒されたなどの状態をみてタスクシステムのListから
除去するように実装する必要があります。
キャラクタの制御にコルーチンを用いたい場合は?
このサンプルでは、1フレームの処理を行うためにupdate()というメソッドを用いました。
この内部がどのような実装になっていようともタスクシステム側は関知しません。
ただ、1フレーム分の処理さえしてくれればいいのです。
ここがオブジェクト指向的な抽象ですね。
C#のコルーチンはIEnumeratorで表現されます。
「繰り返し値を返すもの」として扱われるわけですね。
状態を表すオブジェクト(ここではGameCharacterStatus)を返すコルーチンを用意した場合、
class HogeCharacter : GameCharacter
{
// コルーチンを保持するメンバ
private IEnumerator<GameCharacterStatus> coroutine;
public HogeCharacter()
{
// コルーチンの初期化
this.coroutine = this.getCoroutine();
}
// 1フレームを処理して状態を返す
public override GameCharacterStatus update()
{
this.coroutine.MoveNext();
return this.coroutine.Current;
}
// コルーチンの実装
private IEnumerator<GameCharacterStatus> getCoroutine()
{
GameCharacterStatus status = new GameCharacterStatus();
// ...
yield return sutatus;
// ...
yield return sutatus;
// ...
}
}
といった感じになると思います。
このように、コルーチンの実体と外界との接点をyield returnの値のみにすることで
スパゲッティコード化することを防いでいるわけです。
こういった工夫は小さい規模のプログラムではメリットを感じにくいところですが、
大規模化するほど効果を発揮します。
状態を表すオブジェクトなんて作ってられないというのであれば
もし、こうしたように状態を表すオブジェクトを返すという作りにしにくい場合はどうでしょうか?
状態を表すクラスを宣言するよりも複数の値を返したい場合など、メンバ関数を用いてやり取りする方が楽な場合もあります。
私は二つの値を返したければ、それを保持するクラスを作ることを厭わない人間ですが、
いろいろな人のソースを見ていると、クラスや構造体をわざわざ宣言するということがためらわれるという意見も多いようです。
C#ではジェネリクス型パラメータとしてVoidを設定することはできないようなので
値を返さないIEnumeratorにする場合はダミーの値を返すようにする必要がありますね。
こういったケースでは
C#のyieldに対する誤解と私の見解
で主張されているように「コルーチンとしてのyieldは無意味な値を返すべき」というのは当たっていると思います。
ただし、それはC#のジェネリクス型がVoidを表現できないことからくる実装上の工夫という泥臭い理由によることも忘れてはなりません。
ともあれ、そのような場合はメンバ変数などを用いて状態をやりとりすることになると思いますが、
そのような実装の話は、あくまでGameCharacterというインターフェースの実装の中に隠蔽される事項です。
これはオブジェクト指向で言われるカプセル化の概念ですね。
class PiyoCharacter : GameCharacter
{
// コルーチンを保持するメンバ
private IEnumerator<GameCharacterStatus> coroutine;
public PiyoCharacter()
{
// コルーチンの初期化
this.coroutine = this.getCoroutine();
}
// 1フレームを処理して状態を返す
public override GameCharacterStatus update()
{
this.coroutine.MoveNext();
// メンバ変数を通してstatusオブジェクトを構築する
GameCharacterStatus status = new GameCharacterStatus();
// ...
return staus;
}
// コルーチンの実装
private IEnumerator<int> getCoroutine()
{
// ...
yield return 0;
// ...
yield return 0;
// ...
}
}
このような場合、PiyoCharacterクラス内部とコルーチン部分とではメンバ変数によるデータのやり取りが行われます。
カプセル化という観点ではあまり望ましくない状態ですが、このPiyoCharacterクラスの内部だけの話として
隠蔽してしまうことでスパゲッティコード化することを防いでいます。
いずれにせよ、外側(ここではTaskSystemクラス)から見た場合は、
GameCharacterインターフェースに対するポリモフィズムであり、
内部の諸事情は一切考慮する必要がありません。
この考慮する必要がないという部分がオブジェクト指向で言われる隠蔽であり、
また再利用を高めるための抽象化でもあるのです。
投稿日時 : 2008年3月12日 23:31