Appearance
第04回 構造体と矩形
前回: 第03回 入力と移動 / 次回: 第05回 弾を撃つ
今回の授業内容
- 座標を
Vec2構造体でまとめ、int座標の問題を解消する - 当たり判定用の範囲を
Rect構造体で表す - プレイヤーの座標を
intからVec2へ変更する - 矩形同士の重なりを判定する関数を作り、判定の考え方を理解する
- 見た目と当たり判定の位置を確認する
今回の開始状態
第03回終了時点では、playerXとplayerYという2つのintを使ってプレイヤーを動かしています
今回の終了時点では、位置をVec2で持ち、当たり判定用のRectを作れるようにします
変更するファイル
| ファイル | 変更内容 |
|---|---|
Common.h | Vec2とRectを追加する |
Collision.h | 矩形同士の重なり判定を追加する |
Game.h | プレイヤー座標をVec2へ変更する |
Game.cpp | 移動、描画、デバッグ表示をVec2基準に変更する |
なぜ座標をまとめるのか
第03回では、playerXとplayerYを別々のintとして扱いました
この形には2つの問題があります
1つ目は、xとyが常にセットで使われるにもかかわらず、コード上では別々の変数として離れて存在することです
関数へ座標を渡すときも、2つの引数に分かれてしまいます
2つ目は、intで座標を持っていたことです
第03回ではmoveX * playerSpeedをstatic_cast<int>で整数へ変換してから足していたため、playerSpeedが小さい場合や、斜め移動で1フレームあたりの移動量が1未満になった場合に、小数点以下が切り捨てられて移動量が0になることがありました
座標をfloatで持てば、この切り捨ては最終的に画面へ描画するときの1回だけで済みます
この2つの問題を、Vec2という1つの構造体にまとめることで解決します
Vec2を追加する
Common.hに、2D座標を表す構造体を追加します
cpp
struct Vec2
{
float x;
float y;
};Game.hのプレイヤー座標を、2つのintから1つのVec2へ変更します
cpp
Vec2 playerPosition = {};
float playerSpeed = 4.0f;初期化もVec2へ合わせます
cpp
playerPosition = { screenWidth * 0.5f, screenHeight - 60.0f };移動の計算では、static_cast<int>を挟まずにfloatのまま座標へ足し込みます
cpp
playerPosition.x += moveX * playerSpeed;
playerPosition.y += moveY * playerSpeed;static_cast<int>が必要になるのは、DrawTriangleのように整数座標を要求する描画関数へ値を渡す瞬間だけです
cpp
DrawTriangle(
static_cast<int>(playerPosition.x),
static_cast<int>(playerPosition.y - 18.0f),
static_cast<int>(playerPosition.x - 16.0f),
static_cast<int>(playerPosition.y + 14.0f),
static_cast<int>(playerPosition.x + 16.0f),
static_cast<int>(playerPosition.y + 14.0f),
cyan,
TRUE
);画面外制限も同じ考え方で、float同士の比較へ変更します
cpp
if (playerPosition.x < 18.0f) playerPosition.x = 18.0f;
if (playerPosition.x > screenWidth - 18.0f) playerPosition.x = screenWidth - 18.0f;
if (playerPosition.y < 32.0f) playerPosition.y = 32.0f;
if (playerPosition.y > screenHeight - 24.0f) playerPosition.y = screenHeight - 24.0f;Rectを追加する
当たり判定では、位置だけでなく幅と高さが必要ですCommon.hへ矩形を表す構造体を追加します
cpp
struct Rect
{
float x;
float y;
float width;
float height;
};Rectのxとyは、矩形の中心ではなく左上の座標を表します
一方、プレイヤーの見た目やゲーム内の位置は中心座標(playerPosition)で扱う方が、回転や拡大縮小を考えるときに都合がよいため、この授業では位置は中心、当たり判定は左上基準という2つの基準を使い分けます
プレイヤーの当たり判定を作るときは、中心座標から矩形の半分の大きさを引いて左上座標を求めます
cpp
Rect playerRect =
{
playerPosition.x - 14.0f,
playerPosition.y - 14.0f,
28.0f,
28.0f
};矩形同士の重なりを判定する
SourceにCollision.hを追加し、矩形同士の重なりを判定する関数を作ります
cpp
#pragma once
#include "Common.h"
inline bool IsOverlapping(const Rect& a, const Rect& b)
{
return a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y;
}この判定は、「重なっているか」を直接調べるのではなく、「重なっていない状態」を先に考え、その否定として組み立てています
2つの矩形が重なっていないのは、次のいずれかが成り立つときです
aがbより完全に左にある(a.x + a.width <= b.x)aがbより完全に右にある(a.x >= b.x + b.width)aがbより完全に上にある(a.y + a.height <= b.y)aがbより完全に下にある(a.y >= b.y + b.height)
この4つのどれにも当てはまらなければ、2つの矩形は重なっていますIsOverlapping()はこの4条件をすべて否定した形(<と>を使った比較)をAND(&&)でつないでおり、4つの否定がすべて真になったときにだけtrueを返します
この判定方法をAABB(Axis-Aligned Bounding Box、軸に平行な矩形)同士の衝突判定と呼びます
矩形が回転しない前提であれば、円や多角形を使うより計算が単純で、意図も追いやすくなります
まだ敵や弾はないため、今回はプレイヤーの矩形を線で表示して確認します
cpp
DrawBox(
static_cast<int>(playerRect.x),
static_cast<int>(playerRect.y),
static_cast<int>(playerRect.x + playerRect.width),
static_cast<int>(playerRect.y + playerRect.height),
yellow,
FALSE
);DrawBoxの第5引数FALSEは、塗りつぶさずに枠線だけを描く指定です
枠線にしているのは、この矩形が実際の見た目ではなく、当たり判定の範囲を確認するためのデバッグ表示だからです
動作確認
- 第03回と同じようにプレイヤーを移動できることを確認する
- 当たり判定の黄色い矩形がプレイヤーに追従することを確認する
- 矩形のサイズを変えて、見た目との違いを確認する
Collision.hをインクルードし忘れたときのエラーを確認し、戻すplayerSpeedを0.3fのような小さい値に変えても、Vec2基準ならプレイヤーが動き続けることを確認する
よくある問題
Vec2を追加した後は、playerXやplayerYの古い参照が残っていないか確認するfloatを描画関数へ渡すときは、static_cast<int>で整数へ変換するCollision.hは関数定義をヘッダーに置くため、inlineを付ける。付けないと、複数の.cppから同じヘッダーをインクルードした際に多重定義のリンクエラーになる- 矩形の
x・yを中心座標だと勘違いして計算すると、当たり判定の黄色い枠がプレイヤーより右下にずれる
今回の完成条件
Vec2で位置を表せるRectで当たり判定の範囲を表せる- プレイヤーの移動と描画を
Vec2基準に変更できる - AABB同士の重なり判定を「重なっていない条件の否定」として説明できる
まとめ
今回は、座標と範囲を構造体として整理し、float座標へ移行しました
次回は、固定配列と有効フラグを使って複数の弾を扱います