■ 新・ゲーム開発講座




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


■第53夜:フェードイン/フェードアウト

今回は、場面転換などで使いたいフェードイン、フェードアウトをインプリメントします。とりあえずアルファブレンドは使わないドットマスク式でいきましょう。書式は以下のように致します。パラメータは…あってもいいですが、今回は定速で簡単にいきましょう(・ω・)ノ
なお、実質的な「改ページ処理」に相当するので、SAVEポイントフラグが AUTO になっている場合はSAVEポイント処理(まあ、1行だし…^^;)を行います。ソースは
こちらです。

【フェードアウト】

書式:#fade_out

画面を段階的に暗くする。



【フェードイン】

書式:#fade_in

Back画面にBG画面の内容をマスク処理しながら
順次転送する。


■ドットマスク式?

さてドットマスクとは何ぞや…と思う方がいるかもしれませんが、まずは実物を見て頂きましょう。↓これです。



拡大すると、↓こうなっています。


そう、白黒の点々で濃淡が表されていて灰色(中間色)がないのが、ドットマスクです。
第16夜でカーソル表示をさせたとき、論理演算のANDでマスクを抜いて、ORで合成を行いました。あれの応用です。

※画像を見て「おい、右側の緑の領域はなんだ?」とツッコミが入るかもしれませんが、ここはワーク領域なので区別がつきやすいように色を変えているだけです。重要なのは白黒の部分です(^^;)

白から始まって黒に到る順番で、表示されている画像に対しドットマスクANDすれば、マスクの黒い部分に相当するドットが抜けて黒になります。これを順次くりかえせばフェードアウトになります。

また、画像AにドットマスクをANDし、同じドットマスクを白黒反転して画像BとANDして、そののち画像Aに画像BをORすると、半透明もどきの合成画像になります。マスク濃淡を変えながらこれを行えば、フェードインになります。

さて、理屈がわかったところで実装してみましょう。

■Mode_stat

フェードイン/アウト用にフラグを追加します。値は ONOFF の2値で処理中/それ以外を表します。


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; //システムメニュー状態: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( flag == OFF ){

char str[256];

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

}

}

■フェードアウト

■撃つ

ではまず、フェードアウトのコマンド実行部です。なにはともあれマスク画像を読み込まないといけませんので、マスク用のDCとして Fade_DC、ビットマップのハンドルとして hFade_Bitmap を宣言します。ここにマスク画像を読み込みます。あとは、いついものごとくフラグを立ててタイマー撃ちです。


//------------------------------------------------------------
// フェードアウト
//
// 書式:#fade_out
//
// 画面を段階的に暗くする。
//
// ※実質的に「改ページ処理」の一種に相当するのでセーブ
// ポイント処理も行う。
//------------------------------------------------------------

HDC Fade_DC; //デバイスコンテキスト(フェード処理用)
HBITMAP hFade_Bitmap; //フェード画面本体のハンドル

DWORD fade_timer;
int fade_counter;

void _make_fade_surface()
//フェード処理用のワーク画面を生成する
{

HDC work_hdc; //作業用のDC

work_hdc=GetDC(hwnd); //主(表)画面のDCの内容を取得
Fade_DC =CreateCompatibleDC(work_hdc); //同じ設定でバック画面用のDCを生成
hFade_Bitmap =CreateCompatibleBitmap(work_hdc,640,480); //主(表)画面と同じ属性で画面生成
SelectObject(Fade_DC,hFade_Bitmap); //DCと画面本体を関連付ける
ReleaseDC(hwnd,work_hdc); //作業用DCを開放

}

int Com_fade_out()
{

TEXT++;

//作業用サーフェイス作成
_make_fade_surface();

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

//フラグ操作
Mode_stat.flag_fade_out = ON; //フェードアウトのフラグを立てる
Mode_stat.flag_cursor_blink = OFF; //カーソル表示は停止しておく
Mode_stat.flag_text = OFF; //テキスト表示は停止しておく

//カウンタ初期化
fade_counter = 0;

//タイマーを撃つ
HLS_timer_start( &fade_timer );
return 0;

}


■回す/締める

タイマーでタイミングを計りながら、順次段階的にマスクをバックサーフェイスにOR転送していきます。今回用意したマスク濃度は15段階ありますが、これはカウンタ fade_counter でタスクが呼ばれる毎にカウントアップします。またマスクは横32dot、縦480dotの長方形なので、ローカルカウンタ x でループさせながら順次画面全体にマスク処理します。

fade_counter がマスク濃度15段階分カウントし切ったらフェードアウト終了ですので、フラグを戻しておきます。そのとき、画面が真っ暗になって全面更新状態で終わるので、画面キャッシュを真っ黒画像(ここでは black.bmp)に書き換えておきます。また実質的な改ページに当たるのでSAVEポイント処理第49夜参照)も行っておきます。


int Com_fade_out_task()
{

int x;
int mask_x;

//タイマー処理
if( HLS_timer_check(fade_timer,50)==false )return 0;

HLS_timer_start( &fade_timer ); //次回処理にむけてタイマーを撃つ

mask_x = fade_counter*32; //マスク矩形のX座標

for(x=0;x<640;x+=32){

//Back_DC → Fade_DCのワーク領域(480,0)-(511,479)に、
//横32縦480の矩形領域をコピー
BitBlt( Fade_DC,480,0,32,480,Back_DC,x,0,SRCCOPY );

//マスクをANDする
BitBlt( Fade_DC,480,0,32,480,Fade_DC,mask_x,0,SRCAND );

//結果をBack_DCに書き戻す
BitBlt( Back_DC,x,0,32,480,Fade_DC,480,0,SRCCOPY );

}

fade_counter++;

if( fade_counter>15 ){

//マスク濃度のステップ分ループしたら終了処理を行う
Mode_stat.flag_fade_out = OFF; //フェードアウトのフラグをクリア
Mode_stat.flag_cursor_blink = OFF; //カーソル表示は消したまま
Mode_stat.flag_text = ON; //テキスト表示戻し

DeleteDC( Fade_DC ); //作業用DCの消去

//テキスト表示位置をリセット
TEXT_X = TEXT_AREA.left; //強制的にテキストエリアの左端へ
TEXT_Y = TEXT_AREA.top; //強制的にテキストエリアの上端へ

//フェードアウト処理後Back画面は真っ黒になるため、ダミーとして
//真っ黒なBMPファイル名をキャッシュに書き込んでおきます。
sprintf( BK_bmp_fname,"%sblack.bmp",G_PATH);
if( _save_point_mode == _AUTO_ )make_save_info(); //セーブポイント処理

}

return 0;

}


■フェードイン

■撃つ

フェードインも、同じ要領でインプリメントします。起動部分はほぼ同じです。


//------------------------------------------------------------
// フェードイン
//
// 書式:#fade_in
//
// Back画面にBG画面の内容をマスク処理しながら順次転送する。
//
// ※実質的な「改ページ処理」に相当するので、フラグが AUTO
// になっている場合はSaveポイント処理する。
//------------------------------------------------------------

int Com_fade_in()
{

TEXT++;

//作業用サーフェイス作成
_make_fade_surface();

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

//フラグ操作
Mode_stat.flag_fade_in = ON; //フェードアウトのフラグを立てる
Mode_stat.flag_cursor_blink = OFF; //カーソル表示は停止しておく
Mode_stat.flag_text = OFF; //テキスト表示は停止しておく

//カウンタ初期化
fade_counter = 0;

//タイマーを撃つ
HLS_timer_start( &fade_timer );
return 0;

}



■回す/締める

タスク部分の処理は、単純に黒くなればよかったフェードアウトと比べるとやや複雑です。とはいえ所詮は論理演算なのでそれほど恐れることはないでしょう。BitBlt() の論理演算指定は最後の引数で行います。内容は以下のようになります。

SRCCOPY
SRCAND
SRCERASE
SRCPAINT
そのままコピーする(通常の転送)
論理演算のAND
コピー先を反転してAND
論理演算のOR

この SRCERASE なんて指定があるあたり、マイクロソフトの担当者はドットマスク式の画像合成に使うことを想定したのかな…などと思いたくもなりますが、真相のほどははわかりません(・・・そういえば、初期のWindows用ゲームソフトは確かにドットマスク処理が多かったなぁ ^^;)。まあそれはともかく、便利なのでここではそのまま使わせていただきましょう。合成は、本章の最初の方で述べたとおりの手順です。その他 「締め」 の周辺はフェードアウトと同じです。


int Com_fade_in_task()
{

int x;
int mask_x;

//タイマー処理
if( HLS_timer_check(fade_timer,50)==false )return 0;

HLS_timer_start( &fade_timer ); //次回処理にむけてタイマーを撃つ

mask_x = fade_counter*32; //マスク矩形のX座標

for(x=0;x<640;x+=32){

//マスクをワーク領域にコピー
BitBlt( Fade_DC,480,0,32,480,Fade_DC,mask_x,0,SRCCOPY );

//マスクをBack_DCにAND(くりぬき)
BitBlt( Back_DC,x,0,32,480,Fade_DC,480,0,SRCAND );

//BG_DC → Fade_DCのワーク領域(480,0)-(511,479)に、
//横32縦480の矩形領域をコピー(コピー先=マスクを反転
//してAND)
BitBlt( Fade_DC,480,0,32,480,BG_DC,x,0,SRCERASE );

//結果をBack_DCにOR
BitBlt( Back_DC,x,0,32,480,Fade_DC,480,0,SRCPAINT );

}

fade_counter++;

if( fade_counter>14 ){

//マスク濃度のステップ分ループしたら終了処理を行う
Mode_stat.flag_fade_in = OFF; //フェードアウトのフラグをクリア
Mode_stat.flag_cursor_blink = OFF; //カーソル表示は消したまま
Mode_stat.flag_text = ON; //テキスト表示戻し

DeleteDC( Fade_DC ); //作業用DCの消去

//テキスト表示位置をリセット
TEXT_X = TEXT_AREA.left; //強制的にテキストエリアの左端へ
TEXT_Y = TEXT_AREA.top; //強制的にテキストエリアの上端へ

//BG画面とBack画面の内容が等しくなったのでキャッシュ内容をコピー
strcpy( BK_bmp_fname,BG_bmp_fname );
if( _save_point_mode == _AUTO_ )make_save_info(); //セーブポイント処理

}

return 0;

}

さて、ドットマスクと論理演算で、どのようなフェード処理になるでしょう。
ソースをコンパイルして実行すると、中間色合成ではないため多少のざらつき感はありますが、まずまず使えそうな印象です。アルファブレンドに比べると BitBlt() は圧倒的に高速なので、ドットマスク式の合成は激貧PCにも優しい処理といえるでしょう(ぉぃ






\(^0^)/