■ 新・ゲーム開発講座




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


■第50夜:システムメニュー:SAVE

さてずいぶん時間がかかりましたが、ようやく長い旅路の果てにシステムメニューに戻って参りました(汗 ^^;) SAVEの基本要素は既に前回まででほぼ揃っていますので、ここではインターフェイス周りの整備を行ってシステムメニューの体裁を整えましょう。待望?のソースはこちらです。

まずは動作画面ですが、以下のようなものを作ります。SAVEスロットはとりあえず8ヶ所用意しました。増やす気になれば画面の許す限り増やせますが、まあこのくらいが妥当かな…と思いますのでフィーリングで決めています(^0^)。窓の中には前回までにSAVEした日付と時間を表示し、SAVEしていないスロットには日付/時間の表示はしません。ん〜それにしても、自力窓だとデザインも自由に出来てそれなりにオリジナルの雰囲気を出せるのでいいですねぇ(^0^)





■ファイル情報を得るには

上記サンプル画像のような表示をするためには、当然ファイル情報を取得しなければいけません。本ソース上では、ファイル情報の取得は次のような関数で行っています(SystemMenu_01.cpp)。引数としてSAVEファイルの番号(int num)を渡し、それに応じたファイル名 (char *f_name) と選択ウィンドウに表示すべき文字列 (char *ptr) を取得するというものです。


bool get_file_info( char *f_name, char *ptr , int num )
//SAVEファイルの情報を返す
// f_name:SAVEファイルのパス付き名を展開する領域
// ptr :情報を文字列で格納するための領域
// num :SAVEファイルの番号(1-10)
{

char buf[64];
HANDLE hFindFile;
WIN32_FIND_DATA file_info;
SYSTEMTIME system_time;

strcpy( f_name, SAVE_PATH ); //PATH
sprintf(buf,"data%02d.svd",num);
strcat( f_name, buf );

//ファイル情報を得る
hFindFile = FindFirstFile( f_name,&file_info );

if( hFindFile == INVALID_HANDLE_VALUE ) {

//ファイルが開けなかった場合
sprintf( ptr, "data%02d ----.--.-- --:--:-- ", num );

return false;

} else {

//世界標準時間からローカルタイムに変換
FileTimeToLocalFileTime(&file_info.ftLastWriteTime,&file_info.ftLastWriteTime);
//さらにシステムタイム(年月日表記)に変換
FileTimeToSystemTime(&file_info.ftLastWriteTime,&system_time );
//閉じる
FindClose( hFindFile );

sprintf( ptr, "data%02d %d.%02d.%02d %02d:%02d:%02d ",
num,
system_time.wYear,
system_time.wMonth,
system_time.wDay,
system_time.wHour,
system_time.wMinute,
system_time.wSecond
);

return true;

}

}

ここではSAVEファイル名は data**.svd という名前にして、フォルダ save の中に保存することにします。初期状態(ゲーム開始状態)では save フォルダの中身は空です。

さてまずファイルが存在するかどうかの確認ですが、Win32 APIFindFirstFile() でファイルを開けるかどうかで判定を行っています。ファイルが開けない場合、FindFirstFile() INVALID_HANDLE_VALUE を返しますので、ウィンドウに表示する日付と時刻部分は "----.--.-- --:--:--" という文字列にしてしまいます。

なぜ CreateFile() でなくて FindFirstFile() で開くかといいますと、FindFirstFile() ではファイルオープンと同時に WIN32_FIND_DATA型の引数のアドレスにそのファイルの情報を書き込んでくれるので日付の取得が容易だからです。上記ソースでは WIN32_FIND_DATA型 の file_info という変数を宣言し、そのアドレスを FindFirstFile() に渡すことで情報を取得しています。file_info.ftLastWriteTime が前回ファイルを書き込んだ時間になります。

ただしこの時間は、世界標準時間で取得されるので日本時間とはズレています。
そこで FileTimeToLocalFileTime() で日本のローカル時間に変換します。ただしファイルタイムは内部的には1601年1月1日からの経過時間を100ナノ秒単位でカウントした値で、このままでは使えません。そこでさらに FileTimeToSystemTime() でシステム時間に変換します。システム時間は SYSTEMTIME 型で取得しますが、この型は 年、月、日、時間、分、秒 のメンバをもっているので実用上はここまで変換しておけば十分でしょう。この時間を sprintf() でバッファ領域に書き出して、ウィンドウに表示する文字列としています。

関数の戻り値は、ファイルが存在すれば true、存在しなければ false とします。


■撃つ

下準備が整ったところで、いよいよメニュー窓を開きます。基本的な要領は41〜44夜と同じです。全部解説していると異様に長くなるので、不明な点は41夜〜を適宜参照願います。BMPは以下のようなものを用意することにしましょう。





ソースは以下のようになります。
これまでの窓表示とちょっと違うのは、選択支として表示するファイル情報を格納するために _Save_file_info という構造体を設け、SAVEスロットぶんだけ配列として用意しているところでしょうか。この構造体に、上記 get_file_info() で所得したファイル情報を格納しています。最後にフラグを立てて処理タスクを起動します。


#define _SAVE_MAX 8 //SAVE箇所の数(増やすときはBMP面積も拡張してね)

_Menu_Win SysMenu_save; //SAVEメニューの窓情報用

//選択窓に表示する情報用
struct _Save_file_info {
char f_name[512]; //ファイル名(パスつき)
char ptr[256]; //ファイル説明文字列格納用
bool flag; //ファイルの有無(TRUE/FALSE)
};
_Save_file_info Save_file_info[_SAVE_MAX];

_Save_stat Save_stat; //SAVEファイル構造体

//名前が長いと面倒なので短縮表記を定義(SAVE窓用)
#define SV_HDC SysMenu_save.hDC
#define SV_HBITMAP SysMenu_save.hBitmap
#define SV_WIDTH SysMenu_save.width
#define SV_HIGHT SysMenu_save.hight
#define SV_WX SysMenu_save.wx
#define SV_WY SysMenu_save.wy
#define SV_ESX SysMenu_save.esx
#define SV_ESY SysMenu_save.esy
#define SV_TIMER SysMenu_save.timer
#define SV_IRECT(num) (SysMenu_save.item_rect[num])

int System_save() //SAVEメニュー窓を開く
{

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

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

SV_WIDTH = 300; //窓の幅
SV_HIGHT = 200; //窓の高さ
SV_WX = 170; //窓の左肩の座標 X
SV_WY = 120; //窓の左肩の座標 Y
SV_ESX = 600; //BITMAP上の画面退避位置 X
SV_ESY = 0 ; //BITMAP上の画面退避位置 Y
SysMenu_save.item_max = _SAVE_MAX; //選択アイテム数

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

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

//背景退避
BitBlt( SV_HDC,SV_ESX,SV_ESY,SV_WIDTH,SV_HIGHT, Back_DC,SV_WX,SV_WY,SRCCOPY );

//窓描画
BitBlt( Back_DC,SV_WX,SV_WY,SV_WIDTH,SV_HIGHT,SV_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
SV_IRECT(i) = MK_RECT( sx,sy,sx+270,sy+font_size); //選択支矩形領域
StrPut3D(SV_HDC, sx,sy,font_size,RGB(255,255,100),RGB(0,0,0),Save_file_info[i].ptr);
StrPut3D(SV_HDC,SV_WIDTH+sx,sy,font_size,RGB(255, 0, 0),RGB(0,0,0),Save_file_info[i].ptr);
}

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

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

return 0;

}



■Mainloop

main.cppMainloop() にフラグに応じてシステムメニューのSAVE窓タスクを呼び出す部分を追加します。



//==================================================================
// メインループ
//==================================================================

//メッセージ(マウスやキーの入力など)が無いときに実行するループ
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;
}

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

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

}



■回す

タスク部分も基本は41夜〜と一緒です。さすがに選択支が8個ともなるとループで処理したほうがいいのでちょっとだけスマート?にしてみました(爆)

いずれかの選択アイテムがクリックされると変数 L_button_menu に選択されたアイテム番号が格納されます。有効な番号だったら、「SAVEしますか?」のメッセージボックスを出して、SAVE構造体をファイルに書き出します。ここでは念のために書き込み直後に再度 get_file_info()でファイル情報を取得して確認メッセージを表示しています(くどいと思ったら削除しちゃってください)。


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

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

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

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

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

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

wr = ADD_RECT_OFFSET( SV_IRECT(i),SV_WX,SV_WY ); //座標オフセット加算

if( _Check_mouse_in_Rect( wr )==true ){
//マウスが重なっていたら反転表示
BitBlt( Back_DC,wr.left,wr.top,wr.right-wr.left,wr.bottom-wr.top,
SV_HDC,SV_IRECT(i).left+SV_WIDTH,SV_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,
SV_HDC, SV_IRECT(i).left,SV_IRECT(i).top ,SRCCOPY);
}

}

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

//確認メッセージBOX
if( MessageBox(hwnd,"Saveしますか?","System_save_task()",MB_YESNO)==IDNO )return 0;

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

//DISKに保存
HLS_bsave( f_name,(char*)&Save_stat,sizeof(Save_stat));

//再度ファイル名を取得(書き込まれた日付と時間をファイルから取得して表示)
get_file_info( f_name, ptr , L_button_menu );

sprintf( str, "■SAVEしました\n\n【%s】",ptr);
MessageBox( hwnd,str,"System_save_task()",MB_OK);

_System_save_task_end(); //窓を閉じる(終了フラグ処理)

}

return 0;

}

■締める

処理の終了というよりは、窓を閉じてフラグを戻す部分だけ…というカンジになっている終了関数です。要領は今までと一緒なので、特に新味はありません。



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

BitBlt(Back_DC,SV_WX,SV_WY,SV_WIDTH,SV_HIGHT,SV_HDC,SV_ESX,SV_ESY, SRCCOPY);
DeleteDC( SV_HDC ); //DCの消去

//フラグ操作
Mode_stat.flag_system_menu = OFF; //システムのメインメニュー -> OFF
Mode_stat.flag_cursor_blink = ON; //カーソル表示再開

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

}

さて、では実行してみましょう。
初期状態ではSAVEフォルダの中身をからっぽなのでファイル情報は「----」ですが、SAVEを実行していくと順次ファイルスロットが埋まっていく様子が確認できると思います。確認のメッセージボックスまで自力窓にすればもう少しカッコイイかもしれませんが、まあくににん的にはとくに気にならないのでこのままにしておきましょう(^^;)。気になる方は確認窓の増設にトライしてみてください(最後のフラグ操作で Mode_stat.flag_system_menu = OFF ではなく SYSMENU_SAVE をセットするようにすれば、確認窓が閉じた直後から再度SAVEタスクが回りだします)。

次回は、いよいよLOADです。