■ 新・ゲーム開発講座




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


■第38夜:文字列変数

今回は、読者の方から質問があったので文字列変数について書いてみました。ここでいう文字列変数とは、ADVの主人公の名前などのテキスト情報を保持できる仕掛けのことを指します。スクリプトを書いている時点では確定していない固有名詞やフレーズなどを変数として自由に扱うことができれば、ゲームとして表現できることの幅が広がりますね(^0^)。

キャラの名前と言えば、たとえば

老婆 「わしの名前はのぉ〜、長森瑞佳だよもん!」

…なんてやろうものなら一部の同姓同名キャラのファンから非難轟々になりそうですので、ここはひとつ

老婆 「わしの名前はのぉ〜、\STR 00 だよもん!」


とスクリプト上では記載しておき、表示するときに変数の内容によってキャラ名を変更できるようにしておけば便利そうです。そんな次第で、さっそく行ってみましょう。スクリプトの書式としては、以下のようなものでよいでしょう。ソースは
こちらです。


■文字列変数に値(文字列)を書き込む

書式:#set_str 変数名

変数名:\STR * (* は0〜16の変数番号)


■文字列変数の表示

書式:\STR * (* は0〜16の変数番号)


■内部処理コマンド:文字列変数の終端マーカ

書式:\EOS


変数は、ここでは 256 byte 固定長のバッファを16個用意して、それぞれを番号で区別するようにしたいと思います。「256 byteも要らないよ」 「16個じゃ足らない」 という方は適宜ソースを改造してくださいね♪ また変数名のアタマに \ がついていますが、これは「文字列処理系コマンド」 を表すための記号としてくににんが勝手につけた識別子です。文字列変数を使い出すと、人によっては汎用変数だけではなく \ITEM とか \CHR_NAME とか \SPEL とかいろいろ追加したいだろうと思いますので、多少の拡張性(…というほどのものでは ^^;) を考慮したかたちで実装したいと思います。


■スクリプトに文字列を挿入するって?

ところで、文字変数をスクリプトに挿入する形で表示するというのはどういうことでしょう。本講座で作成中のシステムでは、バッファに読み込んだスクリプト上をポインタ *TEXT が順次なぞるように1文字ずつたどりながら表示していきます。ここに後から余計なテキスト(文字変数)を挿入する訳ですが、さすがに文字変数の記述を発見する毎にいちいちスクリプトを直接書き換えていたのでは面倒です。

このような場合、スクリプト本体を書きかえるようなことは一切しないで、*TEXT を瞬間的にダミーバッファ上で走らせることで文字変数の挿入を実現できます。第28夜のラベルジャンプと似たような処理で、文字変数を格納した配列上に *TEXT を飛ばし、文字変数の終端に達したところで元のスクリプト位置に *TEXT を戻してやるのです。この間、テキストインタプリタの本体ループは *TEXT がどこを走っていようが一切感知することなく、ウェイトをとりながら単純に文字表示を続けています。つまりプレーヤーから見た場合、表示しているのがナマのスクリプト部分でも文字変数の部分でも、画面上の見え方は一緒ということになります。




今回の処理では、この *TEXT を戻すというところがミソと言えるでしょう。別に難しい処理ではありませんが、そのために内部コマンドとして \EOS (=End of String)というのを用意しました。文字列変数の最後に \EOS の文字列を記述しておき、これを見つけたところで *TEXT を戻す処理をしよう、という算段です。文字列の終端はC言語のキマリで16進数の 0x0 になっているのでここをチェックすれば良いようにも思えますが、ここでは明示的に \EOS を終端マーカーとして使います。


■文字列変数の実装

ではさっそく参りましょう。文字列変数はイベントフラグと同じ扱いでフラグ構造体 Mode_stat のメンバとして以下のように実装します(main.h)



#define EVENT_FLAG_MAX 256
#define STR_STR_MAX 16

struct _Str256 {
char str[256];
};

struct _Mode_stat {
int flag_text; //テキスト表示:ON=進行 OFF=停止
int flag_delay; //遅延処理:ON=遅延あり OFF=遅延なし
int flag_cursor_blink; //カーソル点滅:ON=点滅 OFF=点滅なし
int flag_halt; //終了:ON=終了 OFF=終了ではない
int flag_page2; //改ページ type2:ON=改ページ処理中 OFF=処理中でない
int flag_g_change; //画面切替効果:ON=切替中 OFF=処理はない
int flag_select3; //3択:ON=選択中 OFF=処理はない
char event_flag[EVENT_FLAG_MAX];
_Str256 str[STR_STR_MAX]; //汎用文字列変数
};
extern _Mode_stat Mode_stat;



わざわざ _Str256 などという構造体を定義しているのは、単純に str[16][256] などとしてしまうと、実際にメモリ上で連続した256byteの領域が確保される保証がないためです。そのためここではまず 256 byte の配列を構造体として宣言して、それを16個ぶん配列として確保する方法をとっています。


■#set_str コマンドの実装(文字変数に値をセット)

結構ながくなりそうですので、ここではまず文字列変数をセットする #set_str コマンドを先に解説して、その後 \STR *\EOS の実装について解説することにします。

#set_str コマンドについては、従来のコマンド追加と要領はまったく同じです。処理の振り分けを行っている 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;} //ラベルジャンプ
if( strcmp(com_name,"file_change")==0 ){Com_file_change(); flag=ON;} //ファイル間ジャンプ
if( strcmp(com_name,"select3" )==0 ){ Com_select3(); flag=ON;} //3択
if( strcmp(com_name,"play_wav")==0 ){ Com_play_wav(); flag=ON;} //WAV演奏開始
if( strcmp(com_name,"stop_wav")==0 ){ Com_stop_wav(); flag=ON;} //WAV演奏停止
if( strcmp(com_name,"play_midi")==0 ){ Com_play_midi(); flag=ON;} //MIDI演奏開始
if( strcmp(com_name,"stop_midi")==0 ){ Com_stop_midi(); flag=ON;} //MIDI演奏停止
if( strcmp(com_name,"play_mp3")==0 ){ Com_play_mp3(); flag=ON;} //MP3演奏開始
if( strcmp(com_name,"stop_mp3")==0 ){ Com_stop_mp3(); flag=ON;} //MP3演奏停止
if( strcmp(com_name,"set_font_size")==0 ){ Com_set_font_size(); flag=ON;} //フォントサイズ変更
if( strcmp(com_name,"set_text_area")==0 ){ Com_set_text_area(); flag=ON;} //テキストエリア変更
if( strcmp(com_name,"set_text_wait")==0 ){ Com_set_text_wait(); flag=ON;} //テキストウェイト変更
if( strcmp(com_name,"set_text_color")==0 ){ Com_set_text_color(); flag=ON;} //テキストカラー変更
if( strcmp(com_name,"set_str")==0 ){ Com_set_str(); flag=ON;} //文字列変数に値をセット

// ↑
//そのうちここに他のコマンドも追加していきましょう
// ↓

省略

}


個別処理関数 Com_set_str() は、新設した Text_Str_01.cpp に記述することにします。内容は特に難しいことはありません。コマンドパラメータを解釈して、変数に文字列をコピーしているだけです。注意すべきことは、文字列をコピーした後、終端マーカーとして \EOS を書きこんでおくことでしょう。文字列の切り出し処理は TextEngine.cpp に記述してある便利関数を呼んでいます。特に解説はしませんがダブルクォーテーテション「"」で囲まれた文字列の切りだし方なんかが書いてありますので興味の有る方は読んでみてください。

変数に文字列をコピーする部分を別に分けたことにはあまり深い意味はありません。後日、汎用文字変数の \STR * 以外に文字変数の種類を増やしたくなった場合、多少リストが見やすくなるかな…という程度のものです(^^;)。


int _set_str( int num, char *str ) //わざわざ分割記述にしたのは将来の拡張を考えてのことです(^^)
{

strcpy( Mode_stat.str[num].str, str); //文字列コピー
strcat( Mode_stat.str[num].str,"\\EOS"); //終端マーキングとして \EOS を書き込んでおく

return 0;

}

int Com_set_str()
{

char name[256]; //変数の名前
int num; //変数の番号
char str[300]; //設定したい文字列
int flag=OFF;

TEXT++;
strcpy( name,Kaiseki_TextStr() ); //Kaiseki_TextStr() はテキストから文字列を切り出す関数
while( *TEXT==',' || *TEXT==' ' || *TEXT==0x0a )TEXT++; //カンマ、スペース、TABの読み飛ばし

num = kaiseki_10();
while( *TEXT==',' || *TEXT==' ' || *TEXT==0x0a )TEXT++; //カンマ、スペース、TABの読み飛ばし

strcpy( str,Kaiseki_TextStr_DQ() ); //Kaiseki_TextStr_DQ() はテキストから文字列を切り出す関数

if( strcmp( name,"\\STR" )==0) { _set_str( num, str ); flag=ON; };
// :
// 他の文字列変数を追加する場合はここに加えていきましょう♪
// :

if( flag==OFF ) {
char err_mess[256];
sprintf( err_mess,"無効な文字列指定子 [%s] が記述されています",name );
MessageBox( hwnd,err_mess,"Com_set_str()",MB_OK);
}

return 0;



}



■文字列変数 \STR * の処理部分の実装

次に、文字列変数の表示部分の実装です。今回は \ を識別子として処理を分岐しますので、制御文字のチェックを行っている Check_control_chr() \ の処理を追加します。なおリスト中で \\ と表記してあるのは、\エスケープ文字を表す予約語となっているためです(このへんの事情はVCのマニュアルを見てください)。とりあえずこの関数の中では文字変数処理を行う関数 String_call() を呼んでいるだけです。



int Check_control_chr()
{

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 '*': //ラベルに遭遇したら読み捨てる
abel_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;

case '\\': //文字列変数
String_call();
return true;

}

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

}


文字変数の処理を担当する String_call() は、コマンド処理関数 Command_call() と内容的にはほとんど同じ構成になっています。\ をみつけてこの関数に処理が移ったとき、テキストポインタ *TEXT は文字列変数(文字列コマンド)名のアタマ=\ そのものを指しています。そこで TEXT++ して \ を飛ばし、それに続く文字列部分(=変数名/コマンド名)を切り出します。そして切り出した変数名/コマンド名を strcmp() で評価し、該当する個々の処理関数を呼び出します。

今回のソースでは汎用文字列変数 \STR * と 終端処理コマンド \EOS しか実装していませんが、将来ここに別の文字列変数を追加したい場合は Command_csll() を参考にして増設していってください。



void String_call()
{

char com_name[256];
int i=0;
int flag=OFF;

TEXT++; //\を飛ばす

//文字列の切り出し
while( *TEXT>0x20 ) {
com_name[i] = *TEXT;
TEXT++;
i++;
}
TEXT--;
com_name[i]=0; //これはNULL文字

//コマンド名の評価 → 処理関数呼び出し
if( strcmp(com_name,"STR" )==0 ){ Str_str(); flag=ON;} //汎用文字連数
if( strcmp(com_name,"EOS" )==0 ){ Str_eos(); flag=ON;} //End of string == 終端

// ↑
//そのうちここに他の文字変数処理も追加していきましょう
// ↓

//フラグを見て、切り出された文字変数名が有効な名称だったかどうか確認する
if( flag == OFF ) {

char str[256];

sprintf( str,"警告:無効な文字変数 [\\%s] が記述されています",com_name);
MessageBox(NULL,str,"String_call()",MB_OK);

}


}


■文字列処理(Text_Str_01.cpp)

個別の文字列変数処理については、新規に Text_Str_01.cpp というソースに記述することにします。まず、*TEXT を一時的に飛ばしてしかる後に元に戻す…という処理を行うためには、もとのアドレスを記録しておく変数が必要になります。そこでグローバル変数として *TEXT_ORG を用意します。

\STR * の処理は Str_str() で行っています。やっていることは至極単純で、変数の番号を解析して *TEXT の内容を *TEXT_ORG に保存、その後文字列変数の先頭に飛ばしているだけです。リスト中 Mode_stat.str[num].str -1 という記述がありますが、これは Str_str() の処理が終わってTextEngine のメインループに戻った時、*TEXT が1文字分インクリメントされるのを先取りしたものです。とにかく、これで *TEXT は文字列変数領域の上を走るようになります。



unsigned char *TEXT_ORG; //*TEXT の保存用

int Str_str()
{

int num;

//変数の番号を解析する
TEXT++;
num = kaiseki_10(); //※kaiseki_10()はテキスト中の10進数を解析する関数で、TextEngine.cppにあります

if( num >= STR_STR_MAX ) {

char err_mess[256];

sprintf( err_mess,
"変数番号に %d が指定されましたが有効範囲を超えています。\n"
"文字列変数は 0 〜 %d 番までが有効です。はにゃ〜ん(-0-)",
num,STR_STR_MAX

);

MessageBox( hwnd,err_mess,"Str_str()",MB_OK );
return 0;

}

TEXT_ORG = TEXT; //*TEXT のオリジナルの位置を保存

TEXT = (unsigned char *)Mode_stat.str[num].str -1; //*TEXT をスクリプトから文字変数の先頭に飛ばす

return 0;




また \EOS の処理は、そのまんま *TEXT の値を書き戻すだけです。あれれ、たとえばスクリプトに \STR 02 などと書いてあったら、この \STR 02 の文字数分だけ調整する必要があるんじゃないの…と思った方もいるかも知れませんが、心配は無用です。String_call()Str_str() でテキスト解釈している間にポインタ *TEXT はインクリメントされて、ちゃんと辻褄は合っているのです(^^)


int Str_eos()
{

//元の位置に復帰
TEXT = TEXT_ORG;

return 0;

}


■実行させてみると…

ではコンパイルしてサンプルスクリプトを走らせてみましょう。今回はちょっと猟奇サスペンス調に味付けしたスクリプトです(爆 ^^;)。ああ、ドロ○ジョ様ごめんなさい…♪





ではまた次回…♪(^^;)