■ 新・ゲーム開発講座




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


■第32夜:選択肢@

今回は選択肢を選ぶ処理を実装したいと思います。いよいよ、プレーヤがゲームに関与できる部分に手を出す訳ですね♪ フラグも、ラベルジャンプも、#if_flag 文による分岐も、みなこの「ユーザーが選択肢を選ぶ」という機能がなければ存在意義がないと言っても良いくらいです。

今回作る選択機能は、図のようにテキスト中に3択の選択肢を表示し、マウスがクリックされるとそのメニューに応じた分岐をするというものです。あまり奇をてらったものを作ってもナニですので、オーソドックスに参りましょう。前回マウス入力を取得する仕掛けを作りましたので、それもうまく使っていきましょう。ソースはこちらになります。



書式:

#select3 {
選択肢1
選択肢2
選択肢3
/
選択肢1の処理
/
選択肢2の処理
/
選択肢3の処理
}


書式を見ると、ちょっと込み入った感じがしないでもありませんね。もしこの項から読み始めた方がおられましたら、「へっぽこプログラミング入門」の少し前の章から読み直すことをお勧めします(^^;)テキストのブロック処理については「分岐」の項で解説していますし、マウス入力に関しては前項で扱っています。またスクリプトコマンドの基本的な部分については第10夜、第11夜が参考になると思います。


■作業用サーフェイス

さて「選択」と言いますのはプログラム的に見た場合どのような処理になるのでしょうか。基本的には選択肢となる文字列を表示して、その矩形領域内でマウスの左ボタンがクリックされるのを監視する・・・というものです。マウスがクリックされた座標に応じて、該当するテキストアドレスに制御を飛ばせば処理を分岐できます。ゲームらしい視覚効果を狙うなら、クリックを待つあいだ単に空ループを回すのでは芸がないので、ここでは選択肢の上にマウスカーソルが来たら文字色を反転表示させてみましょう。

そのためには、新たに作業用のサーフェイスを2枚用意する必要があります。1枚は「選択肢文字列+背景」を保持するもの、もう1枚は「反転文字+背景」を保持するものです。処理上はこの他に「文字なし背景」を保持するサーフェイスが必要ですが、これは既に BG面として実装していますので特に新規コーディングはしないで済みそうですね。なお、メモリ効率を考えるなら選択肢の面積分だけ作業用サーフェイスを確保すべきなのですが、ここでは面倒なので640×480のゲーム画面そのままのサイズにしようと思います。何故って?・・・サーフェイス間転送で座標変換を気にする必要がありませんので♪(このへんが「へっぽこ」らしい安直なところ ^0^)。





選択コマンドの実装は、以下のような要領になります。@初期化(撃つ)の部分で作業用サーフェイスを作成して文字列を展開する。Aマウス入力監視タスク(回す)ではマウス位置によってノーマル色/反転色の選択肢画像をバックサーフェイスに転送し続ける。B終了処理(締める)では選択肢に応じたデリミタ(区切り文字)以降にテキストポインタ *TEXT を飛ばす・・・というカンジでしょうか。では、以下具体的な実装の話を進めます。

■フラグ

Mainloopで参照しているフラグ構造体 Mode_stat (main.cpp参照)に選択処理用のメンバ flag_select3 を加えます。Mainloopではこのフラグを見て「選択処理」のタスク部分を呼び出します。


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];
};

■コマンド呼び出し部

テキストエンジンのコマンド名評価→呼び出し関数 Command_call() (TextEngine.cpp)に以下の行を追加します。スクリプト上で #select3 というコマンド文字列を見つけたらその処理関数 Com_select3() を呼ぶ、ということですね。

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択

//省略

}

■Mainloop()

↓のようにメインループ(main.cpp)に処理を加えます。フラグがONならタスク処理を呼び出すようにしているだけです。


void Mainloop(void)
{

//テキスト表示
if( Mode_stat.flag_text == ON ) Text_engine();

//遅延
if( Mode_stat.flag_delay == ON ) Com_delay_task();

//カーソルブリンク
if( Mode_stat.flag_cursor_blink == ON ) Com_cursor_blink_task();

//終了
if( Mode_stat.flag_halt == ON ) Com_halt_task();

//改ページ type2
if( Mode_stat.flag_page2 == ON ) Com_page2_task();

//画面切替え効果
if( Mode_stat.flag_g_change == ON ) Com_g_change_task();

//3択
if( Mode_stat.flag_select3 == ON ) Com_select3_task();

Disp_Mouse_info();
HLS_stc_FPS(Back_DC);
BitBlt(win_hdc,0,0,640,480,Back_DC,0,0,SRCCOPY); //裏画面 → 表画面にコピー

}


■コマンド処理開始部(撃つ)

ではコマンド開始部 Com_select3() です。↓ちょっと見るとややこしいように見えますが、似たような処理の繰り返しなのでじっくり観察してください(そんなこと言う前に奇麗に分割して記述しろってば>自分 ^0^;)。

ここでは、大雑把に 事前チェック → 選択肢文字列を解析 → サーフェイスの準備 → 文字列展開 → フラグのセット までを行っています。まず一番最初は、事前チェックとして選択肢を3行分表示する余裕があるか(実際には余裕を見て5行分)を調べます。ここでは面倒なので、NGの場合はいきなり PostQuitMessage() を投げて終了しています(笑)

次の選択肢文字列の取込みですが、スクリプトの書式に従って最初の「{」を飛ばし、さらに改行その他のコントロールコードを含んでスペースまで(ASCIIコードでは全部まとめて 0x20=SPACE 以下の値になります)を飛ばしながらテキストポインタを進め、選択文字列先頭にたどり着いたら順次 Kaiseki_TextStr_CR() で文字列取込みをしていきます。Kaiseki_TextStr_CR() は TextEngine.cpp に記述してある便利関数で、行末までを文字列として取り込みます。いつものようにスペースを区切りに使わないのは、選択肢は単語1つに限らないこと、また英文のようにスペースを頻繁に挟んだものが指定される可能性もあるからです。

選択肢取込みが終了したら、作業用サーフェイスを確保して選択肢を展開します。味付けのためにいつもの50%増しで改行を行い、さらに20ピクセルほどインデントを付けていますが、このへんはお好み次第というところでしょう。また文字ピッチと文字列長さを用いて選択肢の矩形領域を決めて保持しておきます。ここで計算に用いている文字表示位置XYやピッチなどの情報については、テキストエンジンの雛形を作った第9夜を参照して下さい。グローバル変数で持っているので特に断わりもなく使っています。途中から読み始めると???になるかも知れませんね(大汗 ^^;)。
選択肢展開は、背景となるグラフィックデータをバックサーフェイスからコピーしてその上に展開します。BitBlt() の引数が異様に長いように感じるのは気のせいですので(笑)落ち着いて読んでみてください。・・・それにしても、わざわざ RECT型を導入する意味はなかったような気がするなぁ(爆死 ^^;)

選択肢画像が出来たら、それを表画面に転送し、フラグをセットして準備完了です。あとはフラグによって Mainloop からタスク処理部分が呼ばれ続けます。



HDC Select1_DC; //選択肢文字列用
HBITMAP Select1_Bitmap;

HDC Select2_DC; //反転文字列用
HBITMAP Select2_Bitmap;

RECT rect1,rect2,rect3;//選択肢1〜3の画面表示領域

#define POSI_COLOR1 RGB(255,255,100) //選択肢の色
#define POSI_COLOR2 RGB(0,0,0) //選択肢の色(影)
#define NEGA_COLOR1 RGB(255,50,50) //選択肢の反転色
#define NEGA_COLOR2 RGB(0,0,0) //選択肢の反転色(影)

/------------------------------
// 選択肢展開画面の生成と破棄
//------------------------------

void Create_select_Surface1()
{

HDC work_hdc; //作業用のDC

//■ 選択肢展開画面の初期化
work_hdc=GetDC(hwnd); //主(表)画面のDCの内容を取得
Select1_DC=CreateCompatibleDC(work_hdc); //同じ設定でDCを生成
Select1_Bitmap=CreateCompatibleBitmap(work_hdc,640,480);//主(表)画面と同じ属性で画面生成
SelectObject(Select1_DC,Select1_Bitmap); //DCと画面本体を関連付ける

ReleaseDC(hwnd,work_hdc); //作業用DCを開放


}

void Delete_select_Surface1()
{
DeleteDC(Select1_DC); //Createなんたら()関数で生成したDCは DeleteDC()で消去する
}

//------------------------------
// 選択肢反転画面の生成と破棄
//------------------------------

void Create_select_Surface2()
{

HDC work_hdc; //作業用のDC

//■ 選択肢反転画面の初期化
work_hdc=GetDC(hwnd); //主(表)画面のDCの内容を取得
Select2_DC=CreateCompatibleDC(work_hdc); //同じ設定でDCを生成
Select2_Bitmap=CreateCompatibleBitmap(work_hdc,640,480);//主(表)画面と同じ属性で画面生成
SelectObject(Select2_DC,Select2_Bitmap); //DCと画面本体を関連付ける

ReleaseDC(hwnd,work_hdc); //作業用DCを開放

}

void Delete_select_Surface2()
{
DeleteDC(Select2_DC); //Createなんたら()関数で生成したDCは DeleteDC()で消去する

}

//------------------------------
// コマンド起動部(撃つ)
//------------------------------

int Com_select3()
{

char str_1[256],str_2[256],str_3[256];

//選択肢3行+予備ピッチ分を表示するだけの画面の余裕があるか?
if( TEXT_Y+TEXT_Y_PITCH*5 > TEXT_AREA.bottom ){
msg("選択肢を表示するだけの画面の余裕がありません(T0T)\n"
"そんな訳で、すぱっといさぎよく強制終了〜♪","Com_select3() ");
PostQuitMessage(0);
return 0;
}

//選択肢文字列を解析
while( *TEXT != '{' )TEXT++; //最初の「{」を探す
TEXT++;
while( *TEXT <= ' ' )TEXT++; //選択肢1の先頭を探す
strcpy( str_1,Kaiseki_TextStr_CR() ); //行末までをそっくり取り込む

while( *TEXT <= ' ' )TEXT++; //選択肢2の先頭を探す
strcpy( str_2,Kaiseki_TextStr_CR() ); //行末までをそっくり取り込む

while( *TEXT <= ' ' )TEXT++; //選択肢2の先頭を探す
strcpy( str_3,Kaiseki_TextStr_CR() ); //行末までをそっくり取り込む

//作業用サーフェイスを作成
Create_select_Surface1();
Create_select_Surface2();

//文字列を展開(↓長いけど単純な繰り返しなのよん♪)

//選択肢1
TEXT_Y += TEXT_Y_PITCH*3/2; //Y:1.5行分だけ表示位置を下げる
TEXT_X = TEXT_AREA.left+20; //X:20pixelだけインデントをつける
rect1.left = TEXT_X;
rect1.right = TEXT_X + TEXT_X_PITCH*(strlen(str_1)+3);
rect1.top = TEXT_Y;
rect1.bottom = TEXT_Y + TEXT_Y_PITCH;
//バックサーフェイスの矩形領域を作業画面1に転送
BitBlt( Select1_DC,rect1.left,rect1.top,rect1.right-rect1.left,rect1.bottom-rect1.top,
Back_DC,rect1.left,rect1.top,SRCCOPY);
//バックサーフェイスの矩形領域を作業画面2に転送
BitBlt( Select2_DC,rect1.left,rect1.top,rect1.right-rect1.left,rect1.bottom-rect1.top,
Back_DC,rect1.left,rect1.top,SRCCOPY);
StrPut3D(Select1_DC,rect1.left,rect1.top,Font_Size,POSI_COLOR1,POSI_COLOR2,str_1);//選択肢
StrPut3D(Select2_DC,rect1.left,rect1.top,Font_Size,NEGA_COLOR1,NEGA_COLOR2,str_1);//選択肢(反転)

//選択肢2
TEXT_Y += TEXT_Y_PITCH; //Y:1行分だけ表示位置を下げる
TEXT_X = TEXT_AREA.left+20; //X:20pixelだけインデントをつける
rect2.left = TEXT_X;
rect2.right = TEXT_X + TEXT_X_PITCH*(strlen(str_2)+3);
rect2.top = TEXT_Y;
rect2.bottom = TEXT_Y + TEXT_Y_PITCH;
//バックサーフェイスの矩形領域を作業画面1に転送
BitBlt( Select1_DC,rect2.left,rect2.top,rect2.right-rect2.left,rect2.bottom-rect2.top,
Back_DC,rect2.left,rect2.top,SRCCOPY);
//バックサーフェイスの矩形領域を作業画面2に転送
BitBlt( Select2_DC,rect2.left,rect2.top,rect2.right-rect2.left,rect2.bottom-rect2.top,
Back_DC,rect2.left,rect2.top,SRCCOPY);
StrPut3D(Select1_DC,rect2.left,rect2.top,Font_Size,POSI_COLOR1,POSI_COLOR2,str_2);//選択肢
StrPut3D(Select2_DC,rect2.left,rect2.top,Font_Size,NEGA_COLOR1,NEGA_COLOR2,str_2);//選択肢(反転)

//選択肢3
TEXT_Y += TEXT_Y_PITCH; //Y:1行分だけ表示位置を下げる
TEXT_X = TEXT_AREA.left+20; //X:20pixelだけインデントをつける
rect3.left = TEXT_X;
rect3.right = TEXT_X + TEXT_X_PITCH*(strlen(str_3)+3);
rect3.top = TEXT_Y;
rect3.bottom = TEXT_Y + TEXT_Y_PITCH;
//バックサーフェイスの矩形領域を作業画面1に転送
BitBlt( Select1_DC,rect3.left,rect3.top,rect3.right-rect3.left,rect3.bottom-rect3.top,
Back_DC,rect3.left,rect3.top,SRCCOPY);
//バックサーフェイスの矩形領域を作業画面2に転送
BitBlt( Select2_DC,rect3.left,rect3.top,rect3.right-rect3.left,rect3.bottom-rect3.top,
Back_DC,rect3.left,rect3.top,SRCCOPY);
StrPut3D(Select1_DC,rect3.left,rect3.top,Font_Size,POSI_COLOR1,POSI_COLOR2,str_3);//選択肢
StrPut3D(Select2_DC,rect3.left,rect3.top,Font_Size,NEGA_COLOR1,NEGA_COLOR2,str_3);//選択肢(反転)

//テキスト表示再開したときのために改行処理をしておく
TEXT_Y += TEXT_Y_PITCH*3/2; //Y:3/2行分だけ表示位置を下げる
TEXT_X = TEXT_AREA.left; //X:20pixelだけインデントをつける

//選択肢をバックサーフェイスに転送
//選択肢1
BitBlt( Back_DC,rect1.left,rect1.top,rect1.right-rect1.left,rect1.bottom-rect1.top,
Select1_DC,rect1.left,rect1.top,SRCCOPY);
//選択肢2
BitBlt( Back_DC,rect2.left,rect2.top,rect2.right-rect2.left,rect2.bottom-rect2.top,
Select1_DC,rect2.left,rect2.top,SRCCOPY);
//選択肢3
BitBlt( Back_DC,rect3.left,rect3.top,rect3.right-rect3.left,rect3.bottom-rect3.top,
Select1_DC,rect3.left,rect3.top,SRCCOPY);

//テキスト表示を止める
Mode_stat.flag_text = OFF;

//選択のフラグを立てる
Mode_stat.flag_select3 = ON;

return 0;

}



■タスク処理(回す+締める)

タスク処理では、選択メニューの矩形部分毎に _Check_mouse_in_Rect() を呼び出してマウス位置を確認し、それに応じて反転/非反転画像をバックサーフェイスに転送してやります。これによって、マウスがメニューをポイントしているときは反転画像が、またメニューから外れれば通常メニュー画像が現われることになります。マウスが領域内をポイントしているときに左ボタンが押下げられていれば「選択」操作が行われたということですから、該当するテキストブロックにテキストポインタを飛ばしてフラグ解除、さらに作業用サーフェイスを消去して終了します。


//------------------------------
// マウス位置チェック
//------------------------------

bool _Check_mouse_in_Rect( RECT rect )
//マウスカーソルが矩形領域 RECT 内にあれば true を返し
//そうでなければ false を返す下受け関数
{
if( Mouse_stat.xpos>rect.left &&
Mouse_stat.xpos<rect.right &&
Mouse_stat.ypos>rect.top &&
Mouse_stat.ypos<rect.bottom
) {
return true;
}
return false;
}

//-----------------------------
// タスク処理部
//-----------------------------

int Com_select3_task()
{

int L_button_menu = 0; //マウス左ボタンのチェック用

//マウスが各選択肢の矩形領域内に入っていないかチェック

//選択肢1
if( _Check_mouse_in_Rect(rect1 )==true ){
BitBlt( Back_DC,rect1.left,rect1.top,rect1.right-rect1.left,rect1.bottom-rect1.top,
Select2_DC,rect1.left,rect1.top,SRCCOPY); //反転画像
//メニュー領域内でマウス左ボタンが押されているか?
if( Mouse_stat.fwkeys == MK_LBUTTON )L_button_menu = 1; //メニュー番号

}else{
BitBlt( Back_DC,rect1.left,rect1.top,rect1.right-rect1.left,rect1.bottom-rect1.top,
Select1_DC,rect1.left,rect1.top,SRCCOPY); //通常画像

}

//選択肢2
if( _Check_mouse_in_Rect(rect2 )==true ){
BitBlt( Back_DC,rect2.left,rect2.top,rect2.right-rect2.left,rect2.bottom-rect2.top,
Select2_DC,rect2.left,rect2.top,SRCCOPY); //反転画像
//メニュー領域内でマウス左ボタンが押されているか?
if( Mouse_stat.fwkeys == MK_LBUTTON )L_button_menu = 2; //メニュー番号
}else{
BitBlt( Back_DC,rect2.left,rect2.top,rect2.right-rect2.left,rect2.bottom-rect2.top,
Select1_DC,rect2.left,rect2.top,SRCCOPY); //通常画像
}

//選択肢3
if( _Check_mouse_in_Rect(rect3 )==true ){
BitBlt( Back_DC,rect3.left,rect3.top,rect3.right-rect3.left,rect3.bottom-rect3.top,
Select2_DC,rect3.left,rect3.top,SRCCOPY); //反転画像

//メニュー領域内でマウス左ボタンが押されているか?
if( Mouse_stat.fwkeys == MK_LBUTTON )L_button_menu = 3; //メニュー番号
}else{
BitBlt( Back_DC,rect3.left,rect3.top,rect3.right-rect3.left,rect3.bottom-rect3.top,
Select1_DC,rect3.left,rect3.top,SRCCOPY); //通常画像
}

//ボタン内容に応じて飛び先を変える
switch( L_button_menu ){
case 0: //押されていない
break;
case 1: //選択肢1番
next_slush(); //次の「/」の直後に飛ぶ
Mode_stat.flag_text = ON; //フラグを元に戻す(処理終了)
Mode_stat.flag_select3 = OFF;
Delete_select_Surface1(); //作業用サーフェイス消去
Delete_select_Surface2(); //作業用サーフェイス消去
break;
case 2: //選択肢2番
next_slush(); //2つ目の「/」の直後に飛ぶ
next_slush();
Mode_stat.flag_text = ON; //フラグを元に戻す(処理終了)
Mode_stat.flag_select3 = OFF;
Delete_select_Surface1(); //作業用サーフェイス消去
Delete_select_Surface2(); //作業用サーフェイス消去
break;
case 3: //選択肢3番
next_slush(); //3つ目の「/」の直後に飛ぶ
next_slush();
next_slush();
Mode_stat.flag_text = ON; //フラグを元に戻す(処理終了)
Mode_stat.flag_select3 = OFF;
Delete_select_Surface1(); //作業用サーフェイス消去
Delete_select_Surface2(); //作業用サーフェイス消去
break;
}

return 0;

}


テキストポインタを飛ばす際に、条件分岐の項で出てきた next_slush() が↑何度も呼ばれていますね。選択肢をもっと増やしたいときは4回、5回・・・とコールすればいくらでも分岐を増やせます。また選択肢文字列の取込み部分を少し工夫すると可変選択肢対応も可能になります。雛形はここに示しましたので、いろいろな変形Versionを作ってみて下さい。