Dr. Juicyの 斜めブロック講座

これで世界はキミの物じゃ!!

どうも、Dr. Juicyです。ここではアクションゲームにおける斜め床の処理を、僕のツールの内部で使っている方法を元に解説します。といっても、我流の方法ですので、一般的なゲームやアプリケーションで行われている方法と、同様なのか異なるのか、分かりませんし、何も保障しません。


判定方法の種類

以下の議論では、操作するキャラクタは矩形であるとします。矩形のキャラクタを斜めブロックの上で歩行させる場合、斜め床での移動処理は、大きく分けて二種類に分類されます。下の図を見てください。

a)の処理では、キャラクタの矩形の底辺の中点が斜面に接する様に動きます。この場合は矩形の一部が地面にめり込んでいるようになります。 b)の処理では、キャラクタの矩形の頂点のうち、斜面に最も近い点が斜面に接して動きます。この場合は矩形が地面にめり込むことはありません。ただし実際にゲームで使用する場合は、グラフィックである程度の配慮をしなければ、この図のような場合には左足が地面からかなり浮いているように見えてしまいます。


矩形の底辺の中点での斜面処理

基本事項

斜面処理の方法として、キャラクタ矩形の底辺の中点が斜面に接する場合の処理方法を説明します。 まず、座標系は画面左上を原点として、画面右方向にx軸正方向、画面下方向にy軸正方向になっているものとします。 キャラクタ矩形は(x,y,w,h)で表し、x,yは矩形の左上の頂点の座標を、w,hは矩形の横幅と縦幅を示します。 キャラクタ矩形が占める領域は(x,y)-(x+w-1,y+h-1)となります。また、キャラクタの速度を1stepに置ける移動度と定義しておきましょう。速度は横方向縦方向にそれぞれvx,vyと表示することにします。

アクションゲームではキャラクタの移動処理を行う際に、横方向と縦方向どちらの移動から先に処理するか、という部分で流儀が分かれます。この違いは実際の操作性にも影響があります。先に縦移動処理を行い後から横移動処理をする場合には、ブロックの端ギリギリでのジャンプがしやすく、ブロック端ギリギリへの着地が失敗しやすくなります。逆に横移動処理を行ってから縦移動処理を行う場合には、ブロックの端ギリギリでのジャンプがしにくく、ブロック端ギリギリへの着地が成功しやすくなります。

横移動処理と縦移動処理の順序は、斜め床の処理ではさらに別の問題を引き起こします。斜め床のアクションゲームでは、たとえキャラクタの縦方向速度がゼロ(vy=0)だったとしても、キャラクタが斜面に立っている場合は縦方向の移動もさせなければなりません。この、「斜面における縦方向強制移動」が斜め床の処理の肝となる部分です。この処理の仕組みは、横移動処理と縦移動処理の順序に強く依存することになります。どちらを選ぶかは一長一短ですが、今回は先に横移動処理をし後から縦移動処理を行う順序を採用することにします。理由としては様々ありますが、逆の順序で処理した場合、先に縦方向移動で斜面による強制移動を行っても、続いての横移動処理でブロックに当たって前に進めない場合があります。しかしながら、すでに縦方向の強制移動を行ってしまっているためキャラクタが浮いたり沈んだりしてしまっており、再補正を試みなければならないためです。まあもちろん回避する方法もあるのです、個々の好みで決めていけばいいことだと思います。そもそも、こういう処理を自分で考えて動かすところにゲームプログラミングの面白さがあるのだと思います。

メインソースコード

さてさて、繰り返しますが、今回は先に横移動処理をしてから縦移動処理を行います。 実際に僕のツールにおいてキャラクタの移動処理のソースコードを元に解説していきます。 最初に移動処理の関数の全文を貼り付けます。後に個々に分解しながら解説します。

int Map2::Move2(RectV* rc, int mode, OneMap** pom){
	//稼動範囲内で移動させたときの移動後の座標をセット
	//戻り値:第 0 bit = 左の壁にぶつかる
	//戻り値:第 1 bit = 右の壁にぶつかる
	//戻り値:第 2 bit = 床がある
	//戻り値:第 3 bit = 天井がある
	//戻り値:第 4 bit = 階段の上に乗っている
	//戻り値:第 5 bit = 氷の上に乗っている
	//RectVには移動後の座標と実際に移動した(変更された)速度を返す


#define BITSHIFT	12		//8 + 4

	int res = 0;
	int returnvalue = 0;
	int i;
	unsigned char b;
	

	int org_x = rc->x;
	int org_y = rc->y;


	

//横方向移動の処理
	int maptop = ((rc->y) >> BITSHIFT);
	int mapbtm = ((rc->y + rc->h - 1) >> BITSHIFT);
	int mapright = ((rc->x + rc->w-1) >> BITSHIFT);
	int mapleft = ((rc->x) >> BITSHIFT);
	int mapcenter = ((rc->x + rc->w / 2) >> BITSHIFT);

	OneMap* om = *pom;
	if(om == NULL){
		om = FindOneMap(CalcPos(((rc->x + rc->w/2) >> BITSHIFT), ((rc->y + rc->h/2) >> BITSHIFT)));
	}


		//斜め足場用
		//左右の当たり判定をチェックする一番下のブロックの段
		//進行方向の(移動前の)足元が斜めorめり込みなら一段あげる
	int lr_chkbtm = mapbtm;		

		//(vx == 0)では左右あたり判定しない
	if(rc->vx > 0){//右向き移動================================
		
	//斜めブロックの昇降 y座標補正
		b = GetHit2(mapcenter, mapbtm, om);
		if (((b & 0x8) == 0x8) || (b == 1)){
			//中点が斜めブロック(右上がり右下がり両方)ブロックなら右下ブロック一つ分の当たり判定をしない
			//底辺中点が通常ブロックの場合はないとは思うけど、一応例外処理として用意
			lr_chkbtm--;			
		}
	
	//移動
		rc->x += rc->vx;
		int mapright2 = ((rc->x + rc->w-1) >> BITSHIFT);

	//右当たり判定
		//if (mapright != mapright2) {
			
			//ブロックをまたぐ移動。横方向当たり判定必要
			//歩き状態なので、y+hブロックだけは判定しない
			res = 0;
			for (i = maptop; i <= lr_chkbtm; i++){
				b = GetHit2(mapright2, i,om);
				if ( b==1 || (b == 0xC)){
					res = 1;
					rc->x = ((mapright2) << BITSHIFT) - rc->w;
					returnvalue = HITBLOCK_RIGHT;
					//TRACE(_T("r_hit: %d, 0x%x, 0x%x\n"),i, rc->y + rc->h, rc->vy);
					break;
				}
			}

		//}

	}else if (rc->vx < 0){//左向き移動================================
		
										
	//斜めブロックの昇降 y座標補正
		b = GetHit2(mapcenter, mapbtm, om);
		if (((b & 0x8) == 0x8) || (b == 1)){
			//中点が斜めブロック(右上がり右下がり両方)ブロックなら左下ブロック一つ分の当たり判定をしない
			//底辺中点が通常ブロックの場合はないとは思うけど、一応例外処理として用意
			lr_chkbtm--;			
		}

	//移動
		rc->x += rc->vx;
		int mapleft2 = (rc->x >> BITSHIFT);

	//左当たり判定
		//if (mapleft != mapleft2) {		
			//TRACE(L"change\n");
			
			//ブロックをまたぐ移動。横方向当たり判定必要
			//歩き状態なので、y+hブロックだけは判定しない
			res = 0;
			for (i = maptop; i <= lr_chkbtm; i++){
				b = GetHit2(mapleft2, i,om);
				if ( b==1 || (b == 0xB) ){
					res = 1;
					rc->x = ((mapleft2 + 1) << BITSHIFT);
					returnvalue = HITBLOCK_LEFT;
					//TRACE(_T("l_hit: 0x%x, %d, 0x%x\n"), rc->x, rc->x >> 8, rc->vy);
					break;
				}
			}

		//}
		
	}

	mapleft = ((rc->x) >> BITSHIFT);
	mapright = ((rc->x + rc->w - 1) >> BITSHIFT);
	mapcenter = ((rc->x + rc->w / 2) >> BITSHIFT);
	rc->y += rc->vy;

//下より先に上側の当たり判定
	//if (rc->vy < 0){	
	
		maptop = ((rc->y) >> BITSHIFT);
		
		for (i = mapleft; i <= mapright; i++){
			b = GetHit2(i, maptop,om);
			if(b == 1 || (b & 0x8)){
					//TRACE(L"Heading, 0x%x, 0x%x\n", maptop, rc->y);
				rc->y = ((maptop + 1) << BITSHIFT);
				returnvalue |= HITBLOCK_HEAD;
				break;
			}
		}
//	}


//=================================
//下側当たり判定
//
//落下&斜めブロック用の高さ処理
//床で最も高い(y座標の小さい)ものを取得
	
	//まず、移動前の(y + h - 1)をチェックする
	//そこにブロックがあれば一段上をチェック
	//ブロックが無ければ一段下をチェック
	//その後で、移動後の座標と比較して接地しているかをチェック
	

//足場の高さを計算====================================
	int miny;
	unsigned char rideBlock = 0;	//足場の梯子チェック用
	int LowCenter = ((rc->x + rc->w / 2) & 0xFFF);
	
	//まず、移動前の(y + h - 1)をチェックする。
	//ここのmapbtmは移動前のyから計算されている
	if (mode == MOVEMODE_GROUND){				//GROUNDなら梯子を足場にみなす
		miny = HeightLine2(mapleft, mapcenter, mapright, mapbtm, LowCenter, &rideBlock, 1, om);
	}else{										//FLIGHTなら梯子を足場にしない
		miny = HeightLine2(mapleft, mapcenter, mapright, mapbtm, LowCenter, &rideBlock, 0, om);
	}
	if(miny == 0x1000){
		//ブロックが無ければ一段下をチェック
		miny = HeightLine2(mapleft, mapcenter, mapright, mapbtm + 1, LowCenter, &rideBlock, 1, om);
		miny = ((mapbtm + 1) << BITSHIFT) + miny - rc->h;

	}else if(miny == 0x0){
				//ブロックがあれば一段上をチェック(斜めブロックではない)
			miny = HeightLine2(mapleft, mapcenter, mapright, mapbtm - 1, LowCenter, &rideBlock, 0, om);
			miny = ((mapbtm - 1) << BITSHIFT) + miny - rc->h;
	}else{
			miny = ((mapbtm) << BITSHIFT) + miny - rc->h;
	}
//====================================足場の高さを計算
	

//移動後の座標と足場の高さの比較=======================
	if (mode == MOVEMODE_GROUND){
#define MARGIN_GROUND	0x400				//斜めブロックの高さマージン,これ以下の差なら落下にならない
		if ( rc->y > miny - MARGIN_GROUND){	//4はマージン, ==は含まない
			rc->y = miny;
			returnvalue |= HITBLOCK_GROUND;

			if (rideBlock == 0x3){//中心が梯子(接地した場合のみ)
				returnvalue |= HITBLOCK_STANDLADDER;
			}
		}
	}else if (mode == MOVEMODE_LADDER){
		
		if (( rc->y >= miny) && (rideBlock != 0x3) ){//足場が梯子じゃないとき
			rc->y = miny;
			returnvalue |= HITBLOCK_GROUND;
		}

	}else{
		
		if ( rc->y >= miny){
			rc->y = miny;
			returnvalue |= HITBLOCK_GROUND;

			if (rideBlock == 0x3){//中心が梯子(接地した場合のみ)
				returnvalue |= HITBLOCK_STANDLADDER;
			}

		}
	}
//=======================移動後の座標と足場の高さの比較

	if (rideBlock == 0x4){
		returnvalue |= HITBLOCK_STANDICE;
	}



	rc->vx = rc->x - org_x;
	rc->vy = rc->y - org_y;

	om = FindOneMap(CalcPos(((rc->x + rc->w/2) >> BITSHIFT), ((rc->y + rc->h/2) >> BITSHIFT)));
	if(om){	*pom = om;}
	return returnvalue;
}

キャラクタの位置と速度の指定

まず最初に関数の引数を見ていきましょう。

int Map2::Move2(RectV* rc, int mode, OneMap** pom){

RectVなる構造体を渡しています。これはキャラクタ矩形(x,y,w,h)と速度(vx,vy)を要素として持ちます。

struct RectV{
	int x;
	int y;
	int w;
	int h;
	int vx;
	int vy;
};

第二引数modeではキャラクタが接地(立つ・歩行)状態なのか、飛行(ジャンプ・落下)状態なのか、 梯子に捕まっている状態なのかを指定します。 後々アクションごとに処理を振り分けるためです。 例えば、床が右下がりの時にキャラクタを右に動かす場合を考えましょう。 歩行で移動する時は、キャラクタは斜面に沿って降りていくべきですが、 ジャンプ中のときは、斜面とは関係なく移動すべきでしょう。 これらの処理の違いを共通のルーチンで実現できればカッコいいですが、 非常に複雑化してしまうため、処理速度も可読性も不利になるように感じました。 よって今回は第二引数modeによって一部の処理を振り分けることにします。 第三引数は、斜面の処理には関係ないので読み飛ばしてください。

ブロック座標への変換

ブロック座標などと書くと、小難しい感じがしますが、要はキャラクタが原点から何ブロック目に位置するのかを計算するだけです。 ブロックは左上を原点をとして、右方向にiブロック、下方向にjブロック進んだ場所を、(i,j)のブロック座標位置として指定されます。 次がキャラクタの位置からブロック座標を割り出す部分です

	int maptop = ((rc->y) >> BITSHIFT);
	int mapbtm = ((rc->y + rc->h - 1) >> BITSHIFT);
	int mapright = ((rc->x + rc->w-1) >> BITSHIFT);
	int mapleft = ((rc->x) >> BITSHIFT);
	int mapcenter = ((rc->x + rc->w / 2) >> BITSHIFT);

mapleft,maptop云々がキャラクタ矩形左上のブロック位置となります。 一風かわっているのは、割り算をせずに右シフトをしていることでしょうか。 ブロックの幅が16だった場合、例えば
mapleft = rc->x / 16;
としてもいいのですが、4bit右シフトした方が若干速い気がしてこうしています。 ソースコード中では実際にはBITSHIFTの値は12に設定していますが、 これは、僕のツールの中ではキャラクタの座標の下位8bitを固定小数点として扱っているため、 ピクセル単位に直すための8bit右シフト(もしくは256で割る)の分も加味して処理を行っています。

横移動処理(右方向の場合)

最初に横方向の移動処理を行います。 ソースコード中では右移動と左移動を速度vxでif文によって分けています。

	if(rc->vx > 0){//右向き移動================================

ここでは右向きの移動についてだけ解説します。 左向きは適宜読み替えてください。

横方向の移動処理では単純に次のことを行います。 キャラクタの高さhがnブロック分の高さに相当するとした場合、 キャラクタの横にあるn個のブロックを全て調べます。 全てのブロックが通過可能であれば(一つもブロックが無ければ)約束どおりに速度vxだけ座標を進めます。 一方、一つでも通過できないブロックがあれば、キャラクタの移動はブロックに接する所までで止まります。 実際には、このとき調べるブロックはブロックy座標でmaptopからmapbtmまでの(mapbtm-maptop+1)個となります。

ただし、斜め床のあるアクションゲームの場合には例外があります。最初に図を使ってしたように、キャラクタ矩形の底辺の中点を基準にする場合は、矩形の一部が床にめり込んだ形になります。このために、キャラクタが斜めブロックに立っている場合は、横方向の当たり判定において、一番下のブロックだけ判定しないようにしておきます。

ソースコードの中では、キャラクタ横の当たり判定を行うブロックはデフォルトでmaptopからmapbtmとし、例外の時だけmaptopからmapbtm-1を調べることにします。そのために下側のブロック座標を変数lr_chkbtmとしておき、

	int lr_chkbtm = mapbtm;		

斜めブロックに立っているときだけデクリメントして当たり判定を一段減らします。

	//斜めブロックの昇降 y座標補正
		b = GetHit2(mapcenter, mapbtm, om);
		if (((b & 0x8) == 0x8) || (b == 1)){
			//中点が斜めブロック(右上がり右下がり両方)ブロックなら右下ブロック一つ分の当たり判定をしない
			//底辺中点が通常ブロックの場合はないとは思うけど、一応例外処理として用意
			lr_chkbtm--;			
		}

ここで、関数GetHit2(a1,a2,a3)はブロック座標位置(a1,a2)にあるブロックの種類を返す関数です。 第三引数は僕のツール内でのマップ管理の関連で必要なだけで、一般には必要ありません。 ご自身のプログラムの参考にする場合には、気にせず自作関数でもメモリ直読みでもしてください。 ソースコードではGetHit2(mapcenter, mapbtm, om);によってキャラクタ矩形の底辺の中点の部分のブロックを取得しています。 次に、if文による判定条件ですが、
((b & 0x8) == 0x8)
はブロック種類bが斜めブロックであるということを意味します。 斜めブロックならtrue、とだけ認識してくれればかまいません。 またor条件で
(b == 1)
が付いていますが、ブロック種類が四角い不通過ブロック(いわゆる通常のブロック)であるという意味です。 これは例外中の例外として、万が一キャラクターが通常のブロックに埋まってしまっているための処理です。 その他の部分が正しく動作していれば、この後者の条件ははずしても良いはずです。が、念のため付けてあります。 とにかく、これで、進行方向が斜めブロックによってめり込んでいる場合は、横の当たり判定のブロック数を一段減らすことができました。

それでは、いよいよ横方向の当たり判定を行います。 当たり判定で、種類を調べるべきブロックは、「キャラクタ移動後の位置におけるブロック」です。 そのためにキャラクタを移動させ、そのブロック座標をmapright2に取得します。

	//移動
		rc->x += rc->vx;
		int mapright2 = ((rc->x + rc->w-1) >> BITSHIFT);

ここで気づくのは、そもそも、キャラクタの移動前の現在位置が、例外的な位置(例えば通過できないはずの壁にめり込んでいたり)でないならば、移動前と移動後のブロック座標が異なる場合にだけブロックの当たり判定をすればよい、ということです。 このソースコードでは、条件文をコメントアウトして毎回当たり判定をしていますが、 これを実現するためには、次の条件文のコメントアウトを解除してください。

		//if (mapright != mapright2) {

移動前と移動後のブロック座標が異なる場合にだけ当たり判定をするようにすると、 非常に処理回数が減り、圧倒的に高速になります。特に登場キャラクターの多い場合には有効です。 条件となる、移動前のキャラクタが例外的な位置にいないというものは、ちゃんとした当たり判定が行われていれば、 まず満足されるであろう条件です。 僕が現時点で毎回当たり判定をしている理由は、キャラクターのワープなどを今後サポートするかもしれないので、 その場合に現実的な速度が出るのかをテストするためです。 将来的にはコメントアウトを外すかもしれないですね。

実際の当たり判定は、ブロック座標で(mapright2,maptop)から(mapright2,lr_chkbtm)までのブロックの種類をチェックすることになります。lr_chkbtmの値は、通常はmapbtm、斜め床の上ではmapbtm-1となっているはずです。 ソースコードは次のようになっています。

			//ブロックをまたぐ移動。横方向当たり判定必要
			//歩き状態なので、y+hブロックだけは判定しない
			res = 0;
			for (i = maptop; i <= lr_chkbtm; i++){
				b = GetHit2(mapright2, i,om);
				if ( b==1 || (b == 0xC)){
					res = 1;
					rc->x = ((mapright2) << BITSHIFT) - rc->w;
					returnvalue = HITBLOCK_RIGHT;
					//TRACE(_T("r_hit: %d, 0x%x, 0x%x\n"),i, rc->y + rc->h, rc->vy);
					break;
				}
			}

ブロックの種類をGetHit2によって取得し、
b==1
の時、すなわち通常のブロックがに当たる場合、 または、
(b == 0xC)
の時、すなわち右下がりのブロックに左から当たる場合、 これらのどちらかの条件に該当する場合のみ、ブロックに衝突したとみなします。 右上がりのブロックに左から当たる場合は、衝突せず進入できるわけです。 これで、フラットな地面から登り坂に差し掛かる部分でも前進でき、後の縦方向当たり判定のときに登った分だけy方向に移動させます。 ただし、上記の方法では、キャラクタの矩形の上部だけが右上がりのブロックにが引っかかる場合でも、進入して前進できてしまいます。 僕の場合は「斜めブロックの真下は通常ブロックで埋める」という暗黙のルールをマップ作成時に課すことでこのような例外を防ぐことにしています。まあ、ブロックの種類の判定条件を書き換えれば、足元の右上がりブロックだけ進入可能ということも可能だと思います。 例えば、
if ( b==1 || ((b == 0xC) && (i == lr_chkbtm)){
とかですかね。
ブロックに衝突した場合はブロックに接する状態でキャラクタが止まるように、x座標を再設定しています。 この部分は、左方向への移動の時には、次のようになっていますね。

					rc->x = ((mapleft2 + 1) << BITSHIFT);

さて、これで横方向の移動処理はおしまいです。 右への移動の場合だけを解説しましたが、同様にして左への移動も作りましょう。

縦移動処理(上側の処理)

次は縦方向の移動の処理を解説します。 初めに、横方向移動によってキャラクタx座標は変わっているので、 移動後のキャラクタ位置からブロック座標を再計算しておきます。 またy方向の移動後の座標も最初に求めてしおきます。

	mapleft = ((rc->x) >> BITSHIFT);
	mapright = ((rc->x + rc->w - 1) >> BITSHIFT);
	mapcenter = ((rc->x + rc->w / 2) >> BITSHIFT);
	rc->y += rc->vy;

横方向の移動処理では速度の符号によって、左右どちらかの方向だけの当たり判定をすれば事足りました。 縦方向の場合も、一般にはその方法で上下どちらか一方の処理をすれば良いのですが、 斜め床のアクションゲームではそうも行きません。 例えば、縦方向速度vyが上向きの場合でも、上り坂による強制持ち上げの幅の方が、速度より大きい場合には、 下方向の当たり判定処理によってキャラクタ持ち上げてやる必要があるからです。

上方向の移動処理は比較的簡単で、次のようなソースコードで実現できます。 上でうだうだ書きましたが、上方向の移動処理に限っては速度が上向きの場合だけでもOKな気もしますので、その場合は最初のif文のコメントアウトを解除してください。

//下より先に上側の当たり判定
	//if (rc->vy < 0){	
	
		maptop = ((rc->y) >> BITSHIFT);
		
		for (i = mapleft; i <= mapright; i++){
			b = GetHit2(i, maptop,om);
			if(b == 1 || (b & 0x8)){
					//TRACE(L"Heading, 0x%x, 0x%x\n", maptop, rc->y);
				rc->y = ((maptop + 1) << BITSHIFT);
				returnvalue |= HITBLOCK_HEAD;
				break;
			}
		}
//	}

まず、y方向のブロック座標をmaptopに収めます。 この時の座標は移動後の座標になっているはずです。 ブロックの判定は(mapleft,maptop)から(mapright,maptop)までの範囲を調べることになります。 GetHit2でブロックの種類を取得し、ブロックに衝突するかどうかを調べます。 条件文
(b == 1 || (b & 0x8))
は、通常ブロックか斜めブロックに下から衝突した場合、という意味合いになります。 衝突した場合は、ブロックの底面に接するようにy座標を再設定します。 これで、上方向への移動処理は終わりです。 簡単でしたね。斜めブロックがさえ無ければ、みんなこんな処理で済むのですよね。

縦移動処理(下側の処理)

さあ、いよいよ斜め床の肝となる縦方向移動下側の処理です。 もう一度言っておきますが、斜め床の処理には色々な流儀があると思いますので、 あくまで一つの方法だと思ってください。 僕自身、何度も試行錯誤を繰り返しているので、これがベストアンサーとも思っていません。 処理速度よりも若干可読性を重視しています。

ここでは、処理の手順を大きく次の二段階に分けています。

斜め床による縦方向の強制移動(上り坂での持ち上げと下り坂での引き下げ)は、 後者の接触判定結果によって振り分けることになります。 またここでは梯子の上り下りの処理も同時に行います。 肝は、梯子の上に立っている状態から、下方向キー入力によって梯子に捕まる部分の処理です。 これによって処理が若干複雑化しますが、根気良く処理していきましょう。

周辺部の床の高さの算出

では、早速「周辺部の床の高さの算出」を行いましょう。 まずは、現時点での変数の確認です。 ここまでの処理で、mapbtmには移動前のブロック座標(キャラクタ底面)が、 rc->yには移動後のキャラクタ座標が収められています。 まず最初に、移動前のブロックy座標(mapbtm)における床の高さをmapleftからmaprightまでの範囲で算出し、minyに収めます。 この部分のソースコードは、次のようになります。

//足場の高さを計算====================================
	int miny;
	unsigned char rideBlock = 0;	//足場の梯子チェック用
	int LowCenter = ((rc->x + rc->w / 2) & 0xFFF);
	
	//まず、移動前の(y + h - 1)をチェックする。
	//ここのmapbtmは移動前のyから計算されている
	if (mode == MOVEMODE_GROUND){				//GROUNDなら梯子を足場にみなす
		miny = HeightLine2(mapleft, mapcenter, mapright, mapbtm, LowCenter, &rideBlock, 1, om);
	}else{										//FLIGHTなら梯子を足場にしない
		miny = HeightLine2(mapleft, mapcenter, mapright, mapbtm, LowCenter, &rideBlock, 0, om);
	}

ここでHeightLine2は第一~四引数で指定したブロック座標の範囲からブロックの高さを返す関数です。 第七引数に1を指定すると、「梯子の頂上」も乗れる足場としてみなされます。 0を指定すると、「梯子の頂上」は乗れないものとみなされます。 ここで「梯子の頂上」とは、梯子ブロックでありその上のブロックが空のブロックであるものを指します。 「梯子の頂上」にはいつでも乗れると思いがちですが、例えば、 ジャンプ落下中「梯子の頂上」に着地しようとしたけれどもギリギリで乗れずに落ちていく場合、 「梯子の頂上」は乗れないブロックとしておかないと、着地に失敗したはずが成功してしまうことになります。 といったような理由で、ここでは接地状態MOVEMODE_GROUND(立つ・歩行など)の時だけ「梯子の頂上」に乗れるように、 それ以外のときは乗れないように引数をセットします。 関数HeightLine2の詳細については、混乱を避けるため説明を後回しにします。

さて、この時点でminyには、ブロックy座標mapbtmの行のブロックの高さが収まっています。 この値によって、次のように処理を三通りに分岐します。

  1. mapbtmの行にブロックが無かった場合(miny==0x1000)、一つ下の行(mapbtm+1)のブロックの高さを再度算出します。
  2. mapbtmの行のブロックが通常ブロックと同じだった場合、すなわち斜めブロックに乗っていない場合(miny==0)、一つ上の行(mapbtm-1)のブロックの高さを再度算出します。
  3. mapbtmの行でmapcenter(キャラクタ矩形の底辺中点)が斜めブロックだった場合( (0 < miny) && (miny < 0x1000))、その高さを元に処理を進めます。

これらの処理は次のようなソースコードになります。

	if(miny == 0x1000){
		//ブロックが無ければ一段下をチェック
		miny = HeightLine2(mapleft, mapcenter, mapright, mapbtm + 1, LowCenter, &rideBlock, 1, om);
		miny = ((mapbtm + 1) << BITSHIFT) + miny - rc->h;

	}else if(miny == 0x0){
				//ブロックがあれば一段上をチェック(斜めブロックではない)
			miny = HeightLine2(mapleft, mapcenter, mapright, mapbtm - 1, LowCenter, &rideBlock, 0, om);
			miny = ((mapbtm - 1) << BITSHIFT) + miny - rc->h;
	}else{
			miny = ((mapbtm) << BITSHIFT) + miny - rc->h;
	}
//====================================足場の高さを計算

再度ブロック高さを算出した後は、minyに収まっている値を「ブロックの高さ」から「キャラクタがその床にちょうど接する場合のy座標」に変換しておきます。ブロック位置の分を加えてキャラクタの縦幅hを引くだけです。 これで一段階目の処理「周辺部の床の高さの算出」は終了です。

算出した床の高さとキャラクタ底面の接触判定

引き続き二段階目の処理「算出した床の高さとキャラクタ底面の接触判定」を行います。 接触判定の方法は、キャラクタが接地状態(歩行中など)なのか、飛行状態(ジャンプなど)なのか、梯子状態なのかで処理を分けます。

キャラクタが接地状態(歩行中など)の場合、床との接触判定によって行うべき処理は、

  1. 移動によってブロックからの落下が起こったかどうか
  2. 上り坂の場合に縦方向上向きに強制移動
  3. 下り坂の場合に縦方向下向きに強制移動

の三つです。

一番目の落下に関する処理は簡単で、キャラクタの下にブロックがあるかを調べればよいのです。 先ほど求め「キャラクタがその床にちょうど接する場合のy座標」minyを使用すれば、 キャラクタの移動後のy座標rc->yとminyが同じか、minyが上にあれば、キャラクタは床に接しており落下しない、 ということになります。今、y座標は下方向が正の向きにとってあるので、この判定条件は
(rc->y >= miny)
が満たされる時は落下しないことになります。

二番目の処理では、床の高さがキャラクタ底面より上にある場合、すなわち、判定条件
(rc->y > miny)
がtrueの時に、 その差の分だけキャラクタ座標を上に強制移動させます。 といっても、床に接した状態でのキャラクタ座標がそもそもminyの値ですので、
rc->y = miny;
を行うだけです。

三番目の処理では、床の高さがキャラクタ底面より下にある場合、斜めブロックによる下り坂ならば、 それに沿ってキャラクタを下に強制移動させます。 ここでは、足元が斜めブロックかどうかをちゃんと判定する、ということはせず、 キャラクタ底面と足場の隙間がある閾値以下ならば、下り坂と認識する、という方法を取ります。 隙間の閾値をMARGIN_GROUNDで定義し、判定条件
(rc->y > miny - MARGIN_GROUND)
がtrueの時に、 その差の分だけキャラクタ座標を下に強制移動させます。 先ほどと同様にキャラクタ座標にminyの値を再設定するだけです。
rc->y = miny;

三番目の処理がキャラクタ底面と足場の隙間による処理で、キャラクターが正しく動く条件としては、

ということです。どういうことかというと、 ゲーム中で取りえる最大速度をvx_maxとした場合、ゲーム中で最も傾きの急な斜めブロックの斜面が
y = a_max * x
と書ける場合、隙間の閾値MARGIN_GROUNDには
MARGIN_GROUND = a_max * vx_max
をセットすれば良いことになります。 もし一つ目の条件が満たされていなければ、隙間の閾値MARGIN_GROUNDが大きくなりすぎてしまいます。 MARGIN_GROUNDが最小のブロックの段差よりも大きい場合は、落下せずに滑らかに歩き続けてしまいます。 最小のブロックの段差というのは、正方ブロックだけの仕様なら最小のブロックの段差は1ブロック分ですが、 斜めブロックを許す仕様では例外がありえます。例えば、横幅2ブロック分で高さ1ブロック降りるような斜めブロックを使う場合、これは二種類の斜めブロックを必要とします。高さ1から高さ1/2まで降りるブロックと、高さ1/2から高さ0まで降りるブロックです。 このとき、前者のブロックの隣が空ブロックで、斜め下(隣の下)が通常ブロックの場合、ブロック段差が1/2となります。 これは歩いて通過しようとした場合落下すべき部分ですが、隙間の閾値MARGIN_GROUNDが1/2より大きい場合は、 落下とならずに滑らかに歩行を続けてしまいます。 このような状況を避けるため、隙間の閾値MARGIN_GROUNDは使用する斜めブロックの傾きも考慮して上手く決めてください。 これら二つのブロックは必ず隣りあわせで配置するという仕様にして回避するというのも手です。

これら三つの処理は結局一まとめにできて(これがminyを先に計算した理由です)、次のようになります。

#define MARGIN_GROUND	0x400				//斜めブロックの高さマージン,これ以下の差なら落下にならない
		if ( rc->y > miny - MARGIN_GROUND){	//4はマージン, ==は含まない
			rc->y = miny;
			returnvalue |= HITBLOCK_GROUND;

			if (rideBlock == 0x3){//中心が梯子(接地した場合のみ)
				returnvalue |= HITBLOCK_STANDLADDER;
			}
		}

returnvalueにHITBLOCK_GROUNDフラグを立てなかった場合は落下しますということになっています。 また、立っているのが通常のブロックか「梯子の頂点」ブロックかフラグも立てています。 梯子の上で下方向キーを入力した時に梯子状態に移行するかどうかの処理用です。

今度は、キャラクタが梯子状態の場合の接触判定を見てみましょう。 斜めブロックによる強制下りがない分、処理は楽になっていますね。

		if (( rc->y >= miny) && (rideBlock != 0x3) ){//足場が梯子じゃないとき
			rc->y = miny;
			returnvalue |= HITBLOCK_GROUND;
		}

また、飛行状態の場合も同様です。 飛行状態と梯子状態は、わざわざ分岐しなくてもいいかもしれませんね。

		if ( rc->y >= miny){
			rc->y = miny;
			returnvalue |= HITBLOCK_GROUND;

			if (rideBlock == 0x3){//中心が梯子(接地した場合のみ)
				returnvalue |= HITBLOCK_STANDLADDER;
			}

		}

以上で、斜めブロック用の縦方向処理は終わりです。 でもまだ次が残っています。 ブロックの高さを計算するHeightLine2関数です。 もう一息、がんばりましょう。


ブロック高さの取得

縦方向移動で床の高さを算出してくれた、ブロック高さの取得関数HeightLine2について説明します。 例によって、まずはソースコードを全体を示します。その後で個々に解説していきます。

inline int Map2::HeightLine2(int ileft, int icenter, int iright, int iy, int LowCenter,
							 unsigned char* standBlock, int flagLadder, OneMap* om){
	//斜めブロックの扱いはロックマンZERO風に変更(キャラの中心位置で斜めブロックの高さを決める)
	//iy段のブロックの高さを返す
	//flagLadder が 1 なら梯子を足場を扱う(下から通り抜けられる足場も)
	//flagLadder が 0 なら梯子を足場にしない(下から通り抜けられる足場も)
	//梯子は一つ上のブロックも梯子なら足場にならない
	

	int i;
	int cc;
	unsigned char b;

	//まずセンターを調べる
	*standBlock = b = GetHit2(icenter, iy,om);

	if ((b & 0xC) == 0xC){	//右下がり斜めブロック
		cc = LowCenter + ((int)(b & 0x3) < <  BITSHIFT);
		return (cc >> 2);	// cc / 4;

	}else if ((b & 0xC) == 0x8){	//右上がり斜めブロック
		cc = LowCenter + ((int)(b & 0x3) < <  BITSHIFT);
		return 0x1000 - (cc >> 2);	// cc / 4;
			
	}else if (b == 0x3){	//梯子
		if(flagLadder){
			if (GetHit2(icenter, iy - 1,om) != 0x3){
				return 0;
			}
		}
	}else if (b != 0){	//0x1, 0x4(ice)
		return 0;
	}


	//センターにブロックがない
	int miny = 0x1000;
	//センターより左を調べる
	for (i = icenter - 1; i >= ileft; i--){
		b = GetHit2(i, iy,om);

		if((b & 0x8) == 0x8){
			if(b == 0xB){
				*standBlock = b;
				return 0;
			}

		}else if (b == 0x3){	//梯子
			if(flagLadder){
				if (GetHit2(i, iy - 1,om) != 0x3){
					*standBlock = b;
					return 0;
				}
			}
		}else if (b != 0){	//0x1, 0x4(ice)
			*standBlock = b;
			return 0;
		}
	}

	//センターより右を調べる
	for (i = icenter + 1; i < = iright; i++){
		b = GetHit2(i, iy,om);
		
		if((b & 0x8) == 0x8){
			if(b == 0xC){
				*standBlock = b;
				return 0;
			}
		}else if (b == 0x3){	//梯子
			if(flagLadder){
				if (GetHit2(i, iy - 1,om) != 0x3){
					*standBlock = b;
					return 0;
				}
			}
		}else if (b != 0){	//0x1, 0x4(ice)
			*standBlock = b;
			return 0;
		}

	}


	*standBlock = 0;
	return 0x1000;
}

それでは、まずは引数を見ていきましょう。

inline int Map2::HeightLine2(int ileft, int icenter, int iright, int iy, int LowCenter,
							 unsigned char* standBlock, int flagLadder, OneMap* om){

ileft,irightはキャラクタ矩形の左右のブロックx座標です。 icenterはキャラクタ矩形の底辺の中点のブロックx座標です。 iyは高さを調べたい行のブロックy座標です。 LowCenterはキャラクタ矩形の底辺の中点をブロック幅で割った余りです。 本ソースコード中では次の式の結果を引数にセットしています。

	int LowCenter = ((rc->x + rc->w / 2) & 0xFFF);

standBlockには高さを返す元となったブロックの種類を返します。 関数中では(ileft,iy)-(iright,iy)の範囲のブロックを調べて、条件に合うブロックを見つけ、 そのブロックの種類をstandBlockに、ブロックの高さを関数の戻り値にします。 flagLadderは先に説明した「梯子の頂上」にあたる部分を乗れるものとみなすかどうかのフラグです。

この関数の戻り値として返されるブロックの高さは、引数で指定されたキャラクタの情報によって、 次の場合分けによって決まります。

キャラクタ矩形の底辺の中点(icenter,iy)のブロックを最初に調べ、それが斜めブロックの場合は、斜面との接点LowCenterでの高さを戻り値にします。この処理は次のようになります。

	if ((b & 0xC) == 0xC){	//右下がり斜めブロック
		cc = LowCenter + ((int)(b & 0x3) < <  BITSHIFT);
		return (cc >> 2);	// cc / 4;

	}else if ((b & 0xC) == 0x8){	//右上がり斜めブロック
		cc = LowCenter + ((int)(b & 0x3) < <  BITSHIFT);
		return 0x1000 - (cc >> 2);	// cc / 4;

中点(icenter,iy)のブロックが梯子で、かつ、フラグflagLadderが有効の場合、戻り値として0を返します。

	}else if (b == 0x3){	//梯子
		if(flagLadder){
			if (GetHit2(icenter, iy - 1,om) != 0x3){
				return 0;
			}
		}

y座標系は下が正方向なので、戻り値0はブロック高さが最高、戻り値0x1000がブロック高さが最低==ブロック無しを意味します。 誤解を招きそうでややこしいですが、気をつけてください。 中点(icenter,iy)のブロックが通常ブロックの場合も、当然、戻り値0を返します。

	}else if (b != 0){	//0x1, 0x4(ice)
		return 0;
	}

これらの条件に適合した場合は全てreturnで関数の処理から抜けます。 ここまでで条件に当てはまらず次の処理に進むのは、 中点のブロックになにもない(空ブロック)の場合だけです。 中点にブロックが無くとも、それより左右の部分でキャラクタ矩形がブロックに乗っている状態に相当します。 アクションゲームでキャラクタがブロックギリギリに立つような状況ですね。

以後の処理は、中点の左側(ileft,iy)-(icenter - 1,iy)と、右側(icenter + 1,iy)-(iright,iy)のブロックを順に調べていくことになります。左右対称で同じ処理なので、左側の判定処理だけを説明します。

さて、以後の処理では斜めブロックの場合の斜面の計算はしません。 なぜなら、最初に説明した図のa)の「矩形の底辺の中点での斜面処理」の場合、中点以外では斜めブロックの斜面部分に乗らないためです。斜めブロックで気をつけるポイントは、中点の右側のブロック高さを調べる際に、 右上がりには乗らないようにし(めり込む様にする)、 右下がりのブロックには乗れるようにする、ということです。 ソースコードでは次のように、右下がりで左側の高さが1のブロック(b == 0xC)だけに乗れるようにしています。

	//センターより右を調べる
	for (i = icenter + 1; i < = iright; i++){
		b = GetHit2(i, iy,om);
		
		if((b & 0x8) == 0x8){
			if(b == 0xC){
				*standBlock = b;
				return 0;
			}

また梯子にはフラグが有効の場合にだけ乗れるようにします。 通常ブロックの場合にも乗れるようにします。

		}else if (b == 0x3){	//梯子
			if(flagLadder){
				if (GetHit2(i, iy - 1,om) != 0x3){
					*standBlock = b;
					return 0;
				}
			}
		}else if (b != 0){	//0x1, 0x4(ice)
			*standBlock = b;
			return 0;
		}

	}

中点のブロックの判定処理と異なるのは、斜めブロックの場合に斜面の傾きによる高さ計算をするかどうかだけの違いですね。 これら全ての条件に当てはまらないとき。 すなわち(ileft,iy)-(iright,iy)の範囲が全て空ブロックの時だけ、戻り値に0x1000を返しています。 0x1000というのはブロックの縦幅16ピクセルに8bit固定小数点の分だけ左シフトした値です。 ブロックの縦幅が異なるなら、それに合わせて値を適宜変更してください。 これで、ブロックの高さを返す関数HeightLine2の説明は終わりです。


最後に

この資料では、僕の作っているツールの中から斜めブロックに関する処理の部分を抜き出して、 簡単な解説をしました。ツールを使ってみたい方や、これからアクションゲームのプログラムを組んでみたい人にとって、多少の参考になれば幸いです。 ただし、最初にも書きましたが、ここで解説した方法は、あくまで僕の考えた我流です。 既存のゲームを眺めても、処理方法は千差万別です。 そもそも横方向の移動の前に縦方向の移動処理を行うものも少なくありません。 線分同士の接触判定を用いて斜めブロックを実現しているものもあります。 最初に図で示したキャラクタと斜めブロックの接触方法に関しては、 キャラクタ矩形の底辺の中点での接触と、矩形の左右の端点での接触で、 ゲームの操作感や、マップ構成にまで影響してくる問題です。 既存ゲームのパロディーをする場合は、元のものがどちらをタイプに属するかを見極めるのが、 キーポイントの一つになるかもしれません。 ちなみに、某有名アクションゲームの斜めブロック処理は、今回説明した中点での接触判定を行っているようです。 え?どのゲームかって?あれです。壁蹴りハイスピードアクションの正統続編のあれです。ボディーを取られちゃったあの人のです。 とはいっても、大まかな種類が同じだけで、内部の処理は全然違うでしょうね。

でも、個人的には我流でいいと思うのです。 プログラミングの一番楽しい点は、頭で思い描く動作をどのようにコンピュータに処理させるか、その方法を試行錯誤する所だと思います。上手く動かない時は、アイデアの思いつかない時は確かに辛いです。その代わり完成した時の喜びが楽しいという意見も多いですが、その喜びも束の間。完成したものをお客さんやユーザーに出すと、十中八九苦情や新たな注文が出てきます。完成の喜びが大きいほど、このときの気持ちの沈み込みは激しいです。僕個人としては、完成時よりも悩みながらの作成時を楽しんでこそ、プログラミングの醍醐味なのかなと思います。その楽しむ術こそ、調べること以上に我流でいいから自ら考えることかなと思うのです。

長くなりましたが、ここまで読んでくださった方、ありがとうございます。 お互いに、良いゲームが作りたいものですね。

2009/04/06
Dr. Juicy