■ 新・ゲーム開発講座




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


■第52夜:画面を揺らす

LOAD/SAVEが一通りできるようになったところで、いよいよ本講座も終わりが近づいてきました。ここからは、講座の最終章「起動時メニュー」の前に、追加的な演出効果についていくつか解説しようと思います。今回は、比較的軽い内容として「画面を揺らす演出」を取り上げます。ソースはこちらです。

■画面を揺らすには

画面を揺らすのはとても簡単です。バックサーフェイスから表画面に画像を転送する際、その座標を小刻みに変えてやるだけです(・ω・)ノ。最初は大きめに、そしてだんだん振幅を小さく…というタスク処理を行えば良い訳ですね。

単純なのでさくっと実装してみましょう。書式は、以下のような感じでよいかと思います(せっかくなので方向別に揺らしてやりましょう)

■書式 #shake dir


dir : 方向を X / Y / XY で指定

指定方向に画面を揺らす。


■Mode_stat

フラグ構造体 Mode_stat (main.h) にタスク制御用のフラグを追加します。フラグの値は揺れている=ON、処理中でない=OFF としましょう。ところで本稿のソースでは #define OFF 0 となっています。グローバル変数は自動的に初期値がゼロクリアされますので、 Mode_stat のメンバは明示的に初期化しなくても自動的に初期値=0、つまり OFF となります。


//プログラムの制御用FLAG構造体
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=処理中でない
};
extern _Mode_stat Mode_stat;


■コマンド解釈部

もう御馴染み?の TexitEngine.cppCommand_call() です。コマンド文字列と実行部の関数名を追加します。


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( flag == OFF ){

char str[256];

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

}

}


■Mainloop()

では main.cppMainloop() に細工をしましょう。この部分の動作については第13夜で解説しましたが、十分な速さのマシン上では Maonloop() およそ60fpsでシュッ、シュッ…と駆け抜けているはずです。駆け抜ける間に、フラグ構造体 Mode_stat のメンバを参照してそれぞれのタスク処理部分を呼んでゲームの処理を進めています。そして駆け抜ける最後の瞬間に BitBlt() でバックサーフェイス(裏画面)の内容をディスプレイに表示されている表画面に転送しています。

今回は、このバックサーフェイス → 表画面に転送する部分に小細工をします。まず、グローバル変数で G_OFFSET_X、G_OFFSET_Y というオフセット値を格納する変数をつくります。そして通常は (x,y)=(0,0)、つまり画面左肩を指定している BitBlt() の転送先座標をこの変数で置き換えてしまいます。これで、プログラムのどこかで G_OFFSET_X、G_OFFSET_Y を書き換えてやれば、その値ぶんだけズレた位置に画面が転送されるようになります。



//画面表示位置オフセット
int G_OFFSET_X; //バックサーフェイス→表画面に転送する際のオフセットX
int G_OFFSET_Y; //バックサーフェイス→表画面に転送する際のオフセットY

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();

//揺れ効果
if( Mode_stat.flag_shake == ON ) Com_shake_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);

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

}


■コマンド実行部(撃つ)

では、その G_OFFSET_X、G_OFFSET_Y を変更する部分を作りましょう(Text_Com_04.cpp)。まずはコマンドの起動部分です。グローバル変数でエフェクトのタイプを格納する shake_type (値は SHAKE_X,SHAKE_Y,SHAKE_XY)、タイマー変数 shake_timer、そしてカウンタの shake_counter を宣言します。

起動関数 Com_shake() では、 まずコマンドオプションの解析を行います。Kaiseki_TextStr() はテキストファイルから文字列を切り出す関数で TextEngine.cpp に記述してあるものを利用しています。この切り出した文字列から shake_type にそれぞれの値を設定します。

続いてフラグ設定を行います。まず揺れる効果が続いている間にテキスト表示が続いてしまうのを防ぐため Mode_stat.flag_text = OFF とします。続いて画面揺れ効果のフラグを立て、カウンタをクリアして最後に揺れの時間処理をするためにタイマーを撃ちます。

Mode_stat.flag_shake のフラグを立てたので、次回以降 Main_loop() からは揺れ効果のタスク処理部分がぐるぐると呼ばれるようになります。


int shake_type;
DWORD shake_timer;
int shake_counter;

#define SHAKE_X 0
#define SHAKE_Y 1
#define SHAKE_XY 2

int Com_shake()
{

char str[256],msg_str[256];

//パラメータ解析
TEXT++;
strcpy( str, Kaiseki_TextStr() );

if( strcmp( str,"X" )==0 ){
shake_type = SHAKE_X;
}else if( strcmp( str,"Y" )==0 ){
shake_type = SHAKE_Y;
}else if( strcmp( str,"XY" )==0 ){
shake_type = SHAKE_XY;
}else{
sprintf( msg_str,"シンボル [%s] は不正です。強制終了〜♪",str );
msg( msg_str,"Com_shake()");
Sys_exit();

}

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

//画面揺れ効果のフラグを立てる
Mode_stat.flag_shake = ON;

//揺れカウンタをゼロクリア
shake_counter = 0;

//タイマーショット
HLS_timer_start(&shake_timer);

return 0;

}


■回す/締める

ではコマンドのタスク部分=回す処理です。揺れ効果は3タイプ実装することになっていましたので、タイプ別に処理関数を分けて呼ぶようにしています。


void Com_shake_task()
{
switch( shake_type ){
case SHAKE_X: Com_shake_task_X(); break;
case SHAKE_Y: Com_shake_task_Y(); break;
case SHAKE_XY: Com_shake_task_XY(); break;
}
}

まずはX方向から参りましょうか。
↓リストを見ると、まず最初の部分でタイマー処理を行っています。これは前回撃ったタイマーのタイミングから規定時間経過したかどうかを調べています。ここでは前回から 50msec 経過したかどうかをしらべて、経過していなければそのまま return して次回の呼び出しを待ちます。タイマーチェックを抜けたら、次回の処理のためにすかさず次のタイマーを撃っておきます。

そして、次が肝心なのですが、カウンタの値を参照しながら G_OFFSET_X の値をプラス、マイナスに交互に書き換えています。ここが「揺れ」の大きさを表します。ここではだんだん値を小さくしながら16段階にわたってオフセットを変化させ、最後の17段階目でオフセットを0に戻してフラグのクリアをしています(いわゆる「締め」の処理)。揺れフラグを OFF にしたことで、もう次のコマンド発行がなされるまで Main_loop() からタスクが呼ばれることはなく、コマンドは終了したことになります。そしてテキストフラグを ON にすることで、スクリプト解釈が再開します。非常にシンプルですね(^^)


void Com_shake_task_X()
{

if(HLS_timer_check(shake_timer,50)==false)return; //タイマーチェック

HLS_timer_start(&shake_timer); //次回へむけてのタイマー撃ち

switch(shake_counter){
case 0: G_OFFSET_X = 10; break;
case 1: G_OFFSET_X = -10; break;
case 2: G_OFFSET_X = 10; break;
case 3: G_OFFSET_X = -10; break;
case 4: G_OFFSET_X = 7; break;
case 5: G_OFFSET_X = -7; break;
case 6: G_OFFSET_X = 7; break;
case 7: G_OFFSET_X = -7; break;
case 8: G_OFFSET_X = 5; break;
case 9: G_OFFSET_X = -5; break;
case 10: G_OFFSET_X = 5; break;
case 11: G_OFFSET_X = -5; break;
case 12: G_OFFSET_X = 2; break;
case 13: G_OFFSET_X = -2; break;
case 14: G_OFFSET_X = 2; break;
case 15: G_OFFSET_X = -2; break;
case 16: G_OFFSET_X = 0; //オフセットを0に戻して

Mode_stat.flag_shake = OFF; //ループフラグをクリア(締め)
Mode_stat.flag_text = ON; //テキスト表示再開
break;
}
shake_counter++;

}

続いてY方向ですが、これも操作するオフセットが G_OFFSET_Y になるだけで内容は一緒です。

void Com_shake_task_Y()
{

if(HLS_timer_check(shake_timer,50)==false)return; //タイマーチェック

HLS_timer_start(&shake_timer); //次回へむけてのタイマー撃ち

switch(shake_counter) {
case 0: G_OFFSET_Y = 10; break;
case 1: G_OFFSET_Y = -10; break;
case 2: G_OFFSET_Y = 10; break;
case 3: G_OFFSET_Y = -10; break;
case 4: G_OFFSET_Y = 7; break;
case 5: G_OFFSET_Y = -7; break;
case 6: G_OFFSET_Y = 7; break;
case 7: G_OFFSET_Y = -7; break;
case 8: G_OFFSET_Y = 5; break;
case 9: G_OFFSET_Y = -5; break;
case 10: G_OFFSET_Y = 5; break;
case 11: G_OFFSET_Y = -5; break;
case 12: G_OFFSET_Y = 3; break;
case 13: G_OFFSET_Y = -3; break;
case 14: G_OFFSET_Y = 2; break;
case 15: G_OFFSET_Y = -2; break;
case 16: G_OFFSET_Y = 0; //オフセットを0に戻して
Mode_stat.flag_shake = OFF; //ループフラグをクリア(締め)
Mode_stat.flag_text = ON; //テキスト表示再開
break;
}
shake_counter++;

}

続いてXYですが、これも G_OFFSET_X、G_OFFSET_Y を同時に書き換えているだけで内容はまったく一緒です。

void Com_shake_task_XY()
{

if(HLS_timer_check(shake_timer,50)==false)return; //タイマーチェック

HLS_timer_start(&shake_timer); //次回へむけてのタイマー撃ち

switch(shake_counter) {
case 0: G_OFFSET_X = 0;
G_OFFSET_Y = 10; break;
case 1: G_OFFSET_X = 0;
G_OFFSET_Y = -10; break;
case 2: G_OFFSET_X = 10;
G_OFFSET_Y = 0; break;
case 3: G_OFFSET_X = -10;
G_OFFSET_Y = 0; break;
case 4: G_OFFSET_X = 0;
G_OFFSET_Y = 7; break;
case 5: G_OFFSET_X = 0;
G_OFFSET_Y = -7; break;
case 6: G_OFFSET_X = 0;
G_OFFSET_Y = 7; break;
case 7: G_OFFSET_X = 0;
G_OFFSET_Y = -7; break;
case 8: G_OFFSET_X = 5;
G_OFFSET_Y = 0; break;
case 9: G_OFFSET_X = -5;
G_OFFSET_Y = 0; break;
case 10: G_OFFSET_X = 0;
G_OFFSET_Y = 3; break;
case 11: G_OFFSET_X = 0;
G_OFFSET_Y = -3; break;
case 12: G_OFFSET_X = 3;
G_OFFSET_Y = 0; break;
case 13: G_OFFSET_X = -3;
G_OFFSET_Y = 0; break;
case 14: G_OFFSET_X = 0;
G_OFFSET_Y = 2; break;
case 15: G_OFFSET_Y = 0; //オフセットを0に戻して
Mode_stat.flag_shake = OFF; //ループフラグをクリア(締め)
Mode_stat.flag_text = ON; //テキスト表示再開
break;
}
shake_counter++;

}

#shake コマンドは、単独で使用しても良いですが、直前に「ドカッ」「バキッ」等の効果音のWAVを流しておくと効果が倍増?すると思います。ソースに付属のサンプルスクリプトでぜひ動作確認してみてください(・ω・)ノ