Skip to content

第04回 構造体と矩形

前回: 第03回 入力と移動 / 次回: 第05回 弾を撃つ

今回の授業内容

  • 座標をVec2構造体でまとめ、int座標の問題を解消する
  • 当たり判定用の範囲をRect構造体で表す
  • プレイヤーの座標をintからVec2へ変更する
  • 矩形同士の重なりを判定する関数を作り、判定の考え方を理解する
  • 見た目と当たり判定の位置を確認する

今回の開始状態

第03回終了時点では、playerXplayerYという2つのintを使ってプレイヤーを動かしています
今回の終了時点では、位置をVec2で持ち、当たり判定用のRectを作れるようにします

変更するファイル

ファイル変更内容
Common.hVec2Rectを追加する
Collision.h矩形同士の重なり判定を追加する
Game.hプレイヤー座標をVec2へ変更する
Game.cpp移動、描画、デバッグ表示をVec2基準に変更する

なぜ座標をまとめるのか

第03回では、playerXplayerYを別々のintとして扱いました
この形には2つの問題があります

1つ目は、xとyが常にセットで使われるにもかかわらず、コード上では別々の変数として離れて存在することです
関数へ座標を渡すときも、2つの引数に分かれてしまいます

2つ目は、intで座標を持っていたことです
第03回ではmoveX * playerSpeedstatic_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;
};

Rectxyは、矩形の中心ではなく左上の座標を表します
一方、プレイヤーの見た目やゲーム内の位置は中心座標(playerPosition)で扱う方が、回転や拡大縮小を考えるときに都合がよいため、この授業では位置は中心、当たり判定は左上基準という2つの基準を使い分けます
プレイヤーの当たり判定を作るときは、中心座標から矩形の半分の大きさを引いて左上座標を求めます

cpp
Rect playerRect =
{
    playerPosition.x - 14.0f,
    playerPosition.y - 14.0f,
    28.0f,
    28.0f
};

矩形同士の重なりを判定する

SourceCollision.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つの矩形が重なっていないのは、次のいずれかが成り立つときです

  • abより完全に左にある(a.x + a.width <= b.x
  • abより完全に右にある(a.x >= b.x + b.width
  • abより完全に上にある(a.y + a.height <= b.y
  • abより完全に下にある(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は、塗りつぶさずに枠線だけを描く指定です
枠線にしているのは、この矩形が実際の見た目ではなく、当たり判定の範囲を確認するためのデバッグ表示だからです

動作確認

  1. 第03回と同じようにプレイヤーを移動できることを確認する
  2. 当たり判定の黄色い矩形がプレイヤーに追従することを確認する
  3. 矩形のサイズを変えて、見た目との違いを確認する
  4. Collision.hをインクルードし忘れたときのエラーを確認し、戻す
  5. playerSpeed0.3fのような小さい値に変えても、Vec2基準ならプレイヤーが動き続けることを確認する

よくある問題

  • Vec2を追加した後は、playerXplayerYの古い参照が残っていないか確認する
  • floatを描画関数へ渡すときは、static_cast<int>で整数へ変換する
  • Collision.hは関数定義をヘッダーに置くため、inlineを付ける。付けないと、複数の.cppから同じヘッダーをインクルードした際に多重定義のリンクエラーになる
  • 矩形のxyを中心座標だと勘違いして計算すると、当たり判定の黄色い枠がプレイヤーより右下にずれる

今回の完成条件

  • Vec2で位置を表せる
  • Rectで当たり判定の範囲を表せる
  • プレイヤーの移動と描画をVec2基準に変更できる
  • AABB同士の重なり判定を「重なっていない条件の否定」として説明できる

まとめ

今回は、座標と範囲を構造体として整理し、float座標へ移行しました
次回は、固定配列と有効フラグを使って複数の弾を扱います