キャラクターの当たり判定を考える

 

アクションゲームやロールプレイングゲーム(DQとか)などはキャラクター同士の
当たり判定を考える必要がある。

当たり判定は大きく分けて2種類存在する。

1.動くキャラクター同士の判定

スプライトを使ってキャラクターを動かしているときに別のキャラと当たっているか
判断するには二つのキャラの位置を比較しないといけない。

この図のようにキャラAが(x1,y1)の位置にあってキャラBが(x2,y2)にあるとすると
二つのキャラの距離は次のようになる。

横方向で
|x1-x2|

縦方向で
|y1-y2|

距離なのでマイナスの時はプラスに反転させる。

さてキャラクターの大きさが32*32であるとするとこの距離が32よりも小さければ
キャラクターが重なっていることになる。

|x1-x2|<32

|y1-y2|<32

プログラムで書くと次のとおり

int a1=x1-x2;//距離を求める
int a2=y1-y2;//
if(a1<0)a1=-a1;//負は正に
if(a2<0)a2=-a2;//
if(a1<32 && a2<32){
//重なっています。
}

わざわざif文で負の判定をするのが面倒なので次のように書くこともできる。

int a1=(x1-x2)/32;//距離を求める(32ドット単位)
int a2=(y1-y2)/32;//
if(a1==0 && a2==0){
//重なっています。
}

32で割ると-31〜+31の値は0になることを利用している。
もっともこのやり方は割り算の遅いCPUでは少々痛い。

int a1=(x1-x2+32)>>6;
int a2=(y1-y2+32)>>6;
if(a1==0 && a2==0){
//重なっています。
}

というやり方ならGBのような非力なCPUでも速度的には問題ないだろう。
(ただ、サイズが限定されるが・・。)

で、これまでの方式の欠点は全て比較対象のキャラは同じサイズでないといけない。
例えば、キャラAが8*16ドットでキャラBが64*32だとさっきの計算式にうまく当てはまらないのである。

古典的にifの羅列で表現するならば、

if((x1<x2 && (x1+8)>x2) || (x2<x1 && (x2+64)>x1)){
 if((y1<y2 && (y1+16)>y2) || (y2<y1 && (y2+32)>y1)){
  //当たり
 }
}

となる。
これだと条件判定が14回も行っていることになる。(条件分岐はCPUが苦手とする部分)

計算式を工夫すると次のようにすることができる。
キャラAのサイズが(sx1,sy1)、キャラBのサイズが(sx2,sy2)とすると

unsigned int a1=((x1-x2) & (x2-(x1+sx1))) | ((x2-x1) & (x1-(x2+sx2)));
unsigned int a2=((y1-y2) & (y2-(y1+sy1))) | ((y2-y1) & (y1-(y2+sy2)));
unsigned int a3=a1 & a2;
if( a3>>31)
//当たり

これならば条件判定は1つになる。
なおこの原理は比較した結果、条件が成立するときに負の数になることを利用している。
x1<x2ならばx1-x2は必ず負の数になる。
同じように別の変数も比較し、それを論理積する。
その結果、全ての条件が成立していれば最後の答えも負の数になる。
負ならば最上位ビットが1なので31回右にシフトをしている。
(a1&0x80000000でも可能)

実験用プログラムは以下のものをダウンロード。

テストプログラム1(32KB)

2.背景とキャラクターの当たり判定

基本的な原理は1番と同じであるが、背景は通常チップと呼ばれる小さなパーツで
構成されている。
チップは全て同じ大きさで規則正しく並べられているので、当たり判定処理はもっと
単純になる。

背景はチップのサイズを16*16ドットとした。
画面サイズが320*240とすればチップの数は20*15=300個配置できる。
通常、背景は仮想画面に位置情報を記録しておくので以下のような変数を宣言する。

char Map[20*15];

キャラが画面からはみ出すなら少し余裕を持ったほうが処理が楽になる。(と同時にバグを出さない)

ゲームで背景を使うときはマップエディタなどで入力するが、今回はテストなので文字列で
宣言した。

char Map[20*15+1]={//仮想画面
"00000000000000000000"
"00001111000000000000"
(中略)
};

文字の”0”か”1”が並んでいる。
”0”が空間、”1”が障害物として判定は、

if(Map[???]=='1'){
//当たり
}

とすればよい。
もっとも”0”以外が全て障害物と識別するならば

Map[???]-'0'

としても同じことである。

さっき、規則正しいチップは計算が単純と書いたが、(x,y)の位置のチップを調べるには

Map[(x/16) + (y/16)*20]

とすれば求めることができる。
チップが16ドット単位であるからこそ16で割っている。

当たり判定の部分は関数として独立させた。

int IsChipHit(int x,int y)
{//x,y位置のチップを調べる。
 x=x/16;//チップ単位の座標に変換
 y=y/16;
 if(x>=0 && x<20 && y>=0 && y<15){//一応クリッピング
  return (Map[x+y*20]-'0');
 }
 return 0;
}

このように定義しておけば、

if(IsChipHit(x,y)){
//当たり
}

と活用することができる。

これで万事解決と思ったら、この方法ではキャラも16*16ドットでなければならない。
通常キャラはチップよりも大きいので困る。
ならば、キャラクターのサイズを16で割った個数だけチップと比較すればよい。

横に64ドットなら64/16=4個
縦に64ドットなら64/16=4個
合計4*4=16個

となる。
なお、通常はキャラと背景がめりこむことがないならば、キャラの中心部分の判定は
省略することができる。
よって

16-4=12個

となる。

どんな角度から当たっても大丈夫なように判定部分を強化したのが次の通り。

int IsChipHit(int x,int y)
{//x,y位置のチップを調べる。
 int x2=(x+15)/16;
 int y2=(y+15)/16;
 int x1=x/16;//チップ単位の座標に変換
 int y1=y/16;
 if(x1>=0 && x1<20 && y1>=0 && y1<15){//一応クリッピング
  return (Map[x1+y1*20]-'0') | (Map[x1+y2*20]-'0')
   | (Map[x2+y1*20]-'0') | (Map[x2+y2*20]-'0');
 }
 return 0;
}

これなら前後左右の当たり判定が完璧である。

テストプログラム2(33KB)

背景がスクロールする場合は、スクロールでずれた部分を補正しないといけないので
注意すること。

Gポイントポイ活 Amazon Yahoo 楽天

無料ホームページ 楽天モバイル[UNLIMITが今なら1円] 海外格安航空券 海外旅行保険が無料!