Appearance
第04回 2Dシューティングゲーム入門 1 / 構造体と衝突判定の基本
前回: 第03回 入力と移動 / キー入力でキャラクターを動かす / 次回: 第05回 2Dシューティングゲーム入門 2 / 配列で弾を複数扱う
前回の振り返り
キー入力でプレイヤー画像を動かしました
CheckHitKeyを使って、押されているキーを確認しましたplayerXとplayerYを更新して、プレイヤーの位置を変えましたDrawFormatStringで座標を表示し、値を見ながら動作を確かめました- プレイヤーが画面外に出ないよう、値の範囲を制限する処理を追加しました
- 今回は敵と衝突判定を加えて、制作物をよりゲームに近づけていきます
今回の目的
敵との衝突を判定できるようにする
structでプレイヤーと敵の情報を整理できる- 座標と大きさから矩形の重なりを判断できる
- 判定結果を画面表示に反映できる
- 次回の弾処理へつながる土台を説明できる
今回の授業内容
2Dシューティングゲーム入門 1 / 構造体と衝突判定の基本
- シューティングゲームに必要な最小要素
structによる位置と大きさの管理- プレイヤーと敵の表示
- 矩形どうしの衝突判定
- 判定結果の表示
- 次回の弾処理に向けた整理
2Dシューティング ゲーム入門
敵との 衝突判定を作ります
だんだんと ゲームに なっていく
2Dシューティングゲームに必要な要素
最低限の構成要素
- プレイヤーが動く
- 敵が画面にいる
- プレイヤーが弾を発射する
- 衝突判定がある
- 今回はこのうち プレイヤーと敵が当たったことが分かる までの実装を目標にします
- 弾をたくさん出す処理は次回、配列と呼ばれる仕組みを使って作ります
構造体とは
関連する値をひとまとめにする仕組み
cpp
struct GameObject
{
int x;
int y;
int width;
int height;
};structは、関連する複数の値を1つのまとまりとして扱うための仕組みです- 例えばプレイヤーを扱うなら、位置だけでなく衝突判定の大きさも 同じまとまりに入れておくと管理しやすくなります
型とは
C++では変数の種類を型で区別する
intは整数、boolは真偽値というように、C++では変数に必ず型があります- 型が決まると、変数にどのような値を入れられるか、 その変数にどのような処理ができるかも決まります
struct GameObjectと書くと、x、y、width、heightを持つ 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動かすplayerとenemyは同じGameObject型ですが それぞれ別の情報を記録していますplayer.xやenemy.yのように.を使うことで 構造体の中にある値を指定できます 例えばplayer.x += 4;とすれば プレイヤーの位置を右に4動かすことができます
実習1: struct を定義して値を入れる
プレイヤーと敵の情報をまとめる
GameObject構造体を定義するplayerとenemyの変数を作るplayer.xやenemy.yの値を変えて、別々の情報として扱えることを確認する- 余裕があれば、幅や高さの値も変更してみる
衝突判定とは
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;
}処理の手順
衝突判定は毎フレーム、プレイヤーの位置を更新したあとに行う
- キー入力で
player.xとplayer.yを更新する IsHitで、プレイヤーと敵が重なったかを調べる- 結果に応じて、表示文字を変えて描画する
- 毎フレームこれを繰り返す
- 衝突判定は、プレイヤーの位置を更新したあとに行います プレイヤーの位置を変える前に判定してしまうと 入力に対して1フレーム遅れた結果になってしまいます
衝突判定が入ると何が変わるか
ただ動くだけの画面からゲームに近づく
- 入力だけでは「画像が動くサンプル」ですが、衝突判定が入れば オブジェクト同士の関係を扱ってゲームのルールを作ることができます
- シューティングゲームでは、プレイヤーと敵、弾と敵、自機と敵弾など 多くの場面で衝突判定が必要になります
- 今回の内容は、次回の弾処理と、その次のゲームオーバー処理につながります
実習2: 衝突判定を完成させる
プレイヤーと敵が重なるか確かめる
GameObjectの構造体を定義するplayerとenemyを作り、画像として画面に描画するIsHit関数を追加して、重なりを判定する- 当たったときに色か文字が変わるようにする
- 余裕があれば、敵の位置や大きさを変えて当たりやすさを調整します
よくあるつまずき
判定がうまくいかないときの確認ポイント
player.widthやenemy.heightなど、大きさの値が 0 になっていないかLoadGraphの画像ファイル名やフォルダ名が合っているかIsHitの結果を毎フレーム計算しているか- プレイヤーの座標更新と衝突判定の順番が崩れていないか
DrawFormatStringの表示が変わるかどうかで、判定と描画を分けて確認したか
今回のまとめ
今日のポイント
structを使うと、位置や大きさのような関連する情報をまとめて扱えます- 衝突判定は、オブジェクトどうしの重なりを数値で調べる処理です
- プレイヤー移動、敵配置、衝突判定がそろうとゲームの基本形が見えてきます
- 判定結果を色や文字で見えるようにすると、デバッグしやすくなります
- 次回は配列を使い、弾を複数扱う形に進めます