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

最近のエントリー

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

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「effec_07.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「effec_07.exe」をダブルクリックし、実行してみてください(何かキーを押すと終了します)。
どうでしょう?画面が切り替わり、Takabo Soft Networkのロゴが左から徐々に粉々になり落ちていくと思います。

というわけで、今回は徐々に画像を粉々にする方法を紹介します。

早速サンプルコードの解説

というわけで、今回は順番に説明していきまーす。

今回は、なんとなく構造体を使っています。

略

//破片データの作成
struct _dust{
	float x;		//x座標
	float y;		//y座標
	float x_add;	//x増加値
	float y_add;	//y増加値
	BYTE color;	//色番号
	int left;		//動作までの残りカウント
}*dust=NULL;

サンプルプログラムを実行すれば解ると思いますが、画像は左から段々と1ドットずつに分解され、それぞれが独立して動いています。
ですから、1ドットにつき、1データ(上の構造体の場合たぶん28バイト)が必要となります。
今回使用している画像サイズは612×145ですから、計612x145x28=2484720バイト必要となります。
このサイズを配列として「struct _dust dust[145][612]」などとすると、VCの場合、実行時にエラー(スタックオーバーとかその辺)が出てしまい、正常に動作しません。
ですから、「動的に確保」するようにします。

略

//----------[ 終了 ]----------------------------------------------------------------------------
void Quit(char *str=NULL){
	EndDirectDraw();
	if(str!=NULL)
		MessageBox(hw,str,"Message",MB_OK);
	if(dust)
		GlobalFree(dust);
	dust=NULL;
	exit(1);
}

んでもって、プログラム中に「_dust」構造体を動的に確保すると仮定した場合、
プログラムを終了する前にデータを解放しなければなりません。「GlobalFree」については
後でちょっと触れます(^^;

略

//----------[ メイン関数 ]----------------------------------------------------------------------
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){
    
	略
	
	//DirectDraw開始
	StartDirectDraw(hw);

	//パレット設定
	LoadPalette("takabo.bmp");
	
	//ビットマップを作業用サーフェイスへ読み込む
	LoadBitmap(lpWork,"takabo.bmp",0,0,612,145);//15,168

	BYTE *p;
	DWORD tim;

	//サーフェイスのロックで使用
	DDSURFACEDESC desc;
	ZeroMemory(&desc,sizeof(DDSURFACEDESC));
	desc.dwSize=sizeof(DDSURFACEDESC);

	//メモリ確保
	dust=(struct _dust *)GlobalAlloc(GMEM_FIXED,sizeof(struct _dust)*145*612);
	if(dust==NULL)
		Quit("メモリを確保出来ませんでした");

ここでメモリを動的に確保しています。

動的に確保する方法は「malloc」だとか「new」だとか色々ありますが、今回はなんとなく「GlobalAlloc」を使用します。
これはAPIだったかな?まぁ、この関数についての詳細はヘルプを参照してください。
とにかく「GlobalAlloc(GMEM_FIXED,データサイズ);」
としてやれば、データサイズ分のメモリを確保し、確保したメモリの先頭アドレスを返してくれます。
この関数で確保したメモリを解放するのが「GlobalFree(メモリの先頭アドレス);」です(Quit()関数内で使用)。

	int dust_max=0;

	//作業用サーフェイスロック
	lpWork->Lock(NULL,&desc,DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR,NULL);
	p=(BYTE *)desc.lpSurface;

この辺から、先程確保した構造体を初期化します。

まずここで、画像を読み込んである作業用サーフェイスをロックしておきます。

ロックについては「DirectDraw基礎 第12回」でやりました。

	int x,y;
	
	for(y=0;y<145;y++){
		for(x=0;x<612;x++){
			BYTE col=p[y*desc.lPitch+x];
			//黒(0番)以外を処理する
			if(col!=0){
				dust[dust_max].x=float(x+15);	//中央へ持ってくるため、ちょっと補正
				dust[dust_max].y=float(y+168);
				float angle=(rand()%36000)/100.f;
				//float speed=(rand()%3000)/100.f+2;
				float speed=10.f/(rand()%30);
				dust[dust_max].x_add= (float)cos(angle*3.141592/180)*speed;
				dust[dust_max].y_add=-(float)sin(angle*3.141592/180)*speed;
				dust[dust_max].color=col;
				dust[dust_max].left=x/6+rand()%10;
				dust_max++;
			}
		}
	}

上の方でも触れたように、画像の1ドット1ドット全てに対して1つのデータを作成します。
ただ、画像の黒い部分「色番号0」は表示しないので、データは作成しません。
あとはランダム値によって速度と角度を決め、そこから三角関数を使用してX、Yの増加値を決定します。

ちなみに「指定した角度の方向に進む」ような処理は「ゲーム制作基礎 第4回」でやりました。

	//ロックを解除
	lpWork->Unlock(NULL);

	int count=0,i;

	while(1){
		//疑似タイマー処理
		tim=timeGetTime();
		ClearScreen(lpBack);

		count++;

		//爆破前は普通に表示
		if(count<60*2){
			Blt(lpBack,15,168,612,145,lpWork,0,0,FALSE);
		}
		//爆破
		else {
			//サーフェイスをロック
			lpBack->Lock(NULL,&desc,DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR,NULL);
			p=(BYTE*)desc.lpSurface;
			
			//データを書き込む
			for(i=0;i<dust_max;i++){
				if(dust[i].left<=0){
					dust[i].x+=dust[i].x_add;
					dust[i].y+=dust[i].y_add;
					dust[i].y_add+=0.1f;
				} else
					dust[i].left--;
				
				x=int(dust[i].x);
				y=int(dust[i].y);
				if(x>=0 && x<640 && y>=0 && y<480)
					p[y*desc.lPitch+x]=dust[i].color;
			}

			//ロックを解除
			lpBack->Unlock(NULL);
		}
		
		Flip();		//フリッピング
		do{			//メッセージループ
			while(PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)){
				if(!GetMessage(&msg,NULL,0,0)) 
					Quit();
				TranslateMessage(&msg);
				DispatchMessage(&msg);
			}
		}while(timeGetTime()<tim+1000/60);	//60fps
	}
	
	return(FALSE);
}

データを作成してしまえば、あとは毎回各データのx,y座標にx,y増加量を足していき、画面に表示するだけです。
簡単ですね。

ただ、今回はビデオメモリ(lpBack)を直接操作しているので、ビデオカードによっては「やたら遅い!」
という事もあると思います。
そういった場合は、作業用バッファ(その2)としてオフスクリーンサーフェイスをシステムメモリ内に作成し、そこを直接操作してデータを書き込み、バックバッファへBltFastで転送、Flip・・・とすれば、多少は速くなると思いますのでやってみてください。

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

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「effec_06.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「effec_06.exe」をダブルクリックし、実行してみてください(何かキーを押すと終了します)。
どうでしょう?画面が切り替わり、Takabo Soft Networkのロゴが下から徐々に表示されると思います。

というわけで、今回は徐々に画像を表示する方法を説明していきます。

早速サンプルコードの解説

今回の主なコードはこれ。

//----------[ メイン関数 ]----------------------------------------------------------------------
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){
    
	略
	
	//DirectDraw開始
	StartDirectDraw(hw);

	//パレット設定
	LoadPalette("takabo.bmp");
	
	//ビットマップを作業用サーフェイスへ読み込む
	LoadBitmap(lpWork,"takabo.bmp",0,0,612,145);

	int count=1,i;
	DWORD tim;
	while(1){
		tim=timeGetTime();			//疑似タイマー処理

		//徐々に下から表示
		if(count<=145){
			ClearScreen(lpBack);	//バックバッファ全消去
			
			//既に表示済みのものを表示
			Blt(lpBack,15,168+145-count,612,count,lpWork,0,145-count,FALSE);
			
			//一番上に表示しているものを縦に連続させて表示
			for(i=0;i<168+145-count;i++)
				Blt(lpBack,15,i,612,1,lpWork,0,145-count,FALSE);

			Flip();					//画面更新
		}
		//普通に表示
		else {
			ClearScreen(lpBack);
			Blt(lpBack,15,168,612,145,lpWork,0,0,FALSE);
			Flip();
		}

		count++;

		if(count>300)
			count=1;
		
		do{							//メッセージループ
			while(PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)){
				if(!GetMessage(&msg,NULL,0,0)) 
					Quit();
				TranslateMessage(&msg);
				DispatchMessage(&msg);
			}
		}while(timeGetTime()<tim+33);	//1000/30=33.333(30FPS)	
	}
	
	return(FALSE);
}

今回のプログラムは簡単なので、すぐに解ると思います。

下から順番に表示しつつ、新しく表示される部分は上から降ってくるかのように表示する、というよくあるやつですね?

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

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

というわけで、今回はパレットを操作して画像のメイドを変更するぜっ!(壊

明度のお勉強

さて、明度を変更するというテクニックはよく見かけますよね?
例えばゲームで何か表示していて、次のシーン(場面)に移りたい場合に、画面を段々暗くし(フェード・アウト)、画面が真っ暗な間に次の画像を読み込んでおき(まだ表示はしない)準備が終わったら段々明るくしていき(フェード・イン)新しいシーンが始まる・・・というのがあります。
まず、こういったものは「パレットを操作」して明度を変更していると考えて良いでしょう。
というか、256色環境で明度を変更する方法はパレット操作以外まず無いです(近似色検索という解答は未定義)。

では、パレットをどうやって変化させたらいいか、についでですが、簡単です。
明るくしたければ各RGB値を増やしてやればいいし、暗くしたければRGB値を減らしてやればいいのです。
例えばRGB値が(100,25,30)だとして、1明るくしたいのなら各RGB値に1を足して(101,26,31)という感じ。
ただ、ここで注意してもらいたいのが、各RGB値は0~255でなければならないと言うことです。255を越えた場合は255、0より小さくなった場合は
0という風にです。あとはこの作業を256色パレット全てに行えばいい・・・はずですが、
パレット0番と255番は白・黒固定色なので、それ以外を全て変更すればいいのです。

今回はもうちょっと突っ込んでおきましょうかね。
例えば

	int bright=50;	//明度を変更する値(50は適当
	BYTE red=230;	//パレットの値(赤のみ) BYTE = unsigned char

	red+=bright;
	if(red>255)
		red=255;
	if(red<0)
		red=0;

というような事はしないようにしてくださいね。

今週のコードチェック(壊

今回の主なコードはコレ。

略

//----------[ パレットの明るさ変更 ]------------------------------------------------------------
//面倒なのでマクロでやってまう(良い子はマネしないでね)
#define SET_PAL(dest,src,bright) do{int v=src+bright;if(v>255)v=255;if(v<0)v=0;dest=v;}while(0)
void SetPaletteBright(int bright){
	PALETTEENTRY pal_tmp[256];

	for(int i=1;i<255;i++){	//0:黒固定 1:白固定
		SET_PAL(pal_tmp[i].peRed  ,peEntry[i].peRed  ,bright);
		SET_PAL(pal_tmp[i].peGreen,peEntry[i].peGreen,bright);
		SET_PAL(pal_tmp[i].peBlue ,peEntry[i].peBlue ,bright);
	}
	
	//垂直帰線待ち(ちらつき防止)
	VsyncWait();
	//更新
	lpPalette->SetEntries(0,1,254,&pal_tmp[1]);
}

パレットの設定については「DirectDraw基礎 第6回」でやりました。

また垂直帰線については「DirectDraw基礎 第14回」でやりました。

略

//----------[ メイン関数 ]----------------------------------------------------------------------
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){
    
	略
	
	//DirectDraw開始
	StartDirectDraw(hw);

	//パレット設定
	LoadPalette("bridge.bmp");
	
	//ビットマップをバックバッファへ読み込む
	LoadBitmap(lpBack,"bridge.bmp",0,0,320,240);

	//フリッピング(画面へ表示)
	Flip();

	DWORD tim;
	int bright=0,b_add=1;
	while(1){
		//疑似タイマー処理
		tim=timeGetTime();

		//明るさ変更
		bright+=b_add;
		if(bright6gt;=255 || bright<=-255)
			b_add=-b_add;
		SetPaletteBright(bright);

		do{
			//メッセージループ
			while(PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)){
				if(!GetMessage(&msg,NULL,0,0)) 
					Quit();
				TranslateMessage(&msg);
				DispatchMessage(&msg);
			}
		}while(timeGetTime()<tim+16);	//1000/60=16.666(60FPS)
	}
	
	return(FALSE);
}

見て解るとおり、一度画面に写真を表示したら、後はパレットを変更しているだけです。
明度を変更する関数でマクロを使ってしまったので、初心者にはちょっと解りにくいかもしれませんが、これを機、マクロの勉強をしてみてください。マクロを使うとホント楽なので・・・

あ、ちなみにビデオカードによっては、この「パレットを設定」する時間がやたらかかるものがあるので、「パレットを使えば高速に出来る!」とか思いこまないようにしてください。

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

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「ddraw_14.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「ddraw_14.exe」をダブルクリックし、実行してみてください。どうでしょう?画面が切り替わり、フルスクリーン化し、でかいスライムが2匹表示されていると思います(左のが垂直帰線待ち無し、右のが垂直帰線待ち有り)。

というわけで、今回は意図的に垂直帰線待ちをする方法を紹介します。

垂直帰線おさらい

以前フリップの所でも触れましたが、もう一度説明しておきます。
ディスプレイは小さな電子銃からでる電子ビームを走査して、ビデオメモリの内容を連続的に反映しています。
電子ビームは、左上から始まり、右へ進み、一番右まで行ったら一つ下へ下がり、一番左まで行き、また右へ進みます。
それらを繰り返し、最後に右下端まで行ったら、一気に左上端に戻ります。
この「右下端から左端上」に戻る間の事を「垂直帰線」とか言います。

で、フリップはこの垂直帰線中に行っているため、画面はちらつきません。
しかしフリップ以外でフロントバッファにBltFastか何かで転送したい場合、垂直帰線中では無いため、画面がちらつく場合があります。
そこで垂直帰線待ちをして、ちらつきを無くそうというわけです(謎

ただ、この垂直帰線中に処理しきれない場合はやっぱりちらつきますので(^^;

というわけでコードの解説

今回も順番に解説していきます。

まず、先に「長方形塗りつぶし」をする自作関数「Boxfill」を説明しましょうかね。

//----------[ 長方形(塗りつぶし)描画 ]--------------------------------------------------------
void Boxfill(LPDIRECTDRAWSURFACE sf,int x,int y,int width,int height,BYTE color){
	DDBLTFX ddbltfx;
	ZeroMemory(&ddbltfx,sizeof(DDBLTFX));
	ddbltfx.dwSize=sizeof(DDBLTFX);
	ddbltfx.dwFillColor=color;
	RECT rect={x,y,x+width,y+height};
	
	//クリッピング
	if(rect.left<cx1)
		rect.left=cx1;
	if(rect.right>cx2)
		rect.right=cx2;
	if(rect.top<cy1)
		rect.top=cy1;
	if(rect.bottom>cy2)
		rect.bottom=cy2;

    sf->Blt(&rect,NULL,NULL,DDBLT_COLORFILL | DDBLT_WAIT,&ddbltfx);
}

サーフェイスのメンバ関数「Blt」については以前「DirectDraw基礎 第9回」でやっているので、構文等はそちらを参照してください。
今回は、サーフェイスの一部を一色で塗りつぶしたいので、
Blt関数の最初の引数にRECT構造体のアドレスを引き渡してやります(RECT構造体の解説は「DirectDraw基礎 第8回」でやりました)。
・・・ってもう説明する事無いですね(汗

では次は、今回の本題「垂直帰線待ち」をする自作関数「VsyncWait」の説明~。

//----------[ 垂直帰線待ち ]--------------------------------------------------------------------
void VsyncWait(void){
	lpDD->WaitForVerticalBlank(DDWAITVB_BLOCKBEGIN,NULL);
}

見て解る通りDirectDrawオブジェクト(lpDD)がメンバ関数である「WaitForVerticalBlank」を呼び出します。

構文は以下の通りです。

書式 HRESULT WaitForVerticalBlank( DWORD dwFlags, HANDLE hEvent );
dwFlags 以下のどれかを指定します。

DDWAITVB_BLOCKBEGIN 垂直帰線が開始するまで待つ
DDWAITVB_BLOCKBEGINEVENT 垂直帰線が開始したらイベントを起動するらしいが、現在はサポートしてないらしい。
DDWAITVB_END 垂直帰線が終了するまで待つ

hEvent サポートされていないので、とりあえずNULL
戻り値 成功するとDD_OKが返ってくるらしい。

そんでもって、これらを使用した今回のメインコードはこれ。

//----------[ メイン関数 ]----------------------------------------------------------------------
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){
    
	略
	
	//DirectDraw開始
	StartDirectDraw(hw);

	//パレット設定
	LoadPalette("slime.bmp");
	
	//ビットマップを作業用サーフェイスへ読み込む
	LoadBitmap(lpWork,"slime.bmp",0,0,32,32);
	//拡大
	BltStretch(lpWork,0,0,320,480,lpWork,0,0,32,32,FALSE);

	int wid=320,hei=480;

	DWORD tim;
	while(1){
		//疑似タイマー処理
		tim=timeGetTime();

		//左側のスライム(垂直帰線待ち無し)
		Boxfill(lpFront,0,0,wid,hei,0);
		Blt(lpFront,0,0,wid,hei,lpWork,0,0,FALSE);

		//右側のスライム(垂直帰線待ち有り)
		VsyncWait();
		Boxfill(lpFront,320,0,wid,hei,0);
		Blt(lpFront,320,0,wid,hei,lpWork,0,0,FALSE);

		do{
			//メッセージループ
			while(PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)){
				if(!GetMessage(&msg,NULL,0,0)) 
					Quit();
				TranslateMessage(&msg);
				DispatchMessage(&msg);
			}
		}while(timeGetTime()<tim+50);	//今回は適当
	}

ただひたすら「同じ場所に書いたり消したり」するだけで、右のスライムは垂直帰線待ちをし、垂直帰線待ちがどういう働きをするかが一目で解るようなプログラムになっているのである。えっへん(゚o゜)\ばき☆

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

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「effec_04.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「effec_04.exe」をダブルクリックし、実行してみてください(何かキーを押すと終了します)。
どうでしょう?画面が切り替わり、写真が波紋のような歪み方をすると思います(謎。
というわけで、今回もサインカーブを使用して画像を歪ませたいと思います。

今回は解りづらいかも

さて、今まで演出講座2・3回にてサインカーブを使用して地球を揺らしてきましたが、今回は右の写真のように、写真を波紋のように歪めてみます。
今まで真面目に演出講座2・3回を勉強された方なら、おそらく写真を見ただけで「だいたいこうやっているのだろう」という見当は付いていると思います。

んー、ちょっと初心者にはきつい内容かもしれないので、こんなものもあるんだぁーぐらいに流してください。

というかゲーム関連で、波紋使うかどうかは謎です。

まずは数学のお勉強

さて今回はプログラム中に「直線距離の算出」をするので、まずここでざっと説明しておきます。
左の図のように直角三角形がある場合、

という公式があります(ピタゴラスの定理だったか?)。
高校1年くらいには習うと思いますが(詳しくは数学の先生に聞いてください)。

実際は、x座標,y座標があるので、点A(ax,ay)から点B(bx,by)の直線距離は
以下の図のようになります。

パソコン上の座標はY軸の方向が逆になるのですが、今回は2乗しているため符号が必ずプラスになるので
その辺を考える必要はありません。

これを実際にC言語で記述すると、

length = sqrt((ax-bx)*(ax-bx) + (ay-by)*(ay-by));

みたいな感じになりますね。

DirectDraw初期化

さて、サンプルコードを見ながら順番に説明していきます。
今回は基本的な部分はWinMain()内で全てやっていますが、DirectDrawの初期化の部分も少し変更していますので注意してください。
ちなみにDirectDraw関連の基本的なコードは「DirectDraw基礎 第13回」のサンプルコードを
使用しています(MODE Xを使用します)。

ではまず、グローバル変数としてlpGraphを宣言しておきます。
これは写真を読み込んでおく場所として使用します。

LPDIRECTDRAWSURFACE lpBack=NULL;	//オフスクリーンサーフェイス(バックバッファ)
LPDIRECTDRAWSURFACE lpWork=NULL;	//オフスクリーンサーフェイス(作業用バッファ)
LPDIRECTDRAWSURFACE lpGraph=NULL;	//オフスクリーンサーフェイス(作業用バッファ その2)

宣言しただけでは意味が無いので、StartDirectDraw自作関数内でオフスクリーンサーフェイスを作成します。

BOOL StartDirectDraw(HWND hw){

	略

	//作業用オフスクリーンサーフェイス作成
	ZeroMemory(&ddsd,sizeof(ddsd));
	ddsd.dwSize=sizeof(ddsd);
	ddsd.dwFlags=DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT;
	//強制的にシステムメモリ内に作成する
	ddsd.ddsCaps.dwCaps=DDSCAPS_OFFSCREENPLAIN | DDSCAPS_SYSTEMMEMORY;
	ddsd.dwWidth=320;
	ddsd.dwHeight=240;
	if((lpDD->CreateSurface(&ddsd,&lpWork,NULL))!=DD_OK)
		return FALSE;
	if((lpDD->CreateSurface(&ddsd,&lpGraph,NULL))!=DD_OK)
		return FALSE;

今回の場合、オフスクリーンサーフェイスへは、頻繁に直接アクセスするのでDDSCAPS_SYSTEMMEMORYを指定して強制的にシステムメモリ内へ作成します(ビデオメモリ内サーフェイスへの直接アクセスは速度が遅い)。

さて、DirectDrawの設定はこれぐらいです。
あ、ちなみに最後にlpGraphを解放するのを忘れないようにしてくださいね。

よく解らない解説

まず写真(bridge.bmp)をあらかじめ作成しておいたオフスクリーンサーフェイス(lpGraph)へ読み込んでおきます。

//----------[ メイン関数 ]----------------------------------------------------------------------
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){
    
	略
	
	//DirectDraw開始
	StartDirectDraw(hw);

	//パレット設定
	LoadPalette("bridge.bmp");
	
	//ビットマップを作業用サーフェイスへ読み込む
	LoadBitmap(lpGraph,"bridge.bmp",0,0,320,240);

次に、高速化のためにサイン・コサインテーブルを作成します。

	//サイン、コサインテーブル作成(256で一周とする)
	float fsin[256],fcos[256];
	for(int i=0;i<256;i++){
		fsin[i]=(float)sin(i*3.1415926535/128);
		fcos[i]=(float)cos(i*3.1415926535/128);
	}

普通は360度で一周ですが、今回は256度で一周とします。
こうすることで fsin[BYTE(…)] と、BYTE型(unsigned char型)にキャストするだけで不正なアクセスを防ぐことが出来ます。

次に、あらかじめ各座標毎に、中心からの距離・中心への角度(0-255度)を算出しておきます。
これも高速化のためです。

	float length[240][320];
	BYTE angle[240][320];

	int x,y;
	for(y=0;y<240;y++)
		for(x=0;x<320;x++){
			length[y][x]=(float)sqrt((160-x)*(160-x)+(120-y)*(120-y));
			angle[y][x]=BYTE(atan2(-double(120-y),double(160-x))/3.141592*128+256);
		}

距離の算出については、上の方でやりましたね?
また、角度の算出については「ゲーム制作基礎 第11回」でやりました。

	//サーフェイスのロックで使用
	DDSURFACEDESC desc_src,desc_dest;
	ZeroMemory(&desc_src,sizeof(DDSURFACEDESC));
	ZeroMemory(&desc_dest,sizeof(DDSURFACEDESC));
	desc_src.dwSize=desc_dest.dwSize=sizeof(DDSURFACEDESC);
	BYTE *p_src,*p_dest;	//ちなみに src=参照元  dest=転送先
	int sx,sy;
	int pitch_src,pitch_dest;
	
	int count=0;
	float width=0.f,width_max=20,width_add=0.15f;
	BYTE color;

	while(1){
		//疑似タイマー処理
		tim=timeGetTime();
		count++;				//カウンタ

		//ゆれの大きさを時間によって変化させる
		if(width_add>0){
			if((width+=width_add)>width_max)
				width_add=-width_add;
		} else {
			//0まで戻してしまうとちょっと歪むのでやめる
			if((width+=width_add)<=0.05f){
				width=0.05f;
				width_add=-width_add;
			}
		}

↑のは大体解ると思います。
ゆれの大きさの部分に関しては前回・前々回と大体同じです。

次に、あらかじめ画像を読み込んでおいたサーフェイス(lpGraph)と、作業用サーフェイス(lpWork)をロックし、そのサーフェイスへの先頭アドレスを取得します。

		//ロック
		lpGraph->Lock(NULL,&desc_src,DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR,NULL);
		p_src=(BYTE*)desc_src.lpSurface;
		pitch_src=desc_src.lPitch;
	
		lpWork->Lock(NULL,&desc_dest,DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR,NULL);
		p_dest=(BYTE*)desc_dest.lpSurface;
		pitch_dest=desc_dest.lPitch;

サーフェイスのロックについては「DirectDraw基礎 第12回」でやりました。

そんでもって次のが心臓部。

		BYTE *p_angle=&angle[0][0];
		float *p_length=&length[0][0];

		for(y=0;y<240;y++){
			for(x=0;x<320;x++){
				//参照元をずらす
				float len=fsin[BYTE((-count+*p_length)*15)]*width;
				sx=int(x+fcos[*p_angle]*len);
				sy=int(y-fsin[*p_angle]*len);
				color=0;
				if(sx>=0 && sx<320 && sy>=0 && sy<240)
					color=*(p_src+pitch_src*sy+sx);
				*(p_dest++)=color;
				p_angle++;
				p_length++;
			}
			p_dest+=pitch_dest-320;
		}

ポインタいじりまくりですね。
ポインタの概念がしっかり解っていないと理解出来ないと思います。

まず、今までの演出は「転送先の座標を計算する」というパターンですが今回はこれでは穴が空くので逆に「転送先が転送元の座標を計算する」方法でやっています。
ですから画面全体(320×240)について全て計算を行います。

おおまかに説明すると、「画面中心からの距離(p_length)+時間(count)」を使ってサインカーブを参照し、ゆれの幅を計算します。
その揺れの幅分だけ転送元の座標を中央に向かってずらします。あとは転送元の色を転送先へ転送します。

一通り終わったらロックを解除し、一度バックバッファへ転送し、フリップして画面に表示させます。

		//アンロック
		lpWork->Unlock(desc_dest.lpSurface);
		lpGraph->Unlock(desc_src.lpSurface);
		
		Blt(lpBack,0,0,320,240,lpWork,0,0,FALSE);

		Flip();		//フリッピング

		あとは省略(爆

解ってしまえば簡単ですね(^^;

細かい部分で何か解らない事があれば掲示板へどうぞ。