■ 新・ゲーム開発講座




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


■第51夜:システムメニュー:LOAD

前回までで、SAVEはできるようになりました。今回はLOAD処理についてインプリメントしましょう。ソースはこちらです。

■LOADとは

言うまでもなくLOAD処理とは、SAVEしたときのプログラム的な状況を再現する操作です。基本的にはSAVEと逆の方法でデータを取り出し、値をメモリ内の変数に格納します。気をつけなければならないのはポインタ処理で、ポインタの値を直接保存することは無意味なのでオフセットで保存する…というのは前回までにお話したとおりです。まあ、SAVEについて一通りわかっていれば内容的には似たようなものです(・ω・)ノ。

■窓をひらく(撃つ)

メニュー窓を開く部分はSAVEとほとんど同じです(とゆーかそっくりコピーですし ^^;)。
SAVEファイルをチェックする get_file_info()、ファイル情報を格納する配列 Save_file_info[ ] はSAVEメニューから拝借しています。


もちろんBMPも同じサイズです。



//=====================================================
// システムメニュー(LOAD)
//=====================================================

_Menu_Win SysMenu_load; //LOADメニューの窓情報用

//名前が長いと面倒なので短縮表記を定義(SAVE窓用)
#define LD_HDC SysMenu_load.hDC
#define LD_HBITMAP SysMenu_load.hBitmap
#define LD_WIDTH SysMenu_load.width
#define LD_HIGHT SysMenu_load.hight
#define LD_WX SysMenu_load.wx
#define LD_WY SysMenu_load.wy
#define LD_ESX SysMenu_load.esx
#define LD_ESY SysMenu_load.esy
#define LD_TIMER SysMenu_load.timer
#define LD_IRECT(num) (SysMenu_load.item_rect[num])

int System_load() //LOADメニュー窓を開く
{

//再入防止
if( Mode_stat.flag_system_menu == SYSMENU_LOAD ) return 0;

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

LD_WIDTH = 300; //窓の幅
LD_HIGHT = 200; //窓の高さ
LD_WX = 170; //窓の左肩の座標 X
LD_WY = 120; //窓の左肩の座標 Y
LD_ESX = 600; //BITMAP上の画面退避位置 X
LD_ESY = 0 ; //BITMAP上の画面退避位置 Y
SysMenu_load.item_max = _SAVE_MAX; //選択アイテム数

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

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

//背景退避
BitBlt( LD_HDC,LD_ESX,LD_ESY,LD_WIDTH,LD_HIGHT, Back_DC,LD_WX,LD_WY,SRCCOPY );

//窓描画
BitBlt( Back_DC,LD_WX,LD_WY,LD_WIDTH,LD_HIGHT,LD_HDC,0,0,SRCCOPY);

//パス付きファイル名生成&ディスク上にSAVEファイルが存在するかチェック
int i,sx,sy,font_size;

for(i=0;i<_SAVE_MAX;i++) {
Save_file_info[i].flag = get_file_info( Save_file_info[i].f_name, Save_file_info[i].ptr , i );
sx = 20; //選択支座標X
sy = 30+20*i; //選択支座標Y
font_size = 16; //Font size
LD_IRECT(i) = MK_RECT( sx,sy,sx+270,sy+font_size); //選択支矩形領域
StrPut3D(LD_HDC, sx,sy,font_size,RGB(255,255,100),RGB(0,0,0),Save_file_info[i].ptr);
StrPut3D(LD_HDC,LD_WIDTH+sx,sy,font_size,RGB(255, 0, 0),RGB(0,0,0),Save_file_info[i].ptr);
}

//フラグ操作
Mode_stat.flag_system_menu = SYSMENU_LOAD; //LOADメニュー
Mode_stat.flag_cursor_blink = OFF; //カーソル表示は停止しておく
Mode_stat.flag_text = OFF; //テキスト表示は停止しておく

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

return 0;

}



■回す

マウス入力のスキャン部分は毎度毎度々々々々同じなので解説を省きます(^^;)。重要なのは、LOADすべきファイルが選択されたときの動作です。まずはさらりとソースを読んでみてください。



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

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

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

//右クリック(メニュー消去)されていれば背景を書き戻して処理を終了する
if( Mouse_stat.fwkeys == MK_RBUTTON ){
_System_load_task_end();
Mode_stat.flag_cursor_blink = ON;
Mode_stat.flag_system_menu = OFF;
return 0;
}

//---------------------------------
//選択肢のスキャン
//---------------------------------
int L_button_menu = -1; //マウス左ボタンのチェック用

for( i=0;i<_SAVE_MAX;i++ ) {

wr = ADD_RECT_OFFSET( LD_IRECT(i),LD_WX,LD_WY ); //座標オフセット加算

if( _Check_mouse_in_Rect( wr )==true ){
//マウスが重なっていたら反転表示
BitBlt( Back_DC,wr.left,wr.top,wr.right-wr.left,wr.bottom-wr.top,
LD_HDC,LD_IRECT(i).left+LD_WIDTH,LD_IRECT(i).top,SRCCOPY);
//メニュー領域内でマウス左ボタンが押されているか?
if( Mouse_stat.fwkeys == MK_LBUTTON )L_button_menu = i; //メニュー番号
} else {
//マウスが重なっていなければ普通に表示
BitBlt( Back_DC,wr.left,wr.top,wr.right-wr.left,wr.bottom - wr.top,
LD_HDC, LD_IRECT(i).left,LD_IRECT(i).top ,SRCCOPY);
}

}

//---------------------------
// LOAD処理
//---------------------------
char f_name[256];
char ptr[256];
char str[256];
if( L_button_menu>=0 && L_button_menu<_SAVE_MAX ) {

//選ばれた番号(=L_button_menu)に即したファイル名を取得
get_file_info( f_name, ptr , L_button_menu );

if( _check_file( f_name )==true ) {

//確認メッセージ
sprintf( str, "■LOADしますか?\n\n【%s】",ptr);
if( MessageBox( NULL,str,"System_load_task()",MB_YESNO)==IDNO )return 0;

//現在のBGMを停止
Com_stop_midi(); //MIDIが鳴っていたら停止
Com_stop_mp3(); //MP3が鳴っていたら停止
Com_stop_wav(); //WAVが鳴っていたら停止

_System_load_task_end();

//ファイル読み込み
HLS_bload( f_name,&Save_stat);

//環境復元
restore_save_info();

//BGM演奏開始は1EV後にする
Mode_stat.flag_system_menu = SYSMENU_LOAD_BGM; //BGM起動1発タスクへ以降
Mode_stat.flag_cursor_blink = OFF; //カーソル表示OFFのままにしておく

}else{
MessageBox( NULL,"セーブデータがありません","System_load_task()",MB_OK);
_System_load_task_end(); //窓を閉じる(終了フラグ処理)
}

}

return 0;

}

メッセージBOXを用いて確認メッセージ「LOADしますか?」を表示し、プレイヤーが「いいえ」=IDNO を選択したらふたたびスキャンループに戻り、そうでなければLOADの準備として現在鳴っているBGMを停止します。ここでは第33、34、37夜で作成した演奏停止関数を読んで済ませています。

次に、システムメニューのウィンドウを閉じる関数 _System_load_task_end() を呼んでいます。この段階でメニュー窓を閉じるのは、窓の背景の書き戻しを早い段階で済ませてしまうためです。窓BGMの背景退避領域には、システムメニューを開いたときのバックサーフェイスの一部が退避しています。窓を閉じるのがLOADデータの読み込み後になってしまうと、メニュー窓を閉じた後にLOAD前の画面の一部が書き戻されてみっともないことになります(どうせLOAD画面で上書きされるので書き戻さずに放っておくという漢の選択もありますが、気分的に正式に閉じておきたいです… ^^;)。

窓を閉じたら、いよいよSAVE構造体にファイルを読み込み、環境復元のための関数 restore_save_info() を呼びます(内容は後述します)。そして、BGM起動タスクにバトンタッチします。

このBGM起動タスクって何だ?と思われる方もいると思いますが、これはBGMの再生を安定させるための苦肉の策だったりしまして、あまりカッコイイものではありません…(大汗 ^^;) MCIでBGMを演奏する場合、演奏停止 → ファイル読み込み → 演奏開始 を一度に行おうとすると動作が不安定になる場合があるのです。そこで、演奏を再開する前に Mainloop() を一度カラ回りさせてやろうというのがここで行っている操作の意味です。具体的には Mode_stat.flag_system_menuSYSMENU_LOAD_BGM という値(SystemMenu_01.h で定義しています)をセットして、次にループが回ってきたらBGMタスクのほうに処理を振り向けます。BGMタスクでは演奏再開したのちフラグをOFFにしてLOAD処理の終了するという次第です。(とゆーかもっといい方法があったら教えて欲しいです、はい ^0^;)


■締める

LOADタスク終了部分を以下に示します。
上記 System_load_task() ではこの _System_load_task_end() は2箇所で呼ばれていますが、本領を発揮しているのはクリックされた選択支が「ファイルなし」となってキャンセルされている部分(リストの下のほう)で、データファイルを読む直前に呼ばれている部分は「窓を閉める」以上の役割は果たしていなかったりします(^^)。

なぜかと言うと、Mode_stat を操作してもその直後にディスクから呼んだイメージデータで Mode_stat 自体が上書きされてしまうからです(ここがLOAD処理の怖いところです → 慣れないとなかなか気づかない ^^;)。これの対策として、後から改めてフラグをセットしなおしてから System_load_task() は終了しているわけです。注意していないと将来のバグの元になりそうな部分ですね…(汗汗 ^^;)



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

BitBlt(Back_DC,LD_WX,LD_WY,LD_WIDTH,LD_HIGHT,LD_HDC,LD_ESX,LD_ESY, SRCCOPY);
DeleteDC( LD_HDC ); //DCの消去

//フラグ操作
Mode_stat.flag_cursor_blink = OFF; //カーソル表示OFFのままにしておく
Mode_stat.flag_system_menu = SYSMENU_LOAD_BGM; //BGM起動

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

}


■SAVE内容の復帰部分

いよいよ、肝心のSAVE内容の復帰部分です。とはいえ、別に難しいことをしている訳ではありません。まずはスクリプトファイルを読み込み、テキストポインタをリセットします。SAVE構造体に格納してあるのはテキストバッファ先頭からのオフセット量ですから、テキストポインタにはバッファの先頭アドレス+オフセットを設定してやります。
TEXT = TEXT_BUF + Save_stat.TEXT_offset; の部分がそれです。

その後、念のためにデフォルト背景画像を各サーフェイス (バックサーフェイス、背景画面、パーツ画面) に読み込んで初期化したのち、BMPファイル名のキャッシュ内容に従ってそれぞれのBMPを読み込みます(ここでは、ファイル名の長さが0の場合は何もしない=デフォルト背景のままとします)。

画面の復帰が終わったら、コンフィグ項目、SAVEポイントモードを復帰し、フラグ構造体 Mode_stat も復帰(単純なコピー)します。



void restore_save_info()
// Save_stat の内容を復元する(BGM以外)
{

char str[256];

//スクリプトファイル読み込み→実行位置復元
strcpy( Text_fname,Save_stat.script_fname );
HLS_bload( Text_fname,(char*)TEXT_BUF);
TEXT = TEXT_BUF + Save_stat.TEXT_offset;

//画面をいったんデフォルト背景にする(=以前の表示内容を消去)
strcpy( str,G_PATH);
strcat( str,"back00.bmp" );
Load_Bmp( Back_DC, str );

//それぞれの画面内容をキャッシュ内容に従って復帰
strcpy(BK_bmp_fname , Save_stat.BK_bmp_fname);
if( strlen(BK_bmp_fname)>0 ){
Load_Bmp( Back_DC, BK_bmp_fname );
}

strcpy(BG_bmp_fname , Save_stat.BG_bmp_fname);
if( strlen(BG_bmp_fname)>0 ){
Load_Bmp( BG_DC, BG_bmp_fname );
}

strcpy(PT_bmp_fname , Save_stat.PT_bmp_fname);
if( strlen(PT_bmp_fname)>0 ){
Load_Bmp( Parts_DC, PT_bmp_fname );
}

//コンフィグ項目を復帰
Font_Size = Save_stat.Font_Size;
TEXT_AREA = Save_stat.TEXT_AREA;
TEXT_COLOR1 = Save_stat.TEXT_COLOR1;
TEXT_COLOR2 = Save_stat.TEXT_COLOR2;
TEXT_WAIT = Save_stat.TEXT_WAIT;
TEXT_X = TEXT_AREA.left; //強制的にテキストエリアの左端へ
TEXT_X_PITCH= Save_stat.TEXT_X_PITCH;
TEXT_Y = TEXT_AREA.top; //強制的にテキストエリアの上端へ
TEXT_Y_PITCH= Save_stat.TEXT_Y_PITCH;

//セーブポイントモード
_save_point_mode = Save_stat.save_point_mode;

//Mode_stat(フラグ構造体)を復帰
memcpy( (void*)&Mode_stat,(void*)&Save_stat.Mode_stat_buf,sizeof(Mode_stat) );

}

こうしてみると実際のLOAD処理は恐ろしく簡単そうに見えますが、それは保存/復帰する内容を整理したからで、特に画面状況の復帰を大胆にばっさり切ってのけたことが大きいと思います。このあたりはコーディング以前の段階(仕様とか設計とか)で難易度が大きく左右されます。

難しくするつもりなら幾らでも難しく作れるでしょうが…意味のない複雑さはくににん的には避けたいところです。今回くらいの割り切り方が、ちょうど良いのかもしれません…(^^;)


■BGM再生部分

さきほど述べました「Mainloop1回分カラ回り」の処理です。main.cppMainloop() に、フラグに応じてBGMタスクを呼ぶ部分を追加します。それにしても、システムメニューのフラグを Mode_stat.flag_system_menu 1個に絞ったのは正解だったなぁ…(互いに排他処理になるので動作が重ならない)。



void Mainloop(void)
{

static DWORD fps_keep=0; //FPSを一定に保つためのタイマー変数
if( GetTickCount() < fps_keep + 1000/60 ) 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();

//システムメニュー
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;
case SYSMENU_LOAD_BGM://LOAD後のBGM演奏再開
System_load_begin_BGM();
break;
}

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

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

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

}


演奏再開部分は、以下のとおりです。SAVEデータをパッケージしたときに、演奏中でない場合はファイル名をNULL文字列にておいたので、それを手がかりに演奏する/しないを判定しています。念のために mciSendString("seek _MIDI_ to start",NULL,0,0); で強制的に先頭からの演奏を指定している以外は 第33、34、37夜 の内容を踏襲しています。そして、最後にループフラグを OFF にしてLOAD処理を終えています。


void System_load_begin_BGM() //セーブデータの内容に従ってBGMを再生する
{

char str[256];

//MIDIファイル名が空文字列でなければ演奏開始
if( strlen(Save_stat.MIDI_fname)>0 ){

//MIDIファイルをOPEN
sprintf( str, "open %s type sequencer alias _MIDI_", Save_stat.MIDI_fname );
mciSendString(str,NULL,0,NULL);

//OPENは成功したか?
MCIERROR err_code;
char err_mess[256];
err_code=mciSendString("capability _MIDI_ can play wait",str,255,NULL);
if( strcmp( str,"true" )!=0 ) {
// true以外が返ってきた→エラー文字列の取得
mciGetErrorString( err_code,(LPTSTR)err_mess,255);

sprintf( str, "MIDIファイル %s が開けません (T0T)/~~ \n\n■%s ",Save_stat.MIDI_fname,err_mess );
MessageBox( NULL,str,"restore_save_info()",MB_OK );
return;
}
//強制的に先頭にシーク
mciSendString("seek _MIDI_ to start",NULL,0,0);
//演奏終了通知を指定しながら演奏開始♪
mciSendString("play _MIDI_ notify",NULL,0,hwnd );

MIDI_stat.MIDI_play_stat = ON; //ステータス情報の更新
strcpy( MIDI_stat.MIDI_fname,Save_stat.MIDI_fname ); //ファイル名のキャッシュ
MIDI_stat.type = _MIDI; //再生ファイルのタイプを記録

}

//MP3ファイル名が空文字列でなければ演奏開始
if( strlen(Save_stat.MP3_fname)>0 ){
//MP3ファイルをOPEN
sprintf( str, "open %s type sequencer alias _MP3_", Save_stat.MP3_fname );
mciSendString(str,NULL,0,NULL);
//OPENは成功したか?
MCIERROR err_code;
char err_mess[256];
err_code=mciSendString("capability _MP3_ can play wait",str,255,NULL);
if( strcmp( str,"true" )!=0 ) {
// true以外が返ってきた→エラー文字列の取得
mciGetErrorString( err_code,(LPTSTR)err_mess,255);

sprintf( str, "MP3ファイル %s が開けません (T0T)/~~ \n\n■%s ",Save_stat.MP3_fname,err_mess );
MessageBox( NULL,str,"restore_save_info()",MB_OK );
return;
}
//強制的に先頭にシーク
mciSendString("seek _MP3_ to start",NULL,0,0);
//演奏終了通知を指定しながら演奏開始♪
mciSendString("play _MP3_ notify",NULL,0,hwnd );

MIDI_stat.MIDI_play_stat = ON; //ステータス情報の更新
strcpy( MIDI_stat.MIDI_fname,Save_stat.MP3_fname ); //ファイル名のキャッシュ
MIDI_stat.type = _MP3; //再生ファイルのタイプを記録
}

//WAVファイル名が空文字列でなければ演奏開始
if( strlen(Save_stat.WAV_fname)>0 ) {
WAV_stat.WAV_play_stat = ON; //演奏ON
WAV_stat.WAV_play_flag = Save_stat.WAV_loop_flag; //flag内容を書き戻し
strcpy(WAV_stat.WAV_fname,Save_stat.WAV_fname);

PlaySound( WAV_stat.WAV_fname, NULL, WAV_stat.WAV_play_flag );
}

Mode_stat.flag_system_menu = OFF; //一発で終わりなのでループフラグをクリアして戻る


}


さて、それでは実際にコンパイルして実行してみましょう。サンプルスクリプトを書いてみて思ったのですが、SAVEポイントは AUTO 設定で改ページ毎に自動処理にしておくのが一番無難でラクなようです。サンプルでは画面が切り替わりながらテキストが流れるループを繰り返しますので、いろいろなタイミングでSAVE/LOADして動作内容を確認してみてください。