2Dゲーム制作Ⅰ③(2025/04/28)

目標:第一回作品審査会用作品をつくる!

・2Dシューティングゲーム
・企画、アイディア、画像、世界観は自由。横スクロールでもよい
・自機の動き キー入力で移動
・弾の発射 キー押下で発射
・弾の表示、攻撃、弾の発射
・スコア表示、ライフ表示(できれば)、ゲームスタート、ゲームオーバー表示

背景スクロール
⇒実際はかなり長い画像を作っておいてスクロールさせている。
 また、多重スクロールも行っている。

今週の目標

・複数の弾を表示
・敵の表示
・当たり判定(弾と敵)

【複数の弾を表示する】

弾を3発まで同時に管理できるようにパラメータを用意する

int flag, shotX, shotY;
int flag2, shotX2, shotY2;
int flag3, shotX3, shotY3;

(flagは本当はbool型が望ましい【0か1か】)
それぞれの弾について発射・移動・表示処理を書く。
・flag 表示/非表示
・ShotX,ShotY 表示する2D画面上の座標

【ほかの弾と発射タイミングをずらす】

・int shotTimer;
⇒一発撃つとタイマーがセットされ、タイマー期間は別の弾が撃てない。

・タイマーはプラスの数ならば(if shotTime > 0)、1づつ減らす。shotTimer–;
・タイマーの概念は超大事!

【今日のコード①】
#include “DxLib.h”

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
ChangeWindowMode(TRUE); // ウィンドウモード
SetGraphMode(640, 960, 32);
DxLib_Init(); // DXライブラリ初期化
SetDrawScreen(DX_SCREEN_BACK); // 裏画面に描画
int handleBg = LoadGraph(“bg.png”);
int handle = LoadGraph(“blackpiyo-s.png”);
int x = 320;
int y = 480;
int scrollY = 0;
int shotX = 0, shotY = 0 ,shotFlag = 0;
int shotX2 = 0, shotY2 = 0, shotFlag2 = 0;
int shotX3 = 0, shotY3 = 0, shotFlag3 = 0;// shotFlag…0:表示しない 1:表示
int shotTimer = 0;
int handleShot = LoadGraph(“egg.png”);
while (ProcessMessage() == 0) {
ClearDrawScreen();


if (CheckHitKey(KEY_INPUT_RIGHT))
{
x += 2;
}
if (CheckHitKey(KEY_INPUT_LEFT))
{
x -= 2;
}
if (CheckHitKey(KEY_INPUT_DOWN))
{
y += 2;
}
if (CheckHitKey(KEY_INPUT_UP))
{
y -= 2;
}
if (CheckHitKey(KEY_INPUT_SPACE) && (shotTimer == 0))
{
if (shotFlag == 0)
{
shotX = x + 6;
shotY = y;
shotFlag = 1;
shotTimer = 20;
}
else if (shotFlag2 == 0)
{
shotX2 = x + 6;
shotY2 = y;
shotFlag2 = 1;
shotTimer = 20;
}
else if (shotFlag3 == 0)
{
shotX3 = x + 6;
shotY3 = y;
shotFlag3 = 1;
shotTimer = 20;
}
}
if (shotTimer > 0)
{
shotTimer–;
}
scrollY += 8;
if (scrollY >= 960)
{
scrollY = 0;
}
DrawGraph(0, scrollY, handleBg, TRUE);
DrawGraph(0, scrollY – 960, handleBg, TRUE);
DrawGraph(x, y, handle, TRUE);
if (shotFlag == 1)
{
shotY -= 4;
if (shotY < 0)
{
shotFlag = 0;
}
DrawGraph(shotX, shotY, handleShot, TRUE);
}

if (shotFlag == 1)
{
shotY -= 4;
if (shotY > 960)
{
shotFlag = 0;
}
DrawGraph(shotX, shotY, handleShot, TRUE);
}
if (shotFlag2 == 1)
{
shotY2 -= 4;
if (shotY2 < 0)
{
shotFlag2 = 0;
}
DrawGraph(shotX2, shotY2, handleShot, TRUE);
}
if (shotFlag3 == 1)
{
shotY3 -= 4;
if (shotY3 < 0)
{
shotFlag3 = 0;
}
DrawGraph(shotX3, shotY3, handleShot, TRUE);
}

ScreenFlip();
}
DxLib_End();

}

shotごとの座標とflagを追加。タイマーを追加した。

【敵の表示】

int enemyFlag;
int enemyX;
int enemyY;
int enemyFlag;
int enemyX;
int enemyY;

・enemyFlagが1のとき敵を表示。
・Y座標を下に動かすことで「下に移動」させる
・Y座標を下に動かすことで「下に移動」させる。
・画面外に出たらenemyFlag = 0;で消す。

【今日のコード②】

#include "DxLib.h"

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
ChangeWindowMode(TRUE); // ウィンドウモード
SetGraphMode(640, 960, 32);
DxLib_Init(); // DXライブラリ初期化
SetDrawScreen(DX_SCREEN_BACK); // 裏画面に描画
int handleBg = LoadGraph("bg.png");
int handle = LoadGraph("blackpiyo-s.png");
int x = 320;
int y = 480;
int scrollY = 0;
int shotX = 0, shotY = 0 ,shotFlag = 0;
int shotX2 = 0, shotY2 = 0, shotFlag2 = 0;
int shotX3 = 0, shotY3 = 0, shotFlag3 = 0;// shotFlag…0:表示しない 1:表示
int shotTimer = 0;
int enemyFlag = 1, enemyX = 320 - 300 / 2, enemyY = 100; //enemy画像の幅の半分をずらして、真ん中付近に表示
int handleShot = LoadGraph("egg.png");
int handleEnemy = LoadGraph("mike.png");
while (ProcessMessage() == 0) {
    ClearDrawScreen();


    if (CheckHitKey(KEY_INPUT_RIGHT))
    {
        x += 2;
    }
    if (CheckHitKey(KEY_INPUT_LEFT))
    {
        x -= 2;
    }
    if (CheckHitKey(KEY_INPUT_DOWN))
    {
        y += 2;
    }
    if (CheckHitKey(KEY_INPUT_UP))
    {
        y -= 2;
    }
    if (CheckHitKey(KEY_INPUT_SPACE) && (shotTimer == 0))
    {
        if (shotFlag == 0)
        {
            shotX = x + 6;
            shotY = y;
            shotFlag = 1;
            shotTimer = 20;
        }
        else if (shotFlag2 == 0)
        {
            shotX2 = x + 6;
            shotY2 = y;
            shotFlag2 = 1;
            shotTimer = 20;
        }
        else if (shotFlag3 == 0)
        {
            shotX3 = x + 6;
            shotY3 = y;
            shotFlag3 = 1;
            shotTimer = 20;
        }
    }
    if (shotTimer > 0)
    {
        shotTimer--;
    }
    scrollY += 8;
    if (scrollY >= 960)
    {
        scrollY = 0;
    }
    DrawGraph(0, scrollY, handleBg, TRUE);
    DrawGraph(0, scrollY - 960, handleBg, TRUE);
    DrawGraph(x, y, handle, TRUE);
    if (shotFlag == 1)
    {
        shotY -= 4;
        if (shotY < 0)
        {
            shotFlag = 0;
        }
        DrawGraph(shotX, shotY, handleShot, TRUE);
    }

    if (shotFlag == 1)
    {
        shotY -= 4;
        if (shotY > 960)
        {
            shotFlag = 0;
        }
        DrawGraph(shotX, shotY, handleShot, TRUE);
    }
    if (shotFlag2 == 1)
    {
        shotY2 -= 4;
        if (shotY2 < 0)
        {
            shotFlag2 = 0;
        }
        DrawGraph(shotX2, shotY2, handleShot, TRUE);
    }
    if (shotFlag3 == 1)
    {
        shotY3 -= 4;
        if (shotY3 < 0)
        {
            shotFlag3 = 0;
        }
        DrawGraph(shotX3, shotY3, handleShot, TRUE);
    }
    if (enemyFlag == 1)
    {
        DrawGraph(enemyX, enemyY, handleEnemy, TRUE);
    }
    ScreenFlip();
}
DxLib_End();
}

敵のフラグと座標を宣言。アセットを読み込み、DrawGraphで表示。(enemyFlagが1の場合)。

【弾の当たり判定】

int dx = shotX1 – enemyX;
int dy = shotY1 – enemyY;
int dis2 = dx * dx + dy * dy;
if(dis2 < 4 * 4) {
// 当たり判定成立
enemyFlag = 0;
}
※弾2・弾3でも同様の判定を行う。

基本の考え方

  • **ピタゴラスの定理(三平方の定理)**は、2点間の距離を計算するために利用されます。
    • 例えば、弾の位置を ((x_1, y_1))、敵の位置を ((x_2, y_2)) とすると、距離 (d) は次の式で求められます: [ d = \sqrt{(x_2 – x_1)^2 + (y_2 – y_1)^2} ]

計算の効率化

ゲームのリアルタイム処理では、上記の「平方根」(\sqrt{}) を計算するのが負担になることがあります。特に当たり判定は非常に頻繁に行われるので、計算コストを下げる工夫が重要です。

そこで、次の方法が使われます:

  • 平方根を計算しないで済むようにする。
    • 距離 (d) をそのまま使うのではなく、距離の 二乗 (d^2) を比較に利用します。
    • つまり、判定は以下のように行います: [ (x_2 – x_1)^2 + (y_2 – y_1)2 ]
      • (R) は弾と敵が衝突したとみなす距離(半径)です。
      • (R^2) は予め計算しておくと効率的です。

なぜこれが速いのか?

  • 平方根 ((\sqrt{})) 計算の省略
    • 平方根を計算するのは通常の加減乗算よりも計算コストが高いです。
    • 距離の二乗を比較することで、この計算を完全に避けられます。

こうすることで計算を効率化し、ゲームのパフォーマンスを向上させることが可能になります。

結論

リアルタイムの処理が求められるゲームでは、計算コストを減らす工夫が大切です。距離の二乗で比較する方法は、当たり判定を効率的に行うための典型的なテクニックです。

参照:円の衝突判定

【今日のコード③】

#include "DxLib.h"

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
	ChangeWindowMode(TRUE); // ウィンドウモード
	SetGraphMode(640, 960, 32);
	DxLib_Init(); // DXライブラリ初期化
	SetDrawScreen(DX_SCREEN_BACK); // 裏画面に描画
	int handleBg = LoadGraph("bg.png");
	int handle = LoadGraph("blackpiyo-s.png");
	int x = 320;
	int y = 480;
	int scrollY = 0;
	int shotX = 0, shotY = 0, shotFlag = 0;
	int shotX2 = 0, shotY2 = 0, shotFlag2 = 0;
	int shotX3 = 0, shotY3 = 0, shotFlag3 = 0;// shotFlag…0:表示しない 1:表示
	int shotTimer = 0;
	int enemyFlag = 1, enemyX = 320 - 300 / 2, enemyY = 100, enemyTimer = 0; //enemy画像の幅の半分をずらして、真ん中付近に表示
	int handleShot = LoadGraph("egg.png");
	int handleEnemy = LoadGraph("mike.png");
	while (ProcessMessage() == 0) {
		ClearDrawScreen();

		if (CheckHitKey(KEY_INPUT_RIGHT))
		{
			x += 2;
		}
		if (CheckHitKey(KEY_INPUT_LEFT))
		{
			x -= 2;
		}
		if (CheckHitKey(KEY_INPUT_DOWN))
		{
			y += 2;
		}
		if (CheckHitKey(KEY_INPUT_UP))
		{
			y -= 2;
		}
		if (CheckHitKey(KEY_INPUT_SPACE) && (shotTimer == 0))
		{
			if (shotFlag == 0)
			{
				shotX = x + 6;
				shotY = y;
				shotFlag = 1;
				shotTimer = 20;
			}
			else if (shotFlag2 == 0)
			{
				shotX2 = x + 6;
				shotY2 = y;
				shotFlag2 = 1;
				shotTimer = 20;
			}
			else if (shotFlag3 == 0)
			{
				shotX3 = x + 6;
				shotY3 = y;
				shotFlag3 = 1;
				shotTimer = 20;
			}
		}
		if (shotTimer > 0)
		{
			shotTimer--;
		}
		int dx = 0;
		if (shotFlag == 1 && enemyFlag == 1)
		{
			dx = (shotX - enemyX) * (shotX - enemyX) + (shotY - enemyY) * (shotY - enemyY);
			if (dx < 300 * 300)
			{
				enemyFlag = 0;
				shotFlag = 0;
				enemyTimer = 50;
			}
		}
		if (shotFlag2 == 1 && enemyFlag == 1)
		{
			dx = (shotX2 - enemyX) * (shotX2 - enemyX) + (shotY2 - enemyY) * (shotY2 - enemyY);
			if (dx < 300 * 300)
			{
				enemyFlag = 0;
				shotFlag2 = 0;
				enemyTimer = 50;
			}
		}
		if (shotFlag3 == 1 && enemyFlag == 1)
		{
			dx = (shotX3 - enemyX) * (shotX3 - enemyX) + (shotY3 - enemyY) * (shotY3 - enemyY);
			if (dx < 300 * 300)
			{
				enemyFlag = 0;
				shotFlag3 = 0;
				enemyTimer = 50;
			}
		}

		scrollY += 8;
		if (scrollY > 960)
		{
			scrollY = 0;
		}
		DrawGraph(0, scrollY, handleBg, TRUE);
		DrawGraph(0, scrollY - 960, handleBg, TRUE);
		DrawGraph(x, y, handle, TRUE);
		if (shotFlag == 1)
		{
			shotY -= 4;
			if (shotY < 0)
			{
				shotFlag = 0;
			}
			DrawGraph(shotX, shotY, handleShot, TRUE);
		}
		if (shotFlag2 == 1)
		{
			shotY2 -= 4;
			if (shotY2 < 0)
			{
				shotFlag2 = 0;
			}
			DrawGraph(shotX2, shotY2, handleShot, TRUE);
		}

		if (shotFlag3 == 1)
		{
			shotY3 -= 4;
			if (shotY3 < 0)
			{
				shotFlag3 = 0;
			}
			DrawGraph(shotX3, shotY3, handleShot, TRUE);
		}

		if (enemyFlag == 1)
		{
			DrawGraph(enemyX, enemyY, handleEnemy, TRUE);
		}
		else
		{
			if (enemyTimer-- < 0)
			{
				enemyFlag = 1;
			}
		}


		ScreenFlip();
	}
	DxLib_End();
}

衝突判定部分を追加。dxとの比較は、画像が300*346のため、300に。

【敵の動き】
・発生タイミング:ランダム
・発生位置(X座標):ランダム
・使う関数:rand(),srand()
・Y方向に移動
・画面外に出たら消す

【今日のコード③】

#include<stdlib.h>
#include<time.h>
#include "DxLib.h"

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
	ChangeWindowMode(TRUE); // ウィンドウモード
	SetGraphMode(640, 960, 32);
	DxLib_Init(); // DXライブラリ初期化
	SetDrawScreen(DX_SCREEN_BACK); // 裏画面に描画
	int handleBg = LoadGraph("bg.png");
	int handle = LoadGraph("blackpiyo-s.png");
	int x = 320;
	int y = 480;
	int scrollY = 0;
	int shotX = 0, shotY = 0, shotFlag = 0;
	int shotX2 = 0, shotY2 = 0, shotFlag2 = 0;
	int shotX3 = 0, shotY3 = 0, shotFlag3 = 0;// shotFlag…0:表示しない 1:表示
	int shotTimer = 0;
	int enemyFlag = 1, enemyX = 0 , enemyY = 100, enemyTimer = 0,enemyVX = 0, enemyVY = 0;
	srand((unsigned)time(NULL) * 123);//ランダム初期化
	int handleShot = LoadGraph("egg.png");
	int handleEnemy = LoadGraph("mike.png");
	while (ProcessMessage() == 0) {
		ClearDrawScreen();

		if (CheckHitKey(KEY_INPUT_RIGHT))
		{
			x += 2;
		}
		if (CheckHitKey(KEY_INPUT_LEFT))
		{
			x -= 2;
		}
		if (CheckHitKey(KEY_INPUT_DOWN))
		{
			y += 2;
		}
		if (CheckHitKey(KEY_INPUT_UP))
		{
			y -= 2;
		}
		if (CheckHitKey(KEY_INPUT_SPACE) && (shotTimer == 0))
		{
			if (shotFlag == 0)
			{
				shotX = x + 6;
				shotY = y;
				shotFlag = 1;
				shotTimer = 20;
			}
			else if (shotFlag2 == 0)
			{
				shotX2 = x + 6;
				shotY2 = y;
				shotFlag2 = 1;
				shotTimer = 20;
			}
			else if (shotFlag3 == 0)
			{
				shotX3 = x + 6;
				shotY3 = y;
				shotFlag3 = 1;
				shotTimer = 20;
			}
		}
		if (shotTimer > 0)
		{
			shotTimer--;
		}
		int dx = 0;
		if (shotFlag == 1 && enemyFlag == 1)
		{
			dx = (shotX - enemyX) * (shotX - enemyX) + (shotY - enemyY) * (shotY - enemyY);
			if (dx < 300 * 300)
			{
				enemyFlag = 0;
				shotFlag = 0;
				enemyTimer = (rand() % 100);
				enemyX = (rand() % 360);
			}
		}
		if (shotFlag2 == 1 && enemyFlag == 1)
		{
			dx = (shotX2 - enemyX) * (shotX2 - enemyX) + (shotY2 - enemyY) * (shotY2 - enemyY);
			if (dx < 300 * 300)
			{
				enemyFlag = 0;
				shotFlag2 = 0;
				enemyTimer = (rand() % 100);
				enemyX = (rand() % 360);
			}
		}
		if (shotFlag3 == 1 && enemyFlag == 1)
		{
			dx = (shotX3 - enemyX) * (shotX3 - enemyX) + (shotY3 - enemyY) * (shotY3 - enemyY);
			if (dx < 300 * 300)
			{
				enemyFlag = 0;
				shotFlag3 = 0;
				enemyTimer = (rand() % 100);
				enemyX = (rand() % 360);
			}
		}
		scrollY += 8;
		if (scrollY > 960)
		{
			scrollY = 0;
		}
		DrawGraph(0, scrollY, handleBg, TRUE);
		DrawGraph(0, scrollY - 960, handleBg, TRUE);
		DrawGraph(x, y, handle, TRUE);
		if (shotFlag == 1)
		{
			shotY -= 4;
			if (shotY < 0)
			{
				shotFlag = 0;
			}
			DrawGraph(shotX, shotY, handleShot, TRUE);
		}
		if (shotFlag2 == 1)
		{
			shotY2 -= 4;
			if (shotY2 < 0)
			{
				shotFlag2 = 0;
			}
			DrawGraph(shotX2, shotY2, handleShot, TRUE);
		}

		if (shotFlag3 == 1)
		{
			shotY3 -= 4;
			if (shotY3 < 0)
			{
				shotFlag3 = 0;
			}
			DrawGraph(shotX3, shotY3, handleShot, TRUE);
		}

		if (enemyFlag == 1) {
			enemyX += enemyVX; // X方向の移動
			enemyY += enemyVY; // Y方向の移動

			// 範囲外判定:敵が画面外に出たらフラグをオフにし再出現を準備
			if (enemyX < 0 || enemyX > 640) {
				enemyVX = -enemyVX; // X方向の反転
			}
			if (enemyY < 0 || enemyY > 960) {
				enemyVY = -enemyVY; // Y方向の反転
			}
			DrawGraph(enemyX, enemyY, handleEnemy, TRUE);
		}
		else {
			// 敵の再出現処理
			if (enemyTimer-- < 0) {
				enemyFlag = 1;
				enemyX = rand() % 640;
				enemyY = -100;
				enemyVX = rand() % 5 - 2; // -2~2のランダム速度
				enemyVY = rand() % 5 - 2; // -2~2のランダム速度
				enemyTimer = 50;
			}
		}


		ScreenFlip();
	}
	DxLib_End();
}

今回の追加点をざっくり列挙。
弾(ショット)処理
弾は画面外(Y座標が負数)に出たら消えるよう制御。if (CheckHitKey(KEY_INPUT_SPACE) && (shotTimer == 0)) { // 空いているショットフラグに応じて弾を発射 }

スペースキーで弾を発射可能(shotFlag を使用して弾の状態を管理)。
if (CheckHitKey(KEY_INPUT_SPACE) && (shotTimer == 0)) {
// 空いているショットフラグに応じて弾を発射
}

敵の処理
敵が画面外に出ると移動方向を反転する(enemyVX または enemyVY を負にする)。
if (enemyX < 0 || enemyX > 640) {
  enemyVX = -enemyVX;
 } if (enemyY < 0 || enemyY > 960) {
  enemyVY = -enemyVY;
}

敵が画面を移動。enemyXenemyY が毎フレーム enemyVXenemyVY で更新されます。

敵の再出現
敵がプレイヤーの弾に当たるか、画面外に出ると一旦非表示になり、一定時間後に画面上部から再出現します。
if (enemyTimer-- < 0) {
enemyFlag = 1; // 敵を再出現
enemyX = rand() % 640; // ランダムなX位置
enemyY = -100; // 上部から再出現
enemyVX = rand() % 5 - 2; // ランダムな速度
enemyVY = rand() % 5 - 2;
}

弾と敵の当たり判定
弾の座標と敵の座標の距離を計算し、一定範囲内に入ったら衝突と判定。
dx = (shotX – enemyX) * (shotX – enemyX) + (shotY – enemyY) * (shotY – enemyY);
if (dx < 300 * 300) { // 距離が一定以下
enemyFlag = 0; // 敵を非表示
}

srand()について詳しくは↓
苦しんで覚えるC言語

【おまけ】
今日使ったアセット。

コメント

タイトルとURLをコピーしました