ついに当たり判定を計算する
当たり判定は、ゲームを作りたい学生なら大体聞いたことがある言葉だと思う。
ゲームに登場するオブジェクトとオブジェクトが接触しているかどうかを判定する数学的(幾何学的意味合いの)計算をコンピュータ上で行うことである。
詳しくやると、幾何学、離散数学や線形代数、CGの知識がないとなかなか難しい処理ではある。
そこまで難しいものは逆にリアルタイムのゲーム上で行うのも難しくなってきたりもするので、一般的によく用いられる手法を理解して使っていくようにしよう!
まず当たり判定をどのような処理単位でやるかが問題になる。
現在作っているゲームはタイルベース、つまりキャラクタは基本的に軸に平行な四角形(タイル)の形に収まっているものを想定している。
タイルが動くとその中の画像も一緒に動く。タイルの中にはキャラクタの周りに透明部分があって、キャラクタだけが描画されるのはその透明部分のおかげである。
このように背景と合成するために、透明部分を持った素材のことをゲームではよくsprite(スプライト)と呼ぶ。
つまり、厳密にこのスプライトのキャラ部分だけで、細かい当たり判定をしようとするとえらい大変なのである。
なので、もっと簡単な方法で当たり判定をやっていこうではないかという話。
なので、タイルベースゲームでは大体キャラを囲む四角形(矩形)をつかって当たり判定をする。
CGで習ったと思うけど、キャラクタが動いても軸に平行にバウンディングボックスをとる手法をAABB(Axis Aligned Bounding Box)、
キャラやオブジェクトと一緒に開店しちゃうものをOBB(Object-Oriented Bounding Box)と呼ぶ。タイルベースではタイルベースなので大体AABBで処理を行う
あとは、AABB同士が重なる条件を考えて、衝突判定、交差判定(Collision detection/Intersection detection)を行えばよい。
(ものによってはヒットチェック(hit check)というときもあるよ、意味はほとんど全部同じで使ってる)
まず、自分のキャラクタの位置とAABBの関係を考えてみよう。
Siv3Dでは矩形はRect型で表すことができる。Rect型は、タイルの左上の座標と、幅、高さの3つの値をメンバーに持つ型である
座標系の+方向によって変わってくるが、自分のキャラクターの位置が、タイルの真ん中で表されている場合は、タイルの幅と高さの半分の長さがわかると矩形の左上の点がわかる。
今更だが、SetCharaRectの設定はこのやり方そのままである。
Rect同士が、接触している、していないとは、その2つの図形の内部に共通部分が有るか無いかで判別できる。
実際に当たり判定が発生する条件を考えていこう。
RectAとRectBの2つの矩形があったとしよう。RctAとRectBははじめ確実に重ならない状況からスタートする。
その状態から、x軸、y軸沿いにギリ接触するところまでRectBを移動させることを考える。
そうすると以下の図のような状況が考えられる。
x軸、y軸どちらの時も、ギリ接触状態よりもRectBがRecrtAに近寄ってしまった時が接触状態となる。
RectAを基準に接触判定を考えると、
- RectAの中心座標を$P_A$、幅を$width_A$、高さを$height_A$
- RectBの中心座標を$P_B$、幅を$width_B$、高さを$height_B$
- x軸上での条件として
- RectAの中心座標(x軸上での)を原点として、+方向にでも、‐方向にでも$P_A$と$P_B$の距離が$width_A/2 + width_B/2$より小さい
- y軸上での条件として
- RectAの中心座標(y軸上での)を原点として、+方向にでも、‐方向にでも$P_A$と$P_B$の距離が$height_A/2 + height_B/2$より小さい
- これらを両方満たすとき、2つの矩形は接触している
図で表すと
最終的な条件として、
- x軸方向に
- $ | P_A.x - P_B.x| < (width_A/2 + width_B/2) $
- y軸方向に
- $ | P_A.y - P_B.y| < (heigth_A/2 + height_B/2) $
となる。+方向、‐方向の距離が絶対値で表されるのは中学校の数学の教科書か、この辺を見ること
Rectを2つ受け取って当たり判定する関数を考える
当たり判定関数を考えよう。2つの矩形の当たり判定をする。
戻り値は当たっているかどうかなので、boolで返すと良いと思う。
- bool IsRectIntersectsOtherRect(Rect _rectA, Rect _rectB)
ぐらいでいいかなと思う。(長いけど)
値に関してはSiv3DのRect型はいろいろ便利な機能があるのでそれを使ってしまおう
矩形の中心点は、もともとキャラクタの中心点を左上の点に直しているので、キャラクタの中心座標を持ってきてもよいが、実はRect型パラメータとして中心点を取得できる。
_rectAの中心座標は、 (_rectA.CenterX(), _recrA.CenterY())で取得可能である。
矩形_rectAの幅と高さは、幅は_rectA.wと、高さを_rectA.hで参照可能
またC++の標準関数abs(値)で、絶対値を取得できる。(absはオーバーロードという機能を使っており、実数でも整数でも同じように絶対値を取得できる)
関数作ろう
bool IsRectIntersectsOtherRect(Rect _rectA, Rect _rectB) { int wABは_rectA.w / 2と_rectB.w / 2の和(x軸方向の判定用) int hABは_rectA.h / 2と_rectB.h / 2の和(y軸方向の判定用) int distABx = _rectAと_rectBのx軸方向の中心座標の差の絶対値 int distABy = _rectAと_rectBのy軸方向の中心座標の差の絶対値 if (上の説明の2つの条件を同時に満たすとき(複合条件で書くよ)) return true; else return false; }
これで、なんかできそうな気がする
実装例
新しいSiv3Dプロジェクトで、この関数が正しいかどうか確認してみよう
矩形を2個宣言して、キーボードで動かしながらその2つが交差したら、矩形の内部を塗る。そうではないときは画像を表示するソースコードである。
- Main.cpp
# include <Siv3D.hpp> // OpenSiv3D v0.6.10 const int heroSize{ 100 }; const int enemySize{ 80 }; enum direction { UP, LEFT, DOWN, RIGHT, NONE }; direction GetDirection() { if ((KeyUp | KeyW).pressed()) { return UP; } else if ((KeyLeft | KeyA).pressed()) { return LEFT; } else if ((KeyDown | KeyS).pressed()) { return DOWN; } else if ((KeyRight | KeyD).pressed()) { return RIGHT; } else return NONE; } void UpdatePlayer(Point& _pos) { const int SPEED{ 3 }; direction d = GetDirection(); Point moveDir{ 0,0 }; switch (d) { case LEFT: moveDir = { -1, 0 }; break; case RIGHT: moveDir = { 1, 0 }; break; case UP: moveDir = { 0, -1 }; break; case DOWN: moveDir = { 0, 1 }; break; default: return; } //これも関数化しちゃった方がすっきりするかもねぇ _pos = _pos + SPEED * moveDir; } void SetHeroRect(Rect& _rect, Point _pos) { Point heroHosei{ heroSize / 2, heroSize / 2 }; _rect = { _pos - heroHosei, {heroSize, heroSize } }; } bool IsRectIntersectsOtherRect(Rect _rectA, Rect _rectB) { 自分で作った奴使ってね。 } void Main() { const Texture hero{ U"🕺"_emoji }; const Texture enemy{ U"🐲"_emoji }; Point heroPos{ 50,50 }; Point enemyPos{ Scene::Center() }; Point heroHosei{ heroSize / 2, heroSize / 2 }; Point enemyHosei{ enemySize / 2, enemySize / 2 }; Rect heroRect{ heroPos - heroHosei, {heroSize, heroSize} }; Rect enemyRect{ Scene::Center()-enemyHosei, {enemySize, enemySize} }; while (System::Update()) { Scene::SetBackground(Palette::Lightgray); UpdatePlayer(heroPos); SetHeroRect(heroRect, heroPos); hero.resized(heroSize).drawAt(heroPos); if (IsRectIntersectsOtherRect(heroRect, enemyRect)) { heroRect.draw({Palette::Yellow, 0.5}); heroRect.drawFrame(1, 1, Palette::Red); } else heroRect.drawFrame(1, 1, Palette::Red); enemy.resized(enemySize).drawAt(enemyPos); enemyRect.drawFrame(1, 1, Palette::Red); } }