■ 新・ゲーム開発講座




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


■第28夜:ラベルとジャンプ

フラグ、分岐、と来たところで次に欲しい機能は何でしょう。#if_flag によって処理を分岐することは出来ましたが、いくらネスト記述が出来るからといって、複雑な括弧の応酬が繰り返されたのではスクリプトが見づらくなってしまいますよね。分岐したらなるべく早い段階で #if_flag 文のテキストブロックの呪縛?から逃れて、別の場所にジャンプできると便利です。そんな訳で、ここではラベルジャンプについて解説します。コマンドとしての書式は以下のようなものに致しましょう。

■書式:#jump *LABEL:

ラベル *LABEL: の直後にテキストポインタを移動する。

【ラベルの定義】
ラベルは「*」で始まり「 : 」で終る32文字以内の英数字で、途中にスペースは含まない。ジャンプ先として有効なラベルは行頭に位置するものに限定するが、インデント対策として行頭〜ラベル間にスペースまたはTABが入ることは許容する。なお、ラベルは1スクリプト中にいくつ記述しても構わないが、ジャンプ先として有効なのはファイル先頭からサーチして最初にHITしたものとする。(・・・ややこしいな ^^;)



「ラベル」というのは直訳すれば「張り紙」ですが、要するに目印のことです。#jump コマンドは、この目印をスクリプトファイルの先頭から順次サーチして、該当するものがあったらその位置の直後にテキストポインタを移動します。ポインタの移動なんてものは1行で済んでしまいますので、今回のミソはラベルのサーチにあると言えるでしょう。

■サーチ

サーチと言っても、データベースを木構造で探索するような高度なものではありません。単純にファイル先頭から「ラベルはないか〜?」と1バイトずつ比較して探していく芸のないやりかたです。ただしレベル1のワザでも「使えない」「思い浮かばない」ことに比べたら天と地ほどの差があります。たとえ「へっぽこ」でも、勇気をもって力強く生きていきましょう(爆)

連続する文字列の中から特定の文字列(ラベル)を抽出する一番簡単な方法は、ラベル自身の「先頭文字」と「終端文字」を特定してしまうことです。終端文字の代わりに「必ずスペースが入る」でも良いのですが、たまたま文章中の特定の記述に引っかかって誤サーチになる恐れもありますので、ここでは明示的に「:」を終端に入れて区別できるようにしています。

サーチの基本アルゴリズムは単純です。スクリプトファイルの先頭から「*」を探してひたすら1バイトずつ比較をしていきます。「*」がみつかったら、32文字を限度としてその先に「 : 」がないかどうか探して、「 *なんとか: 」 の形式が成立していたらラベルとみなして比較をします。合致したらテキストポインタを書き換え、合致しなければ次を探します。

さて、そうは言っても次のような場合はどうしましょう。先頭から単純にサーチするだけでは、#jump *AAA:(=ジャンプ元の自分自身)のところでヒット扱いになっておかしな動作になってしまいます。さらにこのラベルにジャンプしてくる処理は、1本のスクリプトファイルに何回も出てくるかも知れません。


#if_flag 0 {
いろいろな処理1

/
いろいろな処理2
#jump *AAA:

}
いろいろな処理3

*AAA:
いろいろな処理4
*AAA:
いろいろな処理5



そんな理由もあって、【ラベルの定義】では「行頭にあるラベルのみがジャンプ先として有効である」と規定してあるのです。「行頭」という条件を付ければ、ラベルの直前にあるのは1段上の行の改行コードか、インデント用のタブまたはスペースということになりますので区別ができます。これで少なくとも #jump *AAA: のようにコマンドパラメータとして記述してあるラベル文字列は除外できます。まあ、序論はこのくらいにして実際のコードを見ていきましょう。

■Mode_stat

今回の処理では Mode_stat は使用しません。

■コマンド解釈部

TextEngine.cpp の void Command_call() に以下の部分を加えます。


void Command_call()
{

//省略

//コマンド名の評価 → 処理関数呼び出し
if( strcmp(com_name,"delay" )==0 ){ Com_delay(); flag=ON;} //遅延
if( strcmp(com_name,"wait" )==0 ){ Com_cursor_blink(); flag=ON;} //カーソルブリンク
if( strcmp(com_name,"w" )==0 ){ Com_cursor_blink(); flag=ON;} //カーソルブリンク
if( strcmp(com_name,"halt" )==0 ){ Com_halt(); flag=ON;} //終了
if( strcmp(com_name,"page" )==0 ){ Com_page(); flag=ON;} //改ページ
if( strcmp(com_name,"g_load" )==0 ){ Com_g_load(); flag=ON;} //BMPのLOAD
if( strcmp(com_name,"g_copy" )==0 ){ Com_g_copy(); flag=ON;} //BMPのCOPY
if( strcmp(com_name,"g_change")==0 ){ Com_g_change(); flag=ON;} //画面切替
if( strcmp(com_name,"flag_on" )==0 ){ Com_flag_on(); flag=ON;} //イベントフラグON
if( strcmp(com_name,"flag_off")==0 ){ Com_flag_off(); flag=ON;} //イベントフラグOFF
if( strcmp(com_name,"disp_flag")==0 ){ Com_disp_flag(); flag=ON;} //イベントフラグ表示
if( strcmp(com_name,"if_flag" )==0 ){ Com_if_flag(); flag=ON;} //フラグによる分岐
if( strcmp(com_name,"jump" )==0 ){ Com_jump(); flag=ON;} //ラベルジャンプ

//省略


}

■コマンド実行部

コマンド実行部は以下のようになります。ちょっとややこしいループ構造になってしまったため、条件が適合した場合のループ抜けに禁断?の goto を使っていますが、まあ気にしないで行きましょう。

int Com_jump()
{

char LABEL_1[256],LABEL_2[256];
char *ptr; //作業用ポインタ
int cnt=0; //カウンタ(0で初期化しておく)
int flag=OFF; //loop flag

//パラメータ文字列から目標となるラベルを切り出す
TEXT++;
strcpy( LABEL_1,Kaiseki_TextStr() );

//作業用ポインタ *ptr をテキストバッファ先頭にフィット
ptr = (char *)TEXT_BUF;

//1文字ずつ比較して「*」を探す
do{
if( *ptr=='*' ){

//*を見つけた

//それは行頭か?→まずはTAB(0x09)、SPACE(0x20)をたどってさかのぼる
int j=-1;
int flag=OFF;
while( *(ptr+j)=='\x9' || *(ptr+j)=='\x20' )j--;

//その先にあるのが前の行の「行末」ならジャンプ先候補となり得る
switch( *(ptr+j) ){
case 0xd: //CR
case 0xa: flag=ON; //LF
break;

default: //スクリプトの最初の1行の先頭だった場合を考慮
if( TEXT_BUF - (unsigned char*)(ptr+j) > 0 ) {
flag=ON;
}
}

//ラベルの切り出し
int i=0;
do{
LABEL_2[i]=*ptr;
ptr++;
i++;
cnt++;
}while( i<32 && *ptr!=' ' && *ptr!='\xa' && *ptr!='\xd' && *ptr!='\x9' );
LABEL_2[i]=0;

//行頭フラグが立ち、かつラベルが一致したら脱出
if( flag==ON && strcmp(LABEL_1,LABEL_2)==0 ){
goto OUTLOOP;
}


}else{
// *ではない
ptr++;
cnt++;

}
}while( cnt<SIZE_OF_TEXT_BUF && flag==OFF);

char str[256];
sprintf(str,"ラベル [%s] が見つかりません。強制終了の刑♪",LABEL_1);
msg(str,"Com_jump()");
PostQuitMessage(0);



OUTLOOP:

TEXT = (unsigned char *)ptr; //テキストポインタ書換え(ジャンプ)

return 0;


}

ラベルサーチ部分で、行頭チェックをしている部分に if( TEXT_BUF - (unsigned char*)(ptr+j) > 0 )・・・という記述がありますが、これは「前の行末が見つかれば」式に行頭チェックをしていると、スクリプトファイルの一番最初の行にラベルがあった場合に拾えなくなってしまうための苦肉の策です(^^;)。ポインタ同士を引き算するとメモリ間の距離が得られますので、その符号をチェックすることで前後関係を知ることができます。ラベルの直前に何があるかチェックしようとしてさかのぼったらテキストバッファの先頭を通り越してしまった・・・という場合のみ、「最初の行にラベルが記述してある」という評価を行う訳です。

■ラベルは、見えてはいけない

さてここまでの内容でラベルジャンプは可能になりました。・・・が、大事なことが一つ抜けています。今のままではラベルが丸見えになってしまうのです。このままゲームに応用すると、せっかく物語が盛り上がっているときに
*AAA:おお、オスカル、お前に私の愛を捧げよう*BBB:私もだアンドレ。聞こえるかこの胸の高鳴りが*CCC:・・・」 …などとラベルとセリフの混ざったオマヌケな表示になってしまいます。これはいけません。

ラベルはジャンプ先を現わす単なる目印ですから、テキスト表示上は無視するようにしましょう。そんな訳で、制御文字チェックを担当している TextEngine.cpp の Check_control_chr() を少し拡張します。「*」を見つけたところで、空白を挟まずに32文字以内に「:」があればラベルとみなしてテキストポインタを飛ばしています。


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

{

int i,label_flag;

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

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

case ';': Com_linefeed(); //[;] 改行
return true;

case '*': //ラベルに遭遇したら読み捨てる
label_flag=OFF;
i=0;
do{
if(*(TEXT+i)==':'){
label_flag=ON;
i++;
break;

}
if(*(TEXT+i)==' ')break; //[ : ]に出会わないままスペースに達した
//→ラベルではない
i++;

}while(i<=32);

if( label_flag==ON ){
TEXT+=i; //ラベル長だけ読み捨て
}
return true;

}

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


}
では、サンプルソースをコンパイルして実行してみて下さい。スクリプトファイル default.txt の内容と実際の表示を比較すると、ラベルジャンプの動作状況が良く分かると思います。