Erlang
Elixir

最高のゲームエンジンに足りなかったもの

はじめに

現在、Citadel Xというものを開発中です。Erlang VMを効果的に使うためのElixirで書かれたフレームワーク/ミドルウェアです。この記事ではCitadel Xの作成に至った動機などを説明します。

背景

以前、Elixirで作る最高のゲームエンジンという記事(以下「当該記事」)を書きました。当該記事を先に読んでおく必要をなくすため、内容を要約すると、

  • Elixirがもつ、高い並列性・パターンマッチング・マクロなどの特色がゲーム制作に向いている。
  • メッセージングではなくeventで動作させることにより、プロセス間の結びつきを弱くして拡張性を高める。
  • 現状、プロトタイピングしただけで、テストも書いていない。

というものです。
これを書いてから8ヶ月近く過ぎましたが、あれから何も進んでいません。
その理由は、時間がなかったとか、やる気がなかったとかではありません。当時の私が気づいていなかったことがいくつかあり、それが足りなかったために開発が進まなかったのです。
そこで、足りないものをすべて含み、さらに新しい要素を取り入れた新しいプロジェクトを始めることにしました。また、その機能や特色が、私が現在たずさわっているまたはやろうとしているあらゆるプロジェクト(ゲームエンジン・オンライン経済実験・Amorphosミドルウェア・Yayakaミドルウェア・分散ミニブログ・分散グループチャット・自動デプロイツール)にも必要だということに気づいたので、プロジェクトのスコープを”ゲームエンジン”から広げて”汎用ソフトウェアフレームワーク/ミドルウェア"にしました。

足りなかったもの

それでは、さっそく足りなかったものについて説明します。おおきく分けて以下の3つが足りませんでした。

  1. 強力なイベント基盤
  2. 強力なプロセス管理
  3. 強力な開発プロセス

以下ではこれらについてそれぞれ説明します。

1. 強力なイベント基盤

強力なイベント基盤の必要性

Citadel XではCitadel X側もそれを利用したアプリケーション側も、とにかくイベントで全てが動作します。イベント基盤が貧弱ということはCitadel Xが貧弱ということに等しいです。強力なイベント基盤はCitadel Xの要と言えます。

Dispatcher Pipeline

以前のプロジェクトでは、当該記事に

イベントのやりとりを司るのがEventDispatcherです。
ElixirのRegistryを使っただけなのでZpidsの根幹にしてはかなり短いコードになっています。

と書いたように、単一の機能を持った単一のdispatcherしか利用できませんでした。それに対して、Citadel Xでは複数のdispatcherを好きな順番で並べて利用することができます。これによって柔軟な方法でイベントを購読できるようになります。例えば「全てのイベントを購読」「あるプロセスがpushしたイベントのみを購読」「イベントを関数でfilterして購読」のような機能をもったdispatcherを作ることができるようになったり、それらのdispatcherを順に並べて「あるプロセスがpushしたイベントのうち、関数でfilterされたものを購読」ということが可能になったりします。
つぎに、作成時の利点として、でかい高級なdispatcherを作るよりも、単一の機能を持ったdispatcherを組み合わせるほうが、実装やテストが簡単になるということが挙げられます。
さらに、最適化を行う際の利点もあります。Citadel Xは、何万、何十万のプロセスが、それ以上の数の大量のイベントを互いに交換して協調動作するような世界観を想定しています。そのような世界では、dispatcherがボトルネックになることは十分考えられます。その際に、それぞれのdispatcherの機能に合わせた最適化ができるようになります。また、処理の重いdispatcherをできるだけPipelineの後側におくことで、その影響を狭い範囲に留めることもできるでしょう。

動的なDispatcher Pipelineの置き換え

該当記事に

誰でも、ゲームやゲームエンジンを作る側にまわることができるゲームエンジンを目指しています。
「作る側にまわる」とは、ゲームにオリジナルのキャラクターやカード、能力、ステージなどを追加したり、ゲームをフォークして新しいゲームを作ったりすることを意味しています。
その目的のために、Zpidsでは拡張性やコードの簡単さを重視しています。

と書いていましたが(一部強調)、実際は、拡張性を高くするには高度な設計が必要となり、作る側に回るための敷居が高くなっているという状況でした。例えば、あるゲームで「ダメージを受ける」イベントが発生したときに「分裂する」イベント1を発火させるようなコードを書くことで殴るたびに増殖していくような敵キャラクターを表現できますが、そのゲームのプレイヤーが、自分で実装した自作武器で殴ったときだけは分裂しないようにしたいというときに、以前のゲームエンジンでは敵キャラクターはどうしても増殖してしまいます。なぜなら「増殖イベント」をキャンセルする方法がないからです。
そこで、Citadel XではDispatcher Pipelineを動的に置き換えられるようにしました。前述した例で、自作武器で敵の分裂を防ぐためには、「分裂する」イベントの購読に使われているDispatcher Pipelineを、「分裂する」イベントが自作武器によるものか判定して挙動を切り替える別のDispatcher Pipelineに置き換えればよいということになります。

2. 強力なプロセス管理

強力なプロセス管理の必要性

Erlang VMの通常のプロセスは使い捨てが基本です。プロセスが失敗したら、そのことは忘れて新しいプロセスを起動すればよいという精神です。それとは反対に、Citadel Xでは

Protect your city of automata from humans.

というフレーバーテキストが表しているようにプロセス2を大切なものとして扱います。例えば、プロセスの動作は完全にモニタリング可能ですし、いつErlang VMが終了してもCitadel Xは全てのプロセスを元あった状態に復元しようとします。強力なプロセス管理を持たないCitadel XはStaddle Xとでも呼ばれるべきでしょう。

プロセスの永続化

以前のプロジェクトであるゲームエンジンの問題だった点のひとつに、プロセスの永続化方法が提供されていなかったことが挙げられます。例えば、ゾンビゲームで、ゾンビプロセスを100個作ったとしても、ゲームを再起動したときにはゾンビはいなくなってしまいます。それに対して、Citadel Xでは、Erlang VMを一度落として再起動したとしても、ゾンビたちは前回の状態から何事もなかったかのように動き出します。Citadel Xはそれぞれのプロセスを初期状態と受け取ったイベントの組で永続化することによって、これを実現します。

柔軟なプロセスの関連付け

Erlang VMにはプロセスのリンクという機能がありますが、便利な反面、呼び出し元のプロセスからしかリンクを張れないなどといった制限もあります。
Citadel Xでは、どのプロセスからでも任意の複数のプロセスを任意の関係性で結びつけることができます。例えば、ゲームで、木に火をつけると一定時間で木が消えるような処理を実装したときに、木が消えるときに木に着いていた火も一緒に消えてほしいとします。その場合、「木」プロセスが消えたときに同時に「火」プロセスも一緒に消せばよいので、「木」プロセスと「火」プロセスを「木が消えたときに火も消える」という関係性でリンクするという方法で実現することができます。また、このやり方では一方向の関係性のため、もし「火」プロセスが「水」プロセスによって消されたとしても、「木」プロセスが「火」プロセスと一緒に消えるということはありません。

3. 強力な開発プロセス

以前のプロジェクトでは開発プロセスの全てが最悪でした。無計画にプロトタイプを作ったあと、そのコードをそのまま使って開発をすすめようとしました。テストも書かれていません。品質は最悪で開発効率も確実に下がっていました。
Citadel Xではその反省から、ちゃんとした開発プロセスを使って作られています。

Citadel Xのこれから

現状では要件がある程度かたまり、設計もほとんどおわっています。おそらく来週から実装に入り、8月中には基本的な機能が全て揃っているはずです。そして、Citadel Xはあらゆるものに応用されていくでしょう。

あとがき

Qiitaの記事投稿ページで勢い良く記事を書いていたら間違ってページ遷移を発生させてしまい、1時間くらいの成果が全て消えてしまったので最悪の体験でした。


  1. ここでは「分裂する」イベントを受け取って処理するのが「フィールド」プロセスなのか「エネミーファクトリー」プロセスなのか、はたまた「ゴッド」プロセスなのかについては議論しないことにしましょう。 

  2. Citadel Xでは「プロセス」ではなく「オートマトン」という単語を使いますが、ここでは混乱を避けるため、より一般的な「プロセス」という単語を使います。