■ 新・ゲーム開発講座




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


■第58夜:起動時メニュー

いよいよ本講座も今回で最終回です。ここでは、プログラム起動時に表示されるスタートメニューを作成します。内容的にはシステムメニューとほぼ同じなので、ここでもコピペ改造でお手軽に済ませてしまいましょう(…ってどこまでもコピペかーっ:爆 ^0^) とゆーことで、ソースはこちらです。

■起動時メニューとは

市販/同人を問わず、ゲーム起動時に「はじめから」「LOAD」「EXIT」などと表示される、いわゆるひとつの起動時メニューです。ゲームによってはBGMモードやCG鑑賞モードがついている場合もありますが、ここではそこまでの機能は追及しないことにします(→だって要するに簡易メディアプレーヤと画像ブラウザを作るってことですからねぇ +▽+)。

起動時メニューは、処理そのものは通常のシステムメニューと同じです。違うのは、キャンセルでメニューを抜けることが出来ないことと、「最初からスタート」があることでしょうか。またメニューの起動もマウスの右クリックなどではなくプログラム起動時に自動的に表示されるという性格のものになります。スクリプターの意図によっては、タイトルロゴや簡単なオープニング処理の後に表示したい場合もあるでしょうから、スクリプトの中に埋め込めるような仕様であることが望ましいでしょう。

そのような次第で、ここでは起動時メニューをスクリプトコマンドとして作成します。コマンドの書式は以下のようなものにしちゃいましょう(・ω・) メニュー表示項目はシステムメニューに倣って「LOAD]「EXIT」、そして「最初から」にします。

書式 #start_menu x,y

x,y:起動時メニューを表示する座標

…こんな単純なのでいいの? と抗議の声が聞こえてきそうですが、これで十分です。なぜならLOADを選択すればゲームはSAVE時の状態からそのまま継続という流れになるのでメニューの終了方法についてはほとんどノーケアですし、EXITは文字通りプログラムの終わりですからやはりノーケアで十分なのです。では「最初から」が選ばれたときはどうすれば良いかというと、なにもしないでテキスト処理を再開すればよいのです。なにもしないでテキスト再開…ということは #start_menu コマンドの直後から普通にテキスト解釈が始まるということで、コマンドの直後からそのままゲームの冒頭部分を続けて記述していけば面倒は要らないということになります。まるで何も考えていないような仕様ですが、変に凝ったものを作って使いにくくなるより100万倍くらいマシではないかと…(爆死 ^^;)
ちなみにBMPは以下のようなものを使用することにします。

■Mode_stat

通常のシステムメニューと動作は似ているのですが微妙に異なるところもあるため、ここでは別個のタスクとして実装しましょう。Mode_statには flag_start_menu という名前でフラグを追加します。起動時メニューのあらゆるタスクはこの1個のフラグでループを切り替えながら排他的に動作します。



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]; //汎用文字列変数
int flag_system_menu_effective; //システムメニュー有効=ON 無効=OFF
int flag_system_menu; //システムメニュー状態:OFF=処理中ではない/その他=各種メニューID
int flag_start_menu; //起動時メニュー状態:OFF=処理中ではない/その他=各種メニューID
int flag_fps; //FPS表示:ON=表示あり OFF=表示なし
int flag_shake; //画面揺れ効果: ON=揺れている OFF=処理中でない
int flag_fade_out; //フェードアウト処理 ON=処理中 OFF=処理中でない
int flag_fade_in; //フェードイン処理 ON=処理中 OFF=処理中でない
};
extern _Mode_stat Mode_stat;

■コマンド解釈部

いつもの要領でコマンド名と処理関数を追加します。


void Command_call()
{

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

//コメント文対応
if( *(TEXT+1)=='#' || *(TEXT-1)=='#' ){

//#が2回連続したらコメント文なので行末までテキストポインタを飛ばす

while( *TEXT!=0x0d ) TEXT++;
return;

}

TEXT++; //#を飛ばす

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

//コマンド名の評価 → 処理関数呼び出し
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;} //文字列変数に値をセット
if( strcmp(com_name,"text_input")==0 ){ Com_text_input(); flag=ON;} //ダイアログを開いてテキスト入力
if( strcmp(com_name,"save_point")==0 ){ Com_save_point(); flag=ON;} //セーブポイント設定
if( strcmp(com_name,"sp")==0 ) { Com_save_point(); flag=ON;} //セーブポイント設定(短縮表記)
if( strcmp(com_name,"save_point_mode")==0 ){Com_save_point_mode(); flag=ON;}//セーブポイントモード
if( strcmp(com_name,"disp_save_point_mode")==0 ){Com_disp_save_point_mode();flag=ON;} //セーブポイントDEBUG用
if( strcmp(com_name,"shake")==0 ) {Com_shake(); flag=ON;} //揺れる効果
if( strcmp(com_name,"fade_out")==0 ) {Com_fade_out(); flag=ON;} //フェードアウト
if( strcmp(com_name,"fade_in")==0 ) {Com_fade_in(); flag=ON;} //フェードイン
if( strcmp(com_name,"alpha_copy")==0 ) {Com_alpha_copy(); flag=ON;} //アルファブレンド
if( strcmp(com_name,"system_menu_flag")==0 ){Com_System_menu_flag(); flag=ON;} //システムメニューON/OFF
if( strcmp(com_name,"start_menu")==0 ) {Com_start_menu(); flag=ON;} //起動時メニュー

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

//フラグを見て、切り出されたコマンド名が有効なコマンドだったかどうか確認する
if( flag == OFF ){

char str[256];

sprintf( str,"警告:無効なコマンド [#%s] が記述されています",com_name);
MessageBox(NULL,str,"Command_call()",MB_OK);

}

}

■Mainloop

システムメニューと同じ要領で、各タスクを呼ぶ部分を追加します。ここではBGM再開のMainloopカラ回りタスクはシステムメニューの一部を流用しているので(まあソースを見ていただければ最後にフラグをすりかえてシステムメニューのタスクを回してポイ、としているのが分かると思いますが ^^;)除いてあります。


void Mainloop(void)
{

static DWORD fps_keep=0; //FPSを一定に保つためのタイマー変数
if( GetTickCount() < fps_keep + 1000/80 ) return;
fps_keep = GetTickCount(); //システム時刻を取得

//テキスト表示
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();

//揺れ効果
if( Mode_stat.flag_shake == ON ) Com_shake_task();

//フェード処理
if( Mode_stat.flag_fade_out == ON) Com_fade_out_task();
if( Mode_stat.flag_fade_in == ON) Com_fade_in_task();

//システムメニュー
if( Mode_stat.flag_system_menu_effective==ON ){

switch( Mode_stat.flag_system_menu ){
case SYSMENU_MAIN: //メインメニューのキースキャン
System_menu_task();
break;

case SYSMENU_EXIT: //EXITメニューのキースキャン
System_exit_task();
break;

case SYSMENU_FPS: //FPSメニューのキースキャン
System_fps_task();
break;

case SYSMENU_SAVE: //SAVEメニューのキースキャン
System_save_task();
break;
case SYSMENU_LOAD: //LOADメニューのキースキャン
System_load_task();
break;
}
}

//起動時メニュー
switch( Mode_stat.flag_start_menu ){
case STARTMENU_MAIN: //起動時メインメニューのキースキャン
Start_menu_task();
break;

case STARTMENU_LOAD: //起動時LOADメニューのキースキャン
Start_load_task();
break;

case STARTMENU_EXIT: //起動時EXITメニューのキースキャン

Start_exit_task();
break;
}

//FPS
if( Mode_stat.flag_fps == ON ) HLS_stc_FPS(Back_DC);

//Disp_Mouse_info(); //マウス情報(不要ならコメント化しちゃってください)

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

}

■コマンド起動部分(撃つ)

処理関数 Com_start_menu() では、パラメータ解釈をして窓を開く座標を取得したのち、座標をグローバル変数 _start_menu_pos_x、_start_menu_pos_y に格納して窓を開く関数 _start_menu() を読んでいます。このようにスクリプトのパラメータ解釈をする部分と実際の処理部分(タスクを「撃つ」部分など)を分離して記述すると、スクリプトからだけでなくプログラム上からもタスクの起動ができて便利です。スタートメニューは子ウィンドウ上でキャンセルが選択されてもメニューモードは抜けないで、また親メニューの表示に舞い戻らなくてはいけないので、そのへんを記述するためにこんな書き方にしてあります。さてそれで窓の開き方ですが、これまでのメニュー処理と要領は一緒なので、とくに難しいところはないと思います。


//=====================================================
// STARTメニュー(メイン=親窓)
//=====================================================

_Menu_Win StartMenu_main; //メインメニューの窓情報用

//名前が長いと面倒なので短縮表記を定義(^^;)
#define ST_HDC StartMenu_main.hDC
#define ST_HBITMAP StartMenu_main.hBitmap
#define ST_WIDTH StartMenu_main.width
#define ST_HIGHT StartMenu_main.hight
#define ST_WX StartMenu_main.wx
#define ST_WY StartMenu_main.wy
#define ST_ESX StartMenu_main.esx
#define ST_ESY StartMenu_main.esy
#define ST_TIMER StartMenu_main.timer
#define ST_IRECT1 StartMenu_main.item_rect[0]
#define ST_IRECT2 StartMenu_main.item_rect[1]
#define ST_IRECT3 StartMenu_main.item_rect[2]

int _start_menu_pos_x;
int _start_menu_pos_y;

int _start_menu() //システムメニュー窓を開く
{

//再入防止
if( Mode_stat.flag_start_menu == STARTMENU_MAIN ) return 0;

//前回のクリックから一定時間経過(ここでは200msec)するまではスキャンは開始しない
if( HLS_timer_check( StartMenu_main.timer,200)==false )return 0;

ST_WIDTH = 150; //窓の幅
ST_HIGHT = 100; //窓の高さ
ST_WX = _start_menu_pos_x; //窓の左肩の座標 X
ST_WY = _start_menu_pos_y; //窓の左肩の座標 Y
ST_ESX = 300; //BITMAP上の画面退避位置 X
ST_ESY = 0; //BITMAP上の画面退避位置 Y
StartMenu_main.item_max = 2; //選択アイテム数

ST_IRECT1 = MK_RECT(15,15,140,35); //窓BMP中のローカルな選択肢領域(最初から)
ST_IRECT2 = MK_RECT(15,45,140,60); //窓BMP中のローカルな選択肢領域(LOAD)
ST_IRECT3 = MK_RECT(15,70,140,90); //窓BMP中のローカルな選択肢領域(LOAD)

//BMP読み込み画面を生成する
HDC work_hdc; //作業用のDC
work_hdc=GetDC(hwnd); //主(表)画面のDCの内容を取得
ST_HDC =CreateCompatibleDC(work_hdc); //同じ設定でバック画面用のDCを生成
ST_HBITMAP =CreateCompatibleBitmap(work_hdc,450,100); //主(表)画面と同じ属性で画面生成
SelectObject(ST_HDC,ST_HBITMAP); //DCと画面本体を関連付ける
ReleaseDC(hwnd,work_hdc); //作業用DCを開放

//BITMAP読み込み
char f_name[256];
strcpy( f_name,G_PATH ); //グラフィックデータのパスに
strcat( f_name,"system\\menu_window_start.bmp" ); //BITMAPファイル名を追加して
Load_Bmp( ST_HDC, f_name ); //読み込み

//背景退避
BitBlt( ST_HDC,ST_ESX,ST_ESY,ST_WIDTH,ST_HIGHT, Back_DC,ST_WX,ST_WY,SRCCOPY );

//窓描画
BitBlt( Back_DC,ST_WX,ST_WY,ST_WIDTH,ST_HIGHT,ST_HDC,0,0,SRCCOPY);

//フラグ操作
Mode_stat.flag_start_menu = STARTMENU_MAIN; //起動時メインメニュー
Mode_stat.flag_cursor_blink = OFF; //カーソル表示は停止しておく
Mode_stat.flag_text = OFF; //テキスト表示は停止しておく

//タイマーを撃つ
HLS_timer_start( &ST_TIMER );

return 0;

}

int Com_start_menu() //システムメニュー窓を開く(スクリプト版)
{

//パラメータ解釈(メニューの表示座標)
TEXT++;
_start_menu_pos_x = kaiseki_10();
while( *TEXT==',' || *TEXT==' ' || *TEXT==0x0a )TEXT++;
_start_menu_pos_y = kaiseki_10();
while( *TEXT==',' || *TEXT==' ' || *TEXT==0x0a )TEXT++;

_start_menu();

return 0;

}


■回す

これも従来と要領は同じなのであまり解説する意味がありませんね(^^;) 注目してほしいのは「最初から」が選択されたところで、さきほどの _Start_menu_task_end() を呼んでそのまま終わって(=フラグを戻してテキスト処理を再開)います。つまり、#start_menu コマンドの直後から普通に従来どおりのテキスト解釈が再開するわけで、「最初から」に相当する特別な処理をする訳ではありません。この世界の神であるくににんは、とても安直なルールを適用した訳です(笑)


int Start_menu_task() //システムメニューのタスク部分
{

RECT wr; //使いまわし用RECT

//前回のクリックから一定時間経過(ここでは200msec)するまではスキャンは開始しない
if( HLS_timer_check( StartMenu_main.timer,200)==false )return 0;

//右クリックのスキャンはしない → 起動時メニューがキャンセルされるのはまずいため(^^)

//---------------------------------
//選択肢のスキャン
//---------------------------------
int L_button_menu = 0; //マウス左ボタンのチェック用
//選択肢1(最初から)
wr = ADD_RECT_OFFSET( ST_IRECT1,ST_WX,ST_WY ); //座標オフセット加算
if( _Check_mouse_in_Rect( wr )==true ){
BitBlt( Back_DC,wr.left,wr.top,wr.right-wr.left,wr.bottom-wr.top,
ST_HDC,ST_IRECT1.left+ST_WIDTH,ST_IRECT1.top,SRCCOPY);

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

}

//選択肢2(LOAD)
wr = ADD_RECT_OFFSET( ST_IRECT2,ST_WX,ST_WY ); //座標オフセット加算
if( _Check_mouse_in_Rect( wr )==true ){
BitBlt( Back_DC,wr.left,wr.top,wr.right-wr.left,wr.bottom-wr.top,
ST_HDC,ST_IRECT2.left+ST_WIDTH,ST_IRECT2.top,SRCCOPY);

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

//選択肢3(EXIT)
wr = ADD_RECT_OFFSET( ST_IRECT3,ST_WX,ST_WY ); //座標オフセット加算
if( _Check_mouse_in_Rect( wr )==true ){
BitBlt( Back_DC,wr.left,wr.top,wr.right-wr.left,wr.bottom-wr.top,
ST_HDC,ST_IRECT3.left+ST_WIDTH,ST_IRECT3.top,SRCCOPY);

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


}
//---------------------------
// 選択状態によって処理を分岐
//---------------------------

switch (L_button_menu ) {

case 1: //最初から
_Start_menu_task_end(); //メインメニューを終了
break;



case 2: //LOAD
Start_menu_task_end(); //メインメニューを終了
Start_load(); //起動時のロード処理へ
break;

case 3: //Exit
_Start_menu_task_end(); //メインメニューを終了
Start_exit(); //起動時のEXIT処理へ


}
return 0;

}


■締める

これも従来と要領は同じですね。もう、こぴぺぺぺぺっ・・・と(爆)


void _Start_menu_task_end() //タスクの終了処理部分を分離したもの
{

BitBlt(Back_DC,ST_WX,ST_WY,ST_WIDTH,ST_HIGHT,ST_HDC,ST_ESX,ST_ESY, SRCCOPY);
DeleteDC( ST_HDC ); //DCの消去

//フラグ操作
Mode_stat.flag_start_menu = OFF; //起動時のメインメニュー -> OFF
Mode_stat.flag_cursor_blink = OFF; //カーソル表示はOFF(先に処理を進める)
Mode_stat.flag_text = ON; //テキスト表示(解釈)を再開

//タイマーを撃つ(直後にふたたびメニュー窓を開くのを防止するため)
HLS_timer_start( &ST_TIMER );


}

■LOAD&EXIT

LOADEXITに関してもループフラグの名前が違うくらいでシステムメニューと内容は一緒だったりしますので、さすがにこれ以上の解説は省略させていただきます(だってリストを載せても毎度毎度コピペばかりなんですものっ…@▽@)。ここはひとつ、処理の流れはソースを読んで自力で処理を追いかけてみてください。ここまで読み進んでこられた根性のあるみなさんなら、ワンパターンな記述に始終している選択窓処理の流れは分かっていただけるだろうと思います♪

さて、それではいよいよソースをコンパイルして実行してみましょう。気の抜けたようなタイトル画面で申し訳ありませんが(笑)、ちゃんと起動時メニューは機能していますね。メニュー表示座標はスクリプト上から引数で自由に設定できますので、タイトル画面のデザインに応じて前後左右に寄せて表示することも可能です。いろいろ遊んでみてください。





起動時メニューはスクリプトコマンドですので、ちょっとしたドラマとか演出を行ったあとでカッコいいタイミングで表示することができます。今回のサンプルスクリプトでは真っ白な起動画面にソフトハウス名「Hobby Land Soft.」を表示ししてタイトル画面にフェードインするだけですが、スクリプターのセンス次第でこのへんの演出はいかようにも工夫できるのではないかと思います(・ω・)ノ

さて…ともかくこれでオーソドックスなノベルプレイヤーとしての基本要件は一通り満たすことができ、ました。ひとまず
完成です。・・・「完成」っ! ・・・ああ、なんと素晴らしき甘美な響きの言葉でしょう!(T▽T)

・・・とはいえ、どんなバグが出るか怖いところもありますので、いきなり Ver 1.00 とはしないで Ver 0.10 くらいで控えめにリリースすることに致しましょうか(汗 ^^;)
ちなみに、今回のソースをコンパイルして実行すると、なんと本講座のエンディング(…なんだよそれ:笑)が流れたりします。スケルトンから始まって最終回までの開発過程を追いかける単純なものですが、あらためて開発過程が走馬灯のように浮かんでちょっとばかり、じ〜ぃぃぃんときてしまいました。まあ、この種の感慨は開発者にのみ許される特権ですのでゆっくりと味わうことに……え?不具合報告ですかっ!?そーゆーことは、内密にメールでももももっ!(爆汗 ^^;)