Last update 1999/08/07

Scheme処理系の制作 第6回

(C)平山直之
無断転載は禁止、リンクはフリー
誤字脱字の指摘は歓迎


異なるプログラム方式の連携(理論編その1)

すぐにやめるつもりがいつの間にやら第6回まで来てしまったScheme製作シリーズですが、今度はVAFX(のようなフレーム/イベントドリブンのプログラム)とVismの連携をどうやって行うか、について書きたいと思います。

プログラムの基本構造

本題に入る前に、プログラムの基本構造についておさらいしておきましょう。

かつてGUIが一般的でなかったころは、プログラムはシーケンシャルに組むのが一般的でした。たとえば、次のような構造です。

int main(int,char**)
{
	function1( );

	// ループ1
	for(int i=0;i<10;i++){

	}

	function2( );

	// ループ2
	for(int j=0;j<11;j++){

	}

	function3( );
}

このプログラムはfunction1()〜ループ1〜function2( )〜ループ2〜function3()という処理がシーケンシャルに行われることが前提になっており、ユーザの選択によってその順番が変わることがありません。

なぜこのような構造であったか。それは、プログラムというものが、人間の作業負担を軽減するためにバッチ処理を担当するためのものであったからです。

UNIXやMS-DOSなどの多くのOSでも、こうしたバッチ処理の便宜を図って「ストリーム」という概念が採用されていました。その結果、バッチ処理でカバーできる応用範囲は拡大され、テキスト処理の発達もあいまって「GUIアプリケーションはテキストエディタだけ」的な文化が築かれ、多くの場合上記のようなプログラムでも全く問題が発生しなかったのです。

しかし、コンピュータがハッカーだけのものでなくなり、GUのアプリケーションが必要とされるようになってくると、上記のようなプログラムでは対応できなくなってしまいました。詳しい話はユーザーインターフェイスの書籍に譲りますが、とにかくGUIアプリケーションではプログラムの作成時点ではユーザがどのような順序でどのような処理を行うかを全く特定できないため、「何かが起こるまで待つ」ようなプログラムの書き方をせざるを得なかったからです。

このように「何かが起こるまで待つ」式のプログラムをイベントドリブン方式のプログラムと呼びます。一般的には次のように書きます。

int main(int,char**)
{
	gui_message_t message;

	while(!gui_get_message(&message)){
		gui_dispatch_message(&message);
	}
}

上のプログラムは実際に存在しない仮想的なAPIを用いたコードですが、gui_get_messageでユーザの入力(メッセージ)を受け取り(メッセージがなければユーザから送られるまで待つ)、gui_dispatch_messageでそのメッセージに応じたイベントを実行する、という構造を表しています。このメッセージに応じたイベントの振り分けをディスパッチと呼びますが、これらはしばしば関数ポインタ・仮想関数などで実装されます。

じゃあゲームはどうなの?

さて肝心の、ゲームではどちらのやり方が良いか、です。

大概のゲームはやはりGUIアプリケーションですので、イベントドリブン形式の方がゲームには向いています。中でも、「時間の経過」をイベントとして扱うやり方(微分の考え方と同じ)は、アクションゲームなどすべてのゲームで大変効果を発揮します。

また、非イベントドリブン方式でプログラムを組むと、どうしてもコードの都合で不自由なインターフェイスを作ってしまいがちです。

それだけでなく、イベントドリブンのプログラムにはちょっとしたプロっぽさ(ユーザの入力手順と非同期なオブジェクトが画面上で動いている、など)を加えやすいので、今までやっていなかった人は、できればこちらでプログラムを組むような習慣をつけておいたほうが良いでしょう。

後でオブジェクト指向に移行するときもこちらのほうがスムーズにいきます。

イベントドリブンの問題点 ーシナリオとの排他性ー

しかしながら、イベントドリブン方式とて万能ではありません。

最大の問題点は、イベントの処理に与えられた時間の少なさです。

いくらイベントドリブンとかマルチスレッドとかオブジェクト指向とかプログラムに工夫をこらしたところで、言われたことをいわれた順番にやるというコンピュータの基本的な機能が変わるわけではありません。上であげたイベントドリブン方式のプログラムのメインルーチンを見ればわかるように、イベントドリブン方式とは、基本的にはイベントをシーケンシャルにしたてあげて実行する方式のことなのです。

ということは、一つ一つのイベントの処理に時間をかけていては他のイベントが実行できなくなるということです。こうなってしまってはイベントドリブン方式を採用した意味がありません。

もう一つ、たとえば上で書いたように「時間」をイベント化した場合、その時間で起こるイベントをすべて処理した後でなければ時間が進みません。そのため、非イベントドリブンなら簡単に書けた

(define (event118)
    (move -3 0)
    (move 0 3)
    (move -7 0))

のようなバッチ処理が、イベントドリブンでは素直に書けなくなってしまうのです。なぜなら、このバッチ処理が暗黙のうちに時間の経過を含んでいる(主人公が一瞬で目的地に移動したら、経過に意味がなくなってしまいますよね?)ため、「イベントは単位時間内(概念的には瞬間)に処理される」という前提と矛盾してしまうからです。

これを回避するためには、メッセージキューを使うなどの処置が必要になってしてしまいます。メッセージキューでできるならいいじゃないか、と言いたいところですが、実際にやってみればわかるように、このやり方では戻り値の処理や分岐などの実行の制御が困難で、それを実現するのはインタプリタを実装するのと同じです。

そんな面倒なことをするのはイヤだ!

近代OSでは、そんな面倒なことをするのがイヤな場合に使えるありがたい機能が備わっています。マルチスレッドです。

マルチスレッドというと、どうしても複数のスレッドが同時に実行されるものを思い浮かべてしまう人が多いでしょう。賢明なあなたなら、当然そうなると排他制御もしなくちゃいけないんだろうなあ、それはバグの温床になるんじゃないかなあ、と推論するでしょう。

実際概ねそうなのですが、必ずしもそうとばかりでもありません。スレッドを並行して複数走らせることができるからと言って、必ずしも常に複数走らせる必要はありません。単に実行コンテキストを複数持てるだけのものとみなして、ある一瞬を見ると必ずどれか一つのスレッドしか走っていないような使い方もできるのです。もちろんその際、実行してない側のスレッドのオーバーヘッドはありません(世の中広いですから、そうじゃないOSもあるかもしれませんが)。

そろそろ本題に

ちと前置きが長くなってしまいましたが、そろそろ本題に入ることにしましょう。

VAFXフレームワークは、まさに上で述べたような、時間をイベントの一つとしたイベントドリブン構造になっており、1フレームつき必ず1回ずつ各ゲームコンポーネント(ユーザが作った自機とか弾みたいなゲームオブジェクトを含む)の仮想関数OnProcessを呼び出す仕組みになっています。

この構造は、普通にゲームオブジェクトを定義する分には楽でいいですが、シーケンシャルなスクリプトの実行にはあまり向いていません。オーバーライドされたOnProcessの処理はごく短い時間で終わることが要求されているからです。

これと、シーケンシャルなスクリプトを処理すべく導入されたVismを組み合わせるには、いったいどうしたらよいのでしょうか?

スクリプト処理をスクリプトインタプリタなしで行うとしたら、事実上マルチスレッドを使うしかないでしょう。そうでない方法では、かなり複雑なオブジェクト間通信をハードコーディングせざるを得なくなってしまいます。現実的な制限上そうせざるを得ない場合もあるでしょうが、未来を生きるプログラマがそんな馬鹿げた行為を疑問なくやっていてはいけません。

一方、スクリプトインタプリタがあれば、

のどちらかを選択できます。

Vismではこれらに加えて、schemeならではの、

という選択肢が存在します。この継続を使ってイベントドリブンなプログラムとシーケンシャルなスクリプトの連携を取る方法を説明しようというのが、本稿の趣旨だったわけです。

一応それ以外のものも

一応ざっとそれ以外の選択肢についても説明しておきましょう。

マルチスレッド

現実問題としては、OS備え付けのマルチスレッドを使うのが一番簡単でしょう。欠点は、どうしても環境依存になりがちなことと、並列実行に伴う排他制御の問題が浮上する(ことがある)ことです。

継続を使うよりも若干制限はかかりますが、プログラミングにかかる制約を鑑みた上で、マルチスレッドを使っても問題がないようであれば、使ってしまうのがよいでしょう。

Vismでマルチスレッドを使うなら、

といった構造になるでしょう。Vismの利用に限らずゲーム進行をネイティブコードで書いても似たような構造になるでしょうから、特に問題ないと思います。

仮想マシンの監視・制御

まず最初に、この方法はVismではまだ実現不可能である、と断っておきます(私に需要がないので)。

Vismは、Vism.cppのvism::eval関数を見ればわかるように、「スタックから実行すべきコードをとってきて実行する」という処理を実行すべきコードがなくなるまで繰り返す仕組みになっています(ただし通常の中間コードプロセッサとは異なり、コードは抽象的な構造(lisp木)になっています)。

今のところVismではVIEval APIを用いてこれを一括りに実行することしかできませんが、これを分解して仮想マシンの制御をクライアント側で行えるような仕様を考えています。これが実現した場合、たとえばグローバル変数の値によって仮想マシンを動かしたり動かさなかったりすることができます。

つまり、メッセージ・メニューなどのユーザ入力待ちに入った場合は、フラグを立てることによって一時的に仮想マシンの実行を止めることができるということです。

持ち越し

というわけで本稿はここまでとし、いよいよ次回は実践に入りたいと思います。


(C) 1998 Naoyuki Hirayama. All rights reserved.