概要
C#ではコレクションの列挙中にコレクションの要素を変更することは禁止されていますが、そうしたい場面は多いです。そんな時、C#なら次のような方法を使うとよいでしょう:
- ToArray()などでコレクションのコピーを作る
- コレクションの変更をイベントとしてバッファに溜めて、列挙していない時に操作する
背景
ゲームエンジンAltseedは、ゲーム内で描画したい要素を「オブジェクト(ここでは描画オブジェクトと呼びます)」として管理する機能を持っています。描画オブジェクトを管理しているのは「レイヤー」であり、レイヤーがオブジェクトのリストを持っています。
描画オブジェクトは毎フレーム、移動したり、変色したりなど、OnUpdate
メソッドをオーバーライドすればプログラマーが望む挙動をとらせることができます。これは描画オブジェクトの OnUpdate
メソッドがAltseedのシステムによって毎フレーム呼ばれているおかげです。そのために、Altseedではレイヤーが持つ描画オブジェクトを毎フレームに一回列挙しています。このようなシステムを「オブジェクト更新システム」と呼ぶことにしましょう。そのプログラムは以下のようなイメージです:
public void UpdateLayer()
{
// Objects は描画オブジェクトのリスト
foreach(var obj in Objects)
{
obj.Update(); // 中で OnUpdate を呼び出す
}
}
ところで、C#などの多くの言語では、コレクションの列挙中にコレクションに要素を追加したり、要素を削除したりするのは禁止されていると思います。しかし、オブジェクト更新システムを採用しているAltseedでは、上記のコードでいうobj.Update
関数の中でObjects
に新しいオブジェクトを追加したくなることも多いです。このような要求にはどう答えればよいでしょうか。この問題に対してAltseedで試みた方法を幾つか紹介します。
コレクションをコピーする
C#の ToArray
メソッドを使うと、コレクションをコピーした配列を作ることができます。 IEnumerable<T>
の型を持つ変数を配列型である T[]
の変数に変換することによく使われる ToArray
ですが、コレクションのコピーをするためにもよく使われるメソッドなのです。これを使うと、先ほどのUpdateLayer
は次のように書けます:
public void UpdateLayer()
{
foreach(var obj in Objects.ToArray()) // ToArray() でコピー
{
obj.Update();
}
}
列挙されるのは Objects
ではなくてコピーされた配列なので、列挙中でも Objects
に要素を追加したりすることができます。ただし、列挙中に追加された要素はその列挙には現れません。
この方法はコレクションを一回列挙してしまう方法なので、列挙する回数がそのまま二倍になります。Altseedでは毎フレーム列挙を回す必要があるため、もう一回の列挙にかかる時間を許容しませんでした。現在は次節の方法が使われています。
コレクションの変更をバッファリング
もう一つの方法は、コレクションへの値の追加・削除をそれぞれ何らかのクラスのインスタンスで表現し、バッファに保持しておくことです。
Altseedでは、AddObject
などというメソッドを使ってレイヤーに描画オブジェクトが追加・削除されると、追加を表すクラスのインスタンスがstaticな場所に用意されたバッファに追加され、まだ実際には追加・削除されません。そして追加が行われたフレームの更新が終わると、バッファに溜まった追加・削除処理が一気に処理されます。追加・削除を表すクラスの概要は、Altseedでは以下のようなコードです(あくまで概要です):
// コレクションの操作の種類:追加、削除
enum RegistrationCommand
{
Add = 0,
Remove = 1,
}
// 描画オブジェクトのコレクションに対する操作イベント
class EventToManageObject
{
public EventToManageObject(Layer layer, Object content, RegistrationCommand command)
{
ObjectManager = objectManager;
Content = content;
Command = command;
}
// 描画オブジェクトを追加または削除する対象のレイヤー
private Layer Layer { get; }
// 追加または削除する描画オブジェクト
public Object Content { get; }
// 行う処理が追加なのか削除なのか
public RegistrationCommand Command { get; }
// 実際に追加・削除をする
public void Commit()
{
// 追加操作であれば実際に追加
if(Command == RegistrationCommand.Add)
{
// ImmediatelyAddObjectはバッファリングを介さず直接追加する
Layer.ImmediatelyAddObject(Content);
}
// 削除操作であれば実際に削除
else if(Command == RegistrationCommand.Remove)
{
// ImmediatelyRemoveObjectはバッファリングを介さず直接削除する
Layer.ImmediatelyRemoveObject(Content);
}
}
}
レイヤーのAddObject
のようなメソッドが呼ばれると上記のようなクラスのインスタンスが作られ、キューとして用意されたバッファに溜められます。そのフレームの終わりに、キューの先頭から順にイベントのCommit
メソッドが呼ばれ、レイヤーのImmediatelyAddObject
メソッドなどによって実際の追加・削除が行われます(キューなので、AddObject
などを呼び出した順番も保存されます)。
終わり
ゲームエンジンAltseedではイベントをバッファリングする方法を用いています。そのため、シーン・レイヤー・描画オブジェクトの操作はフレームの終わりまで反映されません(描画オブジェクトのインスタンスからそれを保持しているレイヤーを取得することはレイヤーに追加後すぐにできます)。
もしAltseedの内部コードに興味が出てきたら、ぜひ開発陣に参加してみてください!Slackチームでは新規開発メンバーの他にも、Altseedについて質問や雑談をしたい方も受け入れています。