■ 新・ゲーム開発講座




■ へっぽこプログラミング入門♪


■第10夜:スクリプトコマンドの基礎「遅延」@

前回までの内容で、ノベルらしいテキストの表示の基礎は出来ました。しかし現在の状態では、読み込んだテキストをただ垂れ流し的に表示しているだけで、そのままゲームに応用するには無理があります。テキスト表示には適当なところで一時停止して欲しいですし、場合によっては選択肢を選べるようにもしたいものです。

このような文章本体以外の制御用の命令は、スクリプトコマンドとして解釈 → 実行できるようにしておくと便利です。たとえばテキストを次のように記述しておきます。

ああああああああああああ
#wait
ああああああああああああ


このようなテキストを読み込んで逐次文字を表示していき、識別子 # を見つけたところで一時停止(カーソルをブリンクさせてマウスクリックを待つ)をさせるような処理が出来れば良い訳ですね。今回は、このようなコマンドの解釈 → 実行部の雛形を作ります。

■基本は「遅延」コマンド

とはいえ、いきなりカーソルブリンクをさせるのは少々面倒な部分もありますので、もっと基本的なコマンドから実装していきましょう。最も基本的な(簡単という意味ではない)コマンド・・・それは、「遅延」です。テキストを表示していく途中で、ある一定時間だけ表示を停止させるものです。仕様としては次のようなもので良いでしょう。

書式: #delay n

n :遅延させたい時間(ミリ秒)


内容: テキスト表示を nミリ秒停止し、nミリ秒停止後は
ふたたび表示を再開する


たったこれだけですが、これ以降整備していく各種コマンドの基本要素はすべて入っています。その基本要素とは・・・「飲む」「打つ」「買う」・・・ではなくて(爆死)、

@初期化及び実行開始(撃つ)
AMainloopから呼ばれ続けるタスク処理(回す)
B終了処理(絞める)

の3要素です。なんのこっちゃ、とお思いの方もいることと思いますので、もういちどスケルトンの動作を思い出してみましょう〜♪





図を見ていただければ思い出して頂けると思いますが、キー入力などのイベントが無い限りプログラムの実行はひたすら Mainloop() を駆け抜ける構造になっています。遅延処理といえどもこのプログラム構造から逃れることは出来ません。「とにかく止まればいいんだろう」的に Sleep(1000) などと安直な記述をすると、キースキャンも含めてプログラム全体の動作が止まってしまい、じつにみっともない格好になります。なんとか工夫をして、タッチ&ゴー式に Mainloop() を駆け抜け続けるような記述にする必要があります。つまり、一見単純そうにみえる遅延処理も、処理開始(撃つ)、処理本体ループ(回す)、処理終了(締める)の要素に分解して時間スライス的にインプリメントする必要がある訳です。

■Mainloopの処理

そのために、フラグを一つ用意します。そして、以下のような組み方をします。



int flag_delay; //遅延処理用のフラグ

void Mainloop(void)
{
//フラグによってMainloopから呼ぶ関数を切り替える
if(flag_delay == OFF) {
//テキストエンジンを呼ぶ(こちらは文字表示を担当)
Text_engine();

} else {
//遅延フラグが立っていたら、遅延タスクを呼ぶ
Com_delay_task();

}

}



実際にゲームとして成立するくらいの数のコマンドをインプリメントした場合はもう少し気の効いた記述にした方が良いのですが、まずは Mainloop の中でフラグによって処理を振り分ける構造を理解して頂きたいと思います。実はこの部分が、コマンド処理の「回す」部分にあたります(実際の処理部についてはもう少し先で解説します)。

■フラグを操作するには?

さて、Mainloop でフラグを参照しながらコマンド実行部を呼ぶことは分かりました。ではフラグそのものはどこで操作するのでしょう。

・・・もちろん、テキスト解釈のどこかで操作する訳です。ここでは、テキスト表示をする部分を拡張して、コマンド解釈部を作ってみましょう。こんなソースになります。

第7夜で決めた仕様で、コマンドは識別文字として頭に #を付けることにしてありました。「#」は1バイト文字ですので、1バイト文字の表示部分で識別して処理を振り分けるのが妥当でしょう。そんな訳で、TextEngine() の中身を以下のように変更します。Check_control_chr() というのが今回新しく付け加えた関数で、テキストポインタの指している1バイト文字が「#」ならばコマンド解析を行って true を返し、「#」意外なら false を返しています。ここから、「撃つ」処理に向かって枝葉を伸ばしていきましょう。



//-------------------------------------
// ここがエンジン部本体ね♪
//-------------------------------------

int Text_engine ()
{

//------------------------------------------
// ウェイトを取りながらテキストを表示する
//------------------------------------------
int color1=RGB(100,200,200); //文字色
int color2=RGB( 10, 10, 10); //影色

//指定時間経過していない場合→Mainloopに戻る
if( HLS_timer_check(TEXT_TIMER,TEXT_WAIT)==false )return 0;

HLS_timer_start(&TEXT_TIMER); //タイマー再スタート

//■1文字を表示する
if ( (unsigned char)(*TEXT)<(unsigned char)0x80 ) {

//コントロール文字かどうかを判断し、該当しなければ画面に出力
if( Check_control_chr()==false ){
ChrPut3D(win_hdc,TEXT_X ,TEXT_Y ,Font_Size,color1,color2,(char *)TEXT,1);

Increment_textp_pos(1); //文字表示位置のインクリメント=1byte

}
TEXT++; //テキストポインタを1バイト進める

} else {

//2バイト文字の場合は2バイトずつ出力
ChrPut3D(win_hdc,TEXT_X ,TEXT_Y ,Font_Size,color1,color2,(char *)TEXT,2);

Increment_textp_pos(2); //文字表示位置のインクリメント=2byte

TEXT+=2; //テキストポインタを2バイト進める


}

return 0;


}


■制御文字の解析

さてそこで飛んできた Check_control_chr() ですが、制御文字を switch 文で振り分け、「#」 を見つけた場合にはさらにコマンド処理部を呼ぶようにします。また、ここでは同時に TAB、LF、CR について「無視」をするようにしておきましょう。こうすることで、テキスト中の TAB や改行(LF+CR)が無効になり、いわゆるフリーフォーマット記述が出来るようになります。つまりスクリプトをソースレベルで見やすいように TAB やスペースでインデントをつけても、画面表示には影響しなくなる訳です。ただしそのかわり、明示的に改行表示をしたい場合は別途「改行コマンド」を作成しなければなりません。改行については別途後述します。



int Check_control_chr()
//*TEXT のポイントする1バイト文字をチェックし、
//制御文字であれば処理して true を返し、それ以外は
//false を返す

{
switch ( *TEXT ) {
case '#': Command_call(); //コマンド処理部を呼ぶ
TEXT++;
return true;

case 0x09: //TAB
case 0x0a: //LF
case 0x0d: //CR
return true; //何もしないで戻る==無視♪

}

return false; //制御文字に該当しなかった → falseを返す

}

※*TEXT はグローバル変数として存在するポインタです。読み込んだテキストを表示しながら順次インクリメントされています。


■コマンドの解析

さて、テキスト中で1バイト文字を見つた → それが「#」であることを確認した ・・・までは出来ましたので、次はコマンド名の識別部を作りましょう。識別といっても「へっぽこ」式に単純にします。「#」以降のコマンド名を切り出して strcmp() で比較するという原始的なもので、当たりが出たら該当するコマンド処理関数を呼んでいます。

なんだよ〜、strcmp() でいちいち評価するとコマンド数が増えたときに遅くなるんじゃないか、との声が聞こえて来そうですが・・・まったくそのとおりです(笑)。ただし私はよほど激しいアクションゲームでも作らない限り、現在のCPU速度なら甘受できるレベルだろうと思っています。高速化したいのであれば、コマンド名は文字列で持たずにコード番号(switch 文で振り分けられる)で持つという手段もありますし、根性があればテキストをコンパイルして中間コード生成するという手もあります。しかしまあ、このコーナーは「へっぽこプログラミング入門」なので、面倒なことは避けて文字列評価のまま進みましょう♪ スクリプトを書く人はプログラマとは限りませんので、少しでも分かりやすいコマンド名を文字列で与えてあげるのが親切というものです(^^;)



void Command_call()
//「#」を見つけたところでこの関数を呼ぶと、「#」に
//続くコマンド名を解析してその処理関数を呼びます

{

char com_name[256];
int i=0;

TEXT++; //#を飛ばす

//コマンド文字列の切り出し
while ( *TEXT > 0x20 ) {
com_name[i] = *TEXT;
TEXT++;
i++;

}
com_name[i]=0; //これはNULL文字

//コマンド名の評価 → 処理関数呼び出し
if ( strcmp(com_name,"delay")==0 ) {
Com_delay();

}


}

・・・とゆーことで、ちょっと長くなってしまったので次回に続きます♪