Skip to content

第04回 2Dシューティングゲーム入門 1 / 構造体と衝突判定の基本

前回: 第03回 入力と移動 / キー入力でキャラクターを動かす / 次回: 第05回 2Dシューティングゲーム入門 2 / 配列で弾を複数扱う

前回の振り返り

キー入力でプレイヤー画像を動かしました

  • CheckHitKey を使って、押されているキーを確認しました
  • playerXplayerY を更新して、プレイヤーの位置を変えました
  • DrawFormatString で座標を表示し、値を見ながら動作を確かめました
  • プレイヤーが画面外に出ないよう、値の範囲を制限する処理を追加しました
  • 今回は敵と衝突判定を加えて、制作物をよりゲームに近づけていきます

今回の目的

敵との衝突を判定できるようにする

  • struct でプレイヤーと敵の情報を整理できる
  • 座標と大きさから矩形の重なりを判断できる
  • 判定結果を画面表示に反映できる
  • 次回の弾処理へつながる土台を説明できる

今回の授業内容

2Dシューティングゲーム入門 1 / 構造体と衝突判定の基本

  • シューティングゲームに必要な最小要素
  • struct による位置と大きさの管理
  • プレイヤーと敵の表示
  • 矩形どうしの衝突判定
  • 判定結果の表示
  • 次回の弾処理に向けた整理

2Dシューティング ゲーム入門

敵との 衝突判定を作ります

だんだんと ゲームに なっていく

2Dシューティングゲームに必要な要素

最低限の構成要素

  1. プレイヤーが動く
  2. 敵が画面にいる
  3. プレイヤーが弾を発射する
  4. 衝突判定がある
  • 今回はこのうち プレイヤーと敵が当たったことが分かる までの実装を目標にします
  • 弾をたくさん出す処理は次回、配列と呼ばれる仕組みを使って作ります

構造体とは

関連する値をひとまとめにする仕組み

cpp
struct GameObject
{
    int x;
    int y;
    int width;
    int height;
};
  • struct は、関連する複数の値を1つのまとまりとして扱うための仕組みです
  • 例えばプレイヤーを扱うなら、位置だけでなく衝突判定の大きさも 同じまとまりに入れておくと管理しやすくなります

型とは

C++では変数の種類を型で区別する

  • int は整数、bool は真偽値というように、C++では変数に必ず型があります
  • 型が決まると、変数にどのような値を入れられるか、 その変数にどのような処理ができるかも決まります
  • struct GameObject と書くと、xywidthheight を持つ GameObject型 を自分で定義したことになります GameObject player; は、GameObject型の変数 player を作るという意味です
  • 構造体は自分で新しい型を定義するための方法 のひとつです

構造体を使う理由

変数が増えても整理しやすい

  • 例えばプレイヤーの情報を playerX, playerY, playerWidth, playerHeight のように ばらばらの変数で持つと、対象が増えたときに見分けづらくなります
  • GameObject player; のようにまとめておけば 「この値はプレイヤーの情報だ」とすぐ分かります
  • また、プレイヤーも敵も「位置と大きさを持つ」という点では共通しています GameObject enemy; とすれば、表現を統一できてプログラミングしやすくなります
  • ゲームでは関連する情報をまとめて扱う場面が多いため
    この段階で構造体の扱いに慣れておきましょう

プレイヤーと敵の情報を構造体でまとめる

同じ型から別々のデータを用意する

cpp
GameObject player = { 320, 400, 32, 32 };
GameObject enemy  = { 320, 100, 48, 48 };

player.x += 4;  // プレイヤーの位置を右に4動かす
enemy.y += 2;   // 敵の位置を下に2動かす
  • playerenemy は同じ GameObject 型ですが それぞれ別の情報を記録しています
  • player.xenemy.y のように . を使うことで 構造体の中にある値を指定できます 例えば player.x += 4; とすれば プレイヤーの位置を右に4動かすことができます

実習1: struct を定義して値を入れる

プレイヤーと敵の情報をまとめる

  1. GameObject 構造体を定義する
  2. playerenemy の変数を作る
  3. player.xenemy.y の値を変えて、別々の情報として扱えることを確認する
  4. 余裕があれば、幅や高さの値も変更してみる

衝突判定とは

2つの範囲が重なったかを調べる

画像

  • 衝突判定は、ゲームの中でオブジェクト同士が 当たったかどうかを調べるための処理です
  • 実行時の速度と調整のしやすさを考慮して、 ゲームの衝突判定は矩形や円などの単純な 形状で扱われることが多いです
  • 今回は、プレイヤーと敵の衝突判定を 矩形(四角形) として扱います 四角形どうしが重なっていたら 「衝突した」と判断します

矩形の衝突判定

矩形の左右上下の位置関係で重なりを調べる

  • 二つの矩形の横方向と縦方向が両方とも重なっているときだけ true になります
  • 条件が1つでも満たされないときは false になります
  • 例えば、a.x < b.x + b.width が満たされないときは 「aの左端がbの右端より右にある」ことになり、重なっていないことになります
cpp
bool IsHit(GameObject a, GameObject b)
{
    return a.x < b.x + b.width &&
        b.x < a.x + a.width &&
        a.y < b.y + b.height &&
        b.y < a.y + a.height;
}

画像を表示しながら衝突判定する

表示は画像、判定は四角形で考える

  • 表示は前回までと同じように画像で行います
  • 衝突判定では、画像の位置と大きさを 四角形の範囲として考えます
cpp
DrawGraph(player.x, player.y, playerHandle, TRUE);
DrawGraph(enemy.x, enemy.y, enemyHandle, TRUE);

サンプルコード(1/4)

cpp
#include "DxLib.h"

struct GameObject
{
    int x;
    int y;
    int width;
    int height;
};

bool IsHit(GameObject a, GameObject b)
{
    return a.x < b.x + b.width &&
        b.x < a.x + a.width &&
        a.y < b.y + b.height &&
        b.y < a.y + a.height;
}

サンプルコード(2/4)

cpp
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
    GameObject player = { 320, 400, 32, 32 };
    GameObject enemy  = { 320, 100, 48, 48 };
    int moveSpeed = 4;

    ChangeWindowMode(TRUE);
    if (DxLib_Init() == -1) return -1;
    SetDrawScreen(DX_SCREEN_BACK);

    int playerHandle = LoadGraph(L"Images/Player.png");
    int enemyHandle = LoadGraph(L"Images/Enemy.png");

    while (ProcessMessage() == 0 &&
        CheckHitKey(KEY_INPUT_ESCAPE) == 0)
    {
        if (CheckHitKey(KEY_INPUT_LEFT))  player.x -= moveSpeed;
        if (CheckHitKey(KEY_INPUT_RIGHT)) player.x += moveSpeed;
        if (CheckHitKey(KEY_INPUT_UP))    player.y -= moveSpeed;
        if (CheckHitKey(KEY_INPUT_DOWN))  player.y += moveSpeed;

        bool isHit = IsHit(player, enemy);
        ClearDrawScreen();

サンプルコード(3/4)

cpp
    if (isHit)
    {
        DrawGraph(enemy.x, enemy.y, enemyHandle, TRUE);

        DrawFormatString(10, 10,
            GetColor(255, 255, 255), L"Hit!");
    }
    else
    {
        DrawGraph(enemy.x, enemy.y, enemyHandle, TRUE);

        DrawFormatString(10, 10,
            GetColor(255, 255, 255), L"Not Hit");
    }

サンプルコード(4/4)

cpp
        DrawGraph(player.x, player.y, playerHandle, TRUE);

        ScreenFlip();
    }

    DxLib_End();

    return 0;
}

処理の手順

衝突判定は毎フレーム、プレイヤーの位置を更新したあとに行う

  1. キー入力で player.xplayer.y を更新する
  2. IsHit で、プレイヤーと敵が重なったかを調べる
  3. 結果に応じて、表示文字を変えて描画する
  4. 毎フレームこれを繰り返す
  • 衝突判定は、プレイヤーの位置を更新したあとに行います プレイヤーの位置を変える前に判定してしまうと 入力に対して1フレーム遅れた結果になってしまいます

衝突判定が入ると何が変わるか

ただ動くだけの画面からゲームに近づく

  • 入力だけでは「画像が動くサンプル」ですが、衝突判定が入れば オブジェクト同士の関係を扱ってゲームのルールを作ることができます
  • シューティングゲームでは、プレイヤーと敵、弾と敵、自機と敵弾など 多くの場面で衝突判定が必要になります
  • 今回の内容は、次回の弾処理と、その次のゲームオーバー処理につながります

実習2: 衝突判定を完成させる

プレイヤーと敵が重なるか確かめる

  1. GameObject の構造体を定義する
  2. playerenemy を作り、画像として画面に描画する
  3. IsHit 関数を追加して、重なりを判定する
  4. 当たったときに色か文字が変わるようにする
  • 余裕があれば、敵の位置や大きさを変えて当たりやすさを調整します

よくあるつまずき

判定がうまくいかないときの確認ポイント

  • player.widthenemy.height など、大きさの値が 0 になっていないか
  • LoadGraph の画像ファイル名やフォルダ名が合っているか
  • IsHit の結果を毎フレーム計算しているか
  • プレイヤーの座標更新と衝突判定の順番が崩れていないか
  • DrawFormatString の表示が変わるかどうかで、判定と描画を分けて確認したか

今回のまとめ

今日のポイント

  • struct を使うと、位置や大きさのような関連する情報をまとめて扱えます
  • 衝突判定は、オブジェクトどうしの重なりを数値で調べる処理です
  • プレイヤー移動、敵配置、衝突判定がそろうとゲームの基本形が見えてきます
  • 判定結果を色や文字で見えるようにすると、デバッグしやすくなります
  • 次回は配列を使い、弾を複数扱う形に進めます

おつかれさまでした!

次回予告 第05回 2Dシューティング入門 2

次は弾を発射して さらにゲームっぽく