iPad専用アクションゲーム「スライムは投げる」

最近のエントリー

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「game_01.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「game_01.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化すると思います。
そして、何かキーを押すと、そのキーコードが画面に表示されると思います。
というわけで今回は、キー入力について解説したいと思います。

ウィンドウメッセージ

前にDirectDraw講座でも触れましたが、Windows上ではアプリケーションとWindows(OS)がコミュニケーションをとるために、ウィンドウメッセージというのをやりくりします。

例えばキーが押されたらWindowsがアプリケーションに対して「キーが押されたぞ」というメッセージを送信します。
そしたらアプリ側はメッセージループ中、そのメッセージを受信し、ウィンドウプロシージャと呼ばれる関数を呼び出します(ウィンドウプロシージャは自分で制作する事になります)。

ではサンプルコードを見ていきます。

	
//----------[ ウィンドウプロシージャ ]----------------------------------------------------------
HRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam){
	
	switch(iMessage){

	略

	//キー押された
	case WM_KEYDOWN:
		//pauseキー(キーコード19)が押されたら終了
		if(wParam==19)
			Quit();

		ClearScreen(lpBack);
		DdTextOut(lpBack,0,0,"Pause(STOP)キーで終了",1);
		sprintf(tmp,"key=%d",wParam);
		DdTextOut(lpBack,0,16,tmp,1);
		Flip();

	return(TRUE);

メッセージの種類は「UINT iMessage」に格納されています。
ですから、switch文で分岐してやります。
キーが押された時の値は「WM_KEYDOWN」(マクロ)です。
そして、wParamに、押されたキーのキーコードが格納されています。
このキーコードについては、資料不足で(爆)どの値がどのキーかは判りません。
ですが、キーが押されたら、そのキーコードを画面に表示する事で、キーのキーコードが判るというわけです。

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「ddraw_13.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「ddraw_13.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化し、1ドットの荒いタイル(謎)がスクロールしていると思います。

というわけで、今回は低解像度(MODE X)モードを使ってみたいと思います。

ModeX

DirectDrawでは320×240、320×200と言った低解像度のディスプレイモードをサポートしています。
こういった特殊(?)なディスプレイモードを使用する場合には、ModeXとよばれるものを使用しなければなりません。
「ModeX」が一体何を意味するかは謎として、とにかく320×240モードを使用したい場合にはModeXの設定をすればいいのです(ォ。

使い道として、秒間60枚くらいは画面更新をしたいゲーム(ハデなアクションや格闘)、3D等で描画時間を少しでも減らせるように低解像度にするといった事が考えられます。
ただ、私が試したところ、ModeXを使用すると、一部の環境で不都合が生じる事があります。
そんな時はコントロールパネルのDirectXのアイコンをダブルクリックし、DirectDrawの項目の中の「Use ModeX for 320 for X(?)」みたいな感じのチェックを外してみてください。
たぶん正常に動作すると思います。

では実際にプログラムするにはどうしたら良いでしょう。コードを見てみましょう。

BOOL StartDirectDraw(HWND hw){

	<略>

	//協調レベル設定
	if(lpDD->SetCooperativeLevel(hw,DDSCL_FULLSCREEN | DDSCL_EXCLUSIVE | DDSCL_ALLOWMODEX)!=DD_OK)
		return FALSE;

	//解像度設定
	if(lpDD->SetDisplayMode(320,240,8)!=DD_OK)
		return FALSE;	

自作関数「StartDirectDraw」内の協調レベル設定部分の引数に、「DDSCL_ALLOWMODEX」を追加してやります。
これで、DirectDrawがModeXを使用するように設定出来ました。
次に、解像度設定で、320×240×8(256)を指定し、この瞬間に画面が切り替わります。
とりあえず、DirectDrawの設定はこれで終わりです(^^;

ただ、ModeXにはいくつか制約があります。
まず、プライマリサーフェイスへデータを転送出来ないという点、次にプライマリサーフェイスのロック、最後にプライマリサーフェイスに対するGetDCの使用です。
要するにプライマリサーフェイスにアクセス出来ないと言う事です。
ですから、普段は複合プライマリサーフェイスを作成してバックバッファからフリップしてフロントバッファに表示させるしか方法がありません。
これらを除けばバックバッファやオフスクリーンサーフェイスの扱いは今まで通り出来ます。

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、をお持ちの方はVisual C++でプロジェクトファイルを開いてください(「ddraw_12.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「ddraw_12.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化し、流れ星のようなものが表示されると思います。

というわけで、今回はサーフェイスへ直接アクセスする方法を書いていきたいと思います。

ダイレクトアクセス

まずサーフェイスへ直接アクセスするとはどういう事なのかを簡単に説明します。
サーフェイスは、ビデオメモリもしくはシステムメモリ内に作成されます。
メモリは1次元配列で管理されており、DirectDrawでは、サーフェイスが存在しているメモリの先頭アドレスを取得する事が出来ます。
ですから、このアドレスを使って、1次元配列と同じようにサーフェイス扱う事ができると言うわけです。
と言っても、使用するのは「点を描く」と言った処理程度でしょう。

では、実際にサーフェイスへの先頭アドレスを取得する方法を記述します。
サンプルコードを見てみましょう。

	unsigned char *p;
	DDSURFACEDESC desc;

	<略>
		
		//サーフェイスをロック
		ZeroMemory(&desc,sizeof(DDSURFACEDESC));
		desc.dwSize=sizeof(DDSURFACEDESC);
		lpBack->Lock(NULL,&desc,DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR,NULL);
		p=(unsigned char *)desc.lpSurface;

		//データを書き込む
		for(i=0;i<STAR;i++){
			p[x[i]+(int)y[i]*desc.lPitch]=sp[i];
			if((y[i]+=sp[i]/4.0f)>=480)
				y[i]-=480;	
		}

		//ロックを解除
		lpBack->Unlock(desc.lpSurface);

今回はサーフェイス作成時にも使用した構造体「DDSURFACEDESC」型を使用するので、最初にこれを初期化しておきます(ZeroMemory等)。
そうしたら、サーフェイスのメンバである「Lock」という関数を呼び出します。
サーフェイスをロックする事により、サーフェイスへの先頭アドレスが取得出来ます。

では、Lockの構文を書いておきます。

書式 HRESULT Lock( LPRECT lpDestRect, LPDDSURFACEDESC lpDDSurfaceDesc, DWORD dwFlags, HANDLE hEvent );
lpDestRect ロックしたい領域を識別するRECT構造体のアドレス。NULLであれば、全サーフェスがロックされる
lpDDSurfaceDesc サーフェスについての情報を格納する DDSURFACEDESC 構造体へのポインタ。
dwFlags
DDLOCK_SURFACEMEMORYPTR 指定した矩形の先頭への有効なメモリ ポインタを返さなければならないことを表すフラグ。矩形が指定されない場合、一番上のサーフェスへのポインタが返される。
DDLOCK_WAIT 何らかの原因によってロック(アドレス取得)が出来ない場合に、出来るまで待つという指定
hEvent 現在は使用していないのでNULLを指定しなければならないらしい
戻り値 成功した場合はDD_OKが返ってくるらしい。

こうすると、DDSURFACEDESC構造体のメンバであるlpSurfaceに、サーフェイスの先頭アドレスが格納されるので、そのアドレスをあらかじめ用意しておいたunsigned char型(256色なので)のポインタ変数pへ代入してやります。

では、このポインタの扱い方(サーフェイスへのアクセス)を説明します。
前にも記述しましたが、メモリは1次元配列で構成されているので、「座標X、Yへ5を代入」という場合にp[y][x]=5;とは書けません。
メモリは座標(0,0)から右へ順に行き、突き当たったらY座標が1ドット下へいき、X座標が0になる・・・を繰り返す形で存在しています(謎。
ですから、p[0]が(0,0)になります。
それで、本来ならばp[640]が座標(0,1)つまりp[x+y*640]というように考えるはずなのですがビデオカードによっては、自動的にサーフェイスの横幅を少し増やして、そこをキャッシュとして使用するものがあります。
そして、DDSURFACEDESC構造体のメンバであるlPitchには、このキャッシュの部分を含めた正確な横幅が格納されています。
ですから、サーフェイスメモリへアクセスするにはp[x+y*desc.lPitch]と表すのが正確となります。

一通り、アクセスし終えたら、ロックを解除してやる必要があります。

書式 HRESULT Unlock( LPVOID lpSurfaceData );
lpSurfaceData Lockによって取得され、アンロックすべきサーフェスのアドレス。このパラメータは、対応する Lock 呼び出しで lpDestRect パラメータに NULL を渡して全サーフェスをロックした場合に限り、NULL とする。

ちなみに、全体をロックしなかった場合は、desc.lpSurfaceを引き渡す事になります。

戻り値 成功した場合はDD_OKが返ってくるらしい。

これで、一通り終わりです。
実際にサーフェイスへアクセスするのは、ロックからアンロックする間だけにしてください。
ロックしてサーフェイスポインタを取得し、直ぐにアンロックしてしまった後に、サーフェイスへアクセスする事は一応出来ますが、
ビデオカードによっては上手くいかない場合があるので、止めた方がいいです(確認済み)。
それと、ロックという処理は、結構遅いので1ループに1度というのが理想です。
1ループに何度もロック、アンロックを繰り返すのは速度低下の原因となります。
それから、ロックしたあと、アンロックしないと、そのサーフェイスへの書き込み(GDIやBltFast等による転送)が不自由になるので注意してください。

ちなみに、ビデオメモリ内のサーフェイスへのアクセス(読み込み&書き込み)は非常に遅いので、一度システムメモリ内へコピーし、システムメモリ内をいじってから、ビデオメモリ内のサーフェイスへ転送する方がいいかもしれません。

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「ddraw_11.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「ddraw_11.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化し、SLIME(?)と書かれた画像がタイル状に敷き詰められ、どんどん小さくなりながら左上へスクロールするのが解ると思います。

というわけで今回は、DirectDraw側で画像転送時にクリッピングしたいと思います。

DirectDrawクリッパー

DirectDrawにクリッピングをさせるには、DirectDrawクリッパーというものを使用します。
クリッパー作成は、自作関数「StartDirectDraw」内に追加してあります。

//グローバル変数宣言
LPDIRECTDRAWCLIPPER lpClipper=NULL;	//クリッパーオブジェクト

略

	//クリッパーの設定
	if(lpDD->CreateClipper(0,&lpClipper,NULL)!=DD_OK)
		return FALSE;
	if(lpClipper->SetHWnd(0,hw)!=DD_OK)
		return FALSE;
	if(lpBack->SetClipper(lpClipper)!=DD_OK)
		return FALSE;

意外とソースは短いですかねぇ(^^;。
では、順番に説明していきたいと思います。
まず、DirectDrawオブジェクト(lpDD)のメンバ関数である「CreateClipper」を呼び出し、DirectDrawクリッパーオブジェクトを作成します。
構文は以下の通りです。

書式 HRESULT CreateClipper( DWORD dwFlags, LPDIRECTDRAWCLIPPER FAR *lplpDDClipper, IUnknown FAR *pUnkOuter );
dwFlags このフラグは使用していないらしく、0を指定する必要があるらしい
*lplpDDClipper 新しいDirectDrawClipperオブジェクトを示すポインタへのポインタ。
例によって、LPDIRECTDRAWCLIPPER型自体がポインタ型になっているので、「ポインタへのポインタ」と言っています(たぶん)。
*pUnkOuter 将来拡張のために使用するらしいので今はNULLを指定。
戻り値 成功した場合はDD_OKが返ってくるらしい。

これで、DirectDrawClipperオブジェクトが出来ました。
で次にこいつのメンバ関数である「SetHWnd」を呼び出します。
これは、クリッピング範囲をウィンドウハンドルを参照して設定するというものです。
つまり、プライマリサーフェイスと同じ範囲に設定されるわけです。

というわけで、構文。

書式 HRESULT SetHWnd( DWORD dwFlags, HWND hWnd );
dwFlags このフラグは使用していないらしく、0を指定する必要があるらしい
hWnd クリッピング情報を含むウィンドウ ハンドル
戻り値 成功した場合はDD_OKが返ってくるらしい。

これで、クリッピング範囲を設定し終えたら、次にサーフェイスのメンバ関数である「SetClipper」を呼び出します。
SetClipperによってサーフェイスへクリッパーを関連付けさせれば、そのサーフェイスに対し、クリッピングが可能となります。
一応構文も載せておきます。

書式 HRESULT SetClipper( LPDIRECTDRAWCLIPPER lpDDClipper );
lpDDClipper 関連づけさせるクリッパー。ここを0にしてやると、今まで関連付けられていたクリッパーが外されるらしい(未確認)。
hWnd クリッピング情報を含むウィンドウ ハンドル
戻り値 成功した場合はDD_OKが返ってくるらしい。

設定は以上です。
このクリッパーは、関連付けさせたサーフェイスへ、サーフェイスのメンバ関数「Blt」(自作関数の方では無いので注意してください)で転送した場合にのみ有効となります。
それと、クリッパーが関連づけさせられているサーフェイスへBltFast(自作関数Blt)による転送は出来ません。

今回は、クリッパーをバックバッファ(lpBack)に関連付けさせていますが、ここへBlt(自作関数BltStretch)による転送をしたときにだけ、クリッピングされます。
まぁ、このクリッパーによるクリッピングは私の環境では速度が遅いので使用していません。
ですから詳しいこたぁ、よくわかりましぇ~ん。

後始末

いつも通り、終了する時にはlpClipper->Release();を実行してやらなければなりません。
マクロ使用ではRELEASE(lpClipper);ですね。

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「ddraw_10.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「ddraw_10.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化し、SLIME(?)と書かれた画像がタイル状に敷き詰められ、左上へスクロールするのが解ると思います。

というわけで今回は、自分で画像転送時にクリッピングしたいと思います。

プログラム側でクリッピング

今回のサンプルの注目すべき点は「スクロール」では無く、画面の端っこのほうに表示している「完全でない画像」です(謎。
今まで、BltFast関数を使用した自作関数Bltでは、転送先で少しでも画像がはみ出てしまうと、何も表示されなくなってしまいます。
で、画面から少しでもはみ出してしまった場合、はみ出ていない部分を表示させる処理がクリッピングと言われています(たぶん)。

int cx1=0,cy1=0,cx2=640,cy2=480;	//自前クリッピング処理に使用する範囲

//----------[ クリッピング付きのデータ転送 ]----------------------------------------------------
void BltClip(LPDIRECTDRAWSURFACE saki,int sx,int sy,int width,int height,
		 LPDIRECTDRAWSURFACE moto,int mx,int my,char trans){
	DWORD key[2]={DDBLTFAST_NOCOLORKEY,DDBLTFAST_SRCCOLORKEY};
	if(sx<cx1){
		mx+=cx1-sx;
		width-=cx1-sx;
		sx=cx1;
		if(width<1)
			return;
	}
	if(sy<cy1){
		my+=cy1-sy;
		height-=cy1-sy;
		sy=cy1;
		if(height<1)
			return;
	}

	if(sx>=cx2 || sy>=cy2)
		return;
	if(sx+width>cx2)
		width=cx2-sx;	
	if(sy+height>cy2)
		height=cy2-sy;
	RECT rec={mx,my,mx+width,my+height};
	saki->BltFast(sx,sy,moto,&rec,key[trans] | DDBLTFAST_WAIT);
}
//----------[ クリッピングする範囲を変更 ]------------------------------------------------------
void SetClipArea(int x,int y,int width,int height){
	cx1=x;
	cy1=y;
	cx2=x+width;
	cy2=y+height;
}

この自作関数では、転送前に転送先の座標と縦幅、横幅からはみ出している部分を検出し、うまく表示出来るように値を変更しています。
まぁ、見りゃ解りますね(^^;
ついでなのでクリッピングの範囲を変更出来るようにもしておきました。この自作関数では、どこのサーフェイスでも適応されます。

使用例はこんな感じでしょうかね

//クリッピング範囲が全画面(0,0)~(639,479)の場合
SetClipArea(0,0,640,480);	//width,heightは0(ゼロ)も1ドットと考えるため639,479にはならない

//転送
BltClip(lpBack,-5,0,32,32,lpWork,0,0,0);
Flip();

今回は、プログラム側でクリッピングを行いました。
その理由として、DirectDraw側でクリッピングを行うと、家の環境で試したところ、速度がガタ落ちしたからです。
プログラムで処理した方がよっぽど早いですし、応用も利きます。
ただし、この方法にも欠点はあります。それは拡大縮小を行った時にクリッピング出来ないという点です。
というわけで、次回では一応DirectDrawでのクリッピング方法を説明します(私は使ってませんけど)。