4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

OpenSiv3Dのサンプルにあるシューティングゲームを改造して弾幕を出してみる(初心者向け)

Last updated at Posted at 2021-12-16

#Q : なんですかこれは
A : タイトルの通りです。OpenSiv3Dのチュートリアルで遊んでみます。たぶん初学者~入門者向けだと思います。
あとはベクトル同士の足し算、ベクトルとスカラーの掛け算などが分かれば大丈夫だと思います。

このリンク先に、OpenSiv3Dを用いて書かれたシューティングゲームのサンプルがあります。
シューティングゲームにおける基礎の部分が、簡潔でスタイリッシュに記述されています。
一般的なシューティングゲームなら、このコードを応用すれば簡単に作成できると思います。

...

弾幕、作りたいですね?
どうしても弾幕をお手軽に作りたい私は、このチュートリアルを改造することにしました。
この際、新たな弾幕中毒者なかまを増やすため、

  • 弾幕に関する処理のみにする
  • Main.cppのみでサクッと書ける
  • わりと複雑な弾幕でも書ける
  • 難しいこと/複雑なことはやらない

といったガイドラインを定めることにします。

#結論
このようになりました。せっかくなので、サンプルとして軽い弾幕も作ってみました。

Main.cpp
# include <Siv3D.hpp>
// Siv3D v0.6.3


// 画面サイズは(800, 600)とする
constexpr uint32_t SceneWidth = 800;
constexpr uint32_t SceneHeight = 600;

// 敵の位置を作成する関数
Vec2 GenerateEnemy()
{
	// 適当に中心あたりに出すだけなので...
	return Vec2(SceneWidth / 2, SceneHeight / 3);
}

// 座標がシーン内にあるか判定する関数
bool isOutOfSceneArea(const Vec2& position)
{
	constexpr int margin = 20;// すこし余裕を持たせる
	return position.x < -margin || position.x > SceneWidth + margin || position.y < -margin || position.y > SceneHeight + margin;
}

// 弾
struct Bullet
{
	Bullet()
		: pos(0, 0)
		, vel(0, 0)
		, acc(0, 0)
		, size(1)
		, stopwatch(StartImmediately::Yes)
	{}

	Bullet(const Vec2& _pos, const Vec2& _vel, const Vec2& _acc, float _size)
		: pos(_pos)
		, vel(_vel)
		, acc(_acc)
		, size(_size)
		, stopwatch(StartImmediately::Yes)
	{}

	Vec2 pos;		// 座標
	Vec2 vel;		// 速度
	Vec2 acc;		// 加速度
	double size;	// 大きさ

	Stopwatch stopwatch;// 経過時間

	void update()
	{
		vel += acc * Scene::DeltaTime();
		pos += vel * Scene::DeltaTime();
	}
};

// 弾幕
class BulletCurtain
{
public:

	BulletCurtain()
	{
		// 弾幕全体の周期を決定
		mWholePeriod = 10000; // 10000ms = 10s

		// ここで種類ごとの弾を設定
		mBulletMap.emplace(eSpin, Array<Bullet>());
		mBulletMap.emplace(eTail, Array<Bullet>());
		mBulletMap.emplace(eSnow, Array<Bullet>());
	}

	// 全弾削除してリセット
	void clear()
	{
		mStopWatch.reset();
		for (auto& bullets : mBulletMap)
			bullets.second.clear();
	}

	void start()
	{
		mStopWatch.start();
	}

	void pause()
	{
		mStopWatch.pause();
	}

	void update([[maybe_unused]] const Vec2& myshipPos, [[maybe_unused]] const Vec2& enemyPos)
	{
		// 今止まってるなら更新しない
		if (mStopWatch.isPaused())
			return;

		// イベント更新
		updateEvents(myshipPos, enemyPos);

		// 弾更新
		updateBullets(myshipPos, enemyPos);
		
		// 弾削除
		eraseBullets(myshipPos, enemyPos);

		// 弾幕全体が終了したらストップウォッチをリセットして再スタート
		if (mStopWatch.ms() >= mWholePeriod)
		{
			mStopWatch.reset();
			mStopWatch.start();
		}
		
	}

	void draw() const
	{
		for (const auto& b : mBulletMap.at(eSnow))
			Circle{ b.pos, b.size }.draw(Palette::White);

		for (const auto& b : mBulletMap.at(eTail))
			Circle{ b.pos, b.size }.draw(Palette::Hotpink);

		for (const auto& b : mBulletMap.at(eSpin))
			Circle{ b.pos, b.size }.draw(Palette::White);
	}

	bool checkHit(const Vec2& pos, const double size)
	{
		// 弾が(enemybullet の size + playerPos の playerHitSize) ピクセル以内に接近したら
		for (const auto& enemyBullets : mBulletMap)
			for (const auto& enemyBullet : enemyBullets.second)
				if (enemyBullet.pos.distanceFrom(pos) <= enemyBullet.size + size)
					return true;
		
		return false;
	}

	const HashTable<int, Array<Bullet>>& getBullets()
	{
		return mBulletMap;
	}

private:

	void updateEvents([[maybe_unused]] const Vec2& myshipPos, [[maybe_unused]] const Vec2& enemyPos)
	{
		if (triggerMs(500))// 0.5s
		{
			constexpr int perNum = 5;

			for (int i = 0; i < perNum; ++i)
			{
				const double angle = 2 * Math::Pi / perNum * i;
				mBulletMap[eSpin].emplace_back(Bullet(enemyPos, 120.f * Vec2(cos(angle), sin(angle)), Vec2::Zero(), 10));
			}
		}

		if (passedMs(700) && !passedMs(9000) && periodMs(150))// from 0.7s to 9s per 0.15s
		{
			constexpr double speed = 1.35;

			for (const auto& spinBullet : mBulletMap[eSpin])
			{
				mBulletMap[eTail].emplace_back(Bullet(spinBullet.pos, -speed * spinBullet.vel.rotated(-Math::Pi / 2), Vec2::Zero(), 7));
			}
		}

		if (periodMs(1000 + RandomInt32() % 150))// per 1s ~ 1.5s
		{
			constexpr int perNum = 3;
			for (int i = 0; i < perNum; ++i)
			{
				const double genPos = Random() * 500.0 - 250.0;
				const double speed = Random() * 15.0 + 10.0;
				mBulletMap[eSnow].emplace_back(Bullet(Vec2(genPos, -genPos), Vec2::Zero(), Vec2(speed, 1.2 * speed), 5));
			}
		}

		if (triggerMs(9000))
		{
			for (auto& spinBullet : mBulletMap[eSpin])
			{
				spinBullet.acc = 3.0 * spinBullet.vel;
			}
		}
	}

	void updateBullets([[maybe_unused]] const Vec2& myshipPos, [[maybe_unused]] const Vec2& enemyPos)
	{
		// 弾の種別ごとに更新処理を書く
		for (auto& b : mBulletMap[eSpin])
		{
			b.vel.rotate(Math::Pi / 150);
			b.update();
		}

		for (auto& b : mBulletMap[eTail])
		{
			if (b.stopwatch.ms() < 3000)// 出てから3s以内の弾のみ
				b.vel.rotate(Math::Pi / 270);
			else
				b.vel.rotate(Math::Pi / 450);

			b.update();
		}

		for (auto& b : mBulletMap[eSnow])
		{
			b.vel.rotate(-Math::Pi / 2200);
			b.update();
		}
	}

	void eraseBullets([[maybe_unused]] const Vec2& myshipPos, [[maybe_unused]] const Vec2& enemyPos)
	{
		mBulletMap.at(eSpin).remove_if([](const Bullet& b) { return (isOutOfSceneArea(b.pos)); });
		mBulletMap.at(eTail).remove_if([](const Bullet& b) { return (isOutOfSceneArea(b.pos)); });
		mBulletMap.at(eSnow).remove_if([](const Bullet& b)// この弾は上と左で消えてはいけない
			{
				constexpr int margin = 20;
				return b.pos.x > SceneWidth + margin || b.pos.y > SceneHeight + margin;
			});

	}


	// 時点の差がdeltatime内に収まっている = だいたい今か次の更新内でその時点を過ぎる
	bool triggerMs(int32 triggerTimePoint)
	{
		// 少し余裕を持たせている
		return abs(mStopWatch.ms() - triggerTimePoint) <= Scene::DeltaTime() * 1000.0 / 1.7;
	}

	// 周期ver. 
	bool periodMs(int32 period)
	{
		const int32 now = mStopWatch.ms();
		// -deltatime / 2 <= now % period - period <= deitatime / 2ならその周期(少し余裕を持たせている)
		const double deltams = Scene::DeltaTime() * 1000.0 / 1.5;
		return abs(now % period - period) <= deltams || now % period <= deltams;
	}

	// 時点経過ver.
	bool passedMs(int32 timePoint)
	{
		return mStopWatch.ms() >= timePoint;
	}

	// BulletMapのキー
	enum BulletKey
	{
		eSpin = 0,
		eTail = 1,
		eSnow = 2,
	};

	// 敵弾
	HashTable<int, Array<Bullet>> mBulletMap;

	// 時間
	Stopwatch mStopWatch;

	// 全体としての周期
	int32 mWholePeriod;
};

void Main()
{
	Scene::Resize(SceneWidth, SceneHeight);

	while (System::Update())
	{
		if (Scene::Time() > 1)
			break;
	}

	// 背景色
	Scene::SetBackground(ColorF{ 0.1, 0.2, 0.7 });

	// 自機テクスチャ
	const Texture playerTexture{ U"🤖"_emoji };
	// 敵テクスチャ
	const Texture enemyTexture{ U"👾"_emoji };

	// 自機
	Vec2 playerPos{ SceneWidth / 2, SceneHeight / 5 * 4 };
	// 敵
	Vec2 enemy = GenerateEnemy();

	// 自機のスピード
	constexpr double playerSpeed = 450.0;
	// 自機の当たり判定の大きさ
	constexpr double playerHitSize = 4.0;

	// 自機ショットのクールタイム(秒)
	constexpr double playerShotCoolTime = 0.1;

	// エフェクト
	Effect effect;

	// 弾幕作成
	BulletCurtain bulletCurtain;
	// 開始
	bulletCurtain.start();

	while (System::Update())
	{
		// ゲームオーバー判定
		bool gameover = false;

		const double deltaTime = Scene::DeltaTime();

		//-------------------
		//
		// 移動
		//

		// 自機の移動
		const Vec2 move = Vec2{ (KeyRight.pressed() - KeyLeft.pressed()), (KeyDown.pressed() - KeyUp.pressed()) }
		.setLength(deltaTime * playerSpeed * (KeyShift.pressed() ? 0.5 : 1.0));
		playerPos.moveBy(move).clamp(Scene::Rect());

		if (KeyP.pressed())
			bulletCurtain.pause();
		if (KeyEnter.pressed())
			bulletCurtain.start();

		bulletCurtain.update(playerPos, enemy);

		//-------------------
		//
		// 攻撃判定
		//

		// 敵ショット vs 自機
		if (bulletCurtain.checkHit(playerPos, playerHitSize))
		{
			//爆発エフェクトを追加
			effect.add([pos = playerPos](double t)
			{
				const double t2 = (1.0 - t);
				Circle{ pos, 10 + t * 70 }.drawFrame(20 * t2, AlphaF(t2 * 0.5));
				return (t < 1.0);
			});

			gameover = true;
		}
	
		// ゲームオーバーならリセット
		if (gameover)
		{
			playerPos = Vec2{ SceneWidth / 2, SceneHeight / 5 * 4 };
			bulletCurtain.clear();
			bulletCurtain.start();
		}

		//-------------------
		//
		// 描画
		//

		// 背景のアニメーション
		for (auto i : step(12))
		{
			const double a = Periodic::Sine0_1(2s, Scene::Time() - (2.0 / 12 * i));
			Rect{ 0, (i * 50), 800, 50 }.draw(ColorF(1.0, a * 0.2));
		}

		// 自機の描画
		playerTexture.resized(80).flipped().drawAt(playerPos);

		// 当たり判定の描画(低速時のみ)
		if (KeyShift.pressed())
			Circle{ playerPos, playerHitSize }.draw(Palette::Red);

		// 敵の描画
		enemyTexture.resized(60).drawAt(enemy);
		
		// 弾幕の描画
		bulletCurtain.draw();

		effect.update();

	}
}

OpenSiv3Dが動く環境でペタッと貼り付けて実行すると、こんな感じになると思います。
screenshot.png

どの部分が何をやっているのか説明していきます。

##弾を表す構造体

//弾
struct Bullet
{
	Bullet()
		: pos(0, 0)
		, vel(0, 0)
		, acc(0, 0)
		, size(1)
		, stopwatch(StartImmediately::Yes)
	{}

	Bullet(const Vec2& _pos, const Vec2& _vel, const Vec2& _acc, float _size)
		: pos(_pos)
		, vel(_vel)
		, acc(_acc)
		, size(_size)
		, stopwatch(StartImmediately::Yes)
	{}

	Vec2 pos;		// 座標
	Vec2 vel;		// 速度
	Vec2 acc;		// 加速度
	double size;	// 大きさ

	Stopwatch stopwatch;// 経過時間

	void update()
	{
		vel += acc * Scene::DeltaTime();
		pos += vel * Scene::DeltaTime();
	}
};

この記事では、単に型の集まりの場合は構造体、しっかり振る舞いがある場合はクラスというように分けています。また、めんどくさい前述の違いを表現するため、ここでは構造体にはアクセス制限をつけないことにしています。

座標、速度、加速度をVec2で、弾の大きさをfloatで、生成されてから経過した時間をStopWatchで表現します。update関数で1deltaTimeぶんの更新を行います。
deltaTimeについてよく知らない場合はこのリンク先を参照してください。特に、細かい弾幕を作るときなどはベタな調整が多いと思うので、ちゃんと知っておかないと結構まずいことになります。(1敗)
stopwatchの(StartImmediately::Yes)は、ストップウォッチを構築すると同時にカウントを開始したい場合に指定する引数です。オブジェクトにストップウォッチを持たせるときはこういうことが多いと思うので、便利です(私も知りませんでした)

##弾幕を表すクラス

// 弾幕
class BulletCurtain
{
public:

	BulletCurtain()
	{
		// 弾幕全体の周期を決定
		mWholePeriod = 10000; // 10000ms = 10s

		// ここで種類ごとの弾を設定
		mBulletMap.emplace(eSpin, Array<Bullet>());
		mBulletMap.emplace(eTail, Array<Bullet>());
		mBulletMap.emplace(eSnow, Array<Bullet>());
	}

	// 全弾削除してリセット
	void clear()
	{
		mStopWatch.reset();
		for (auto& bullets : mBulletMap)
			bullets.second.clear();
	}

	void start()
	{
		mStopWatch.start();
	}

	void pause()
	{
		mStopWatch.pause();
	}

	void update([[maybe_unused]] const Vec2& myshipPos, [[maybe_unused]] const Vec2& enemyPos)
	{
		// 今止まってるなら更新しない
		if (mStopWatch.isPaused())
			return;

		// イベント更新
		updateEvents(myshipPos, enemyPos);

		// 弾更新
		updateBullets(myshipPos, enemyPos);
		
		// 弾削除
		eraseBullets(myshipPos, enemyPos);

		// 弾幕全体が終了したらストップウォッチをリセットして再スタート
		if (mStopWatch.ms() >= mWholePeriod)
		{
			mStopWatch.reset();
			mStopWatch.start();
		}
		
	}

	void draw() const
	{
		for (const auto& b : mBulletMap.at(eSnow))
			Circle{ b.pos, b.size }.draw(Palette::White);

		for (const auto& b : mBulletMap.at(eTail))
			Circle{ b.pos, b.size }.draw(Palette::Hotpink);

		for (const auto& b : mBulletMap.at(eSpin))
			Circle{ b.pos, b.size }.draw(Palette::White);
	}

	bool checkHit(const Vec2& pos, const double size)
	{
		// 弾が(enemybullet の size + playerPos の playerHitSize) ピクセル以内に接近したら
		for (const auto& enemyBullets : mBulletMap)
			for (const auto& enemyBullet : enemyBullets.second)
				if (enemyBullet.pos.distanceFrom(pos) <= enemyBullet.size + size)
					return true;
		
		return false;
	}

	const HashTable<int, Array<Bullet>>& getBullets()
	{
		return mBulletMap;
	}

private:

	void updateEvents([[maybe_unused]] const Vec2& myshipPos, [[maybe_unused]] const Vec2& enemyPos)
	{
		if (triggerMs(500))// 0.5s
		{
			constexpr int perNum = 5;

			for (int i = 0; i < perNum; ++i)
			{
				const double angle = 2 * Math::Pi / perNum * i;
				mBulletMap[eSpin].emplace_back(Bullet(enemyPos, 120.f * Vec2(cos(angle), sin(angle)), Vec2::Zero(), 10));
			}
		}

		if (passedMs(700) && !passedMs(9000) && periodMs(150))// from 0.7s to 9s per 0.15s
		{
			constexpr double speed = 1.35;

			for (const auto& spinBullet : mBulletMap[eSpin])
			{
				mBulletMap[eTail].emplace_back(Bullet(spinBullet.pos, -speed * spinBullet.vel.rotated(-Math::Pi / 2), Vec2::Zero(), 7));
			}
		}

		if (periodMs(1000 + RandomInt32() % 150))// per 1s ~ 1.5s
		{
			constexpr int perNum = 3;
			for (int i = 0; i < perNum; ++i)
			{
				const double genPos = Random() * 500.0 - 250.0;
				const double speed = Random() * 15.0 + 10.0;
				mBulletMap[eSnow].emplace_back(Bullet(Vec2(genPos, -genPos), Vec2::Zero(), Vec2(speed, 1.2 * speed), 5));
			}
		}

		if (triggerMs(9000))
		{
			for (auto& spinBullet : mBulletMap[eSpin])
			{
				spinBullet.acc = 3.0 * spinBullet.vel;
			}
		}
	}

	void updateBullets([[maybe_unused]] const Vec2& myshipPos, [[maybe_unused]] const Vec2& enemyPos)
	{
		// 弾の種別ごとに更新処理を書く
		for (auto& b : mBulletMap[eSpin])
		{
			b.vel.rotate(Math::Pi / 150);
			b.update();
		}

		for (auto& b : mBulletMap[eTail])
		{
			if (b.stopwatch.ms() < 3000)// 出てから3s以内の弾のみ
				b.vel.rotate(Math::Pi / 270);
			else
				b.vel.rotate(Math::Pi / 450);

			b.update();
		}

		for (auto& b : mBulletMap[eSnow])
		{
			b.vel.rotate(-Math::Pi / 2200);
			b.update();
		}
	}

	void eraseBullets([[maybe_unused]] const Vec2& myshipPos, [[maybe_unused]] const Vec2& enemyPos)
	{
		mBulletMap.at(eSpin).remove_if([](const Bullet& b) { return (isOutOfSceneArea(b.pos)); });
		mBulletMap.at(eTail).remove_if([](const Bullet& b) { return (isOutOfSceneArea(b.pos)); });
		mBulletMap.at(eSnow).remove_if([](const Bullet& b)// この弾は上と左で消えてはいけない
			{
				constexpr int margin = 20;
				return b.pos.x > SceneWidth + margin || b.pos.y > SceneHeight + margin;
			});

	}


	// 時点の差がdeltatime内に収まっている = だいたい今か次の更新内でその時点を過ぎる
	bool triggerMs(int32 triggerTimePoint)
	{
		// 少し余裕を持たせている
		return abs(mStopWatch.ms() - triggerTimePoint) <= Scene::DeltaTime() * 1000.0 / 1.7;
	}

	// 周期ver. 
	bool periodMs(int32 period)
	{
		const int32 now = mStopWatch.ms();
		// -deltatime / 2 <= now % period - period <= deitatime / 2ならその周期(少し余裕を持たせている)
		const double deltams = Scene::DeltaTime() * 1000.0 / 1.5;
		return abs(now % period - period) <= deltams || now % period <= deltams;
	}

	// 時点経過ver.
	bool passedMs(int32 timePoint)
	{
		return mStopWatch.ms() >= timePoint;
	}

	// BulletMapのキー
	enum BulletKey
	{
		eSpin = 0,
		eTail = 1,
		eSnow = 2,
	};

	// 敵弾
	HashTable<int, Array<Bullet>> mBulletMap;

	// 時間
	Stopwatch mStopWatch;

	// 全体としての周期
	int32 mWholePeriod;
};

各所で使っている定数は、可能な限りconstexpr(付けるとコンパイル時定数になる)やconst修飾を付けるようにしています。
弾幕のデータ構造を、弾の種類ごとに分けたArrayのHashTableとして表現しています。これにより、新たな種類の弾の追加/削除が楽に行えます。
Arrayは、OpenSiv3Dで用意されている、長さを変えられる配列です。std::vectorと同じように扱えます。
HashTableは、OpenSiv3Dで用意されている、自分で決めた型の値を添え字にして、配列と同じような書き方でアクセスできるデータ構造です。ハッシュ配列、連想配列と呼ばれるアレです(C++にはmapがありますが...)。std::unordered_mapと同じように扱えます。

unordered_mapについて詳しく 内部構造、名前の意味、mapとの違いなどは省略しますので、知りたい方は[信頼できるサイト](https://cpprefjp.github.io/reference/unordered_map/unordered_map.html)などを是非見てください。
ここではキーをenum(=型はint)にしています。
  1. enumに弾の種類を表すキーを追加し、
  2. コンストラクタでHashTableにキーと対応するArrayを追加し、
  3. 種類ごとに生成処理や更新処理を書いていく

といった流れで新たな種類の弾を追加します。

また、update関数内で、いくつかのprivateメンバ関数によって処理を分けています。
updateEvents関数では、経過した時間に従って起きるイベント(弾の生成など)を書きます。
ミリ秒単位で、それぞれ時点を過ぎたか調べるpassedMs関数、周期が来たか調べるperiodMs関数、今が時点を過ぎるフレームかどうか調べるtriggerMs関数を使って、タイムラインを管理しています。
updateBullets関数では、弾の種類に応じた更新処理を書きます。
eraseBullets関数では、画面外に出たりして不要になった弾を消します。
update関数の最後の部分で、弾幕全体の周期を管理し、時間が過ぎていたら最初に戻します。

draw関数では、弾の描画処理を行っています。弾の種類ごとに描画処理をどんどん変えたりすると面白いと思います。constメンバ関数にしているため、対象を変更できてしまう[ ]演算子によるアクセスではなく.at()メソッドでアクセスしています。
checkHit関数では、弾と座標の当たり判定を計算します。現在は円形の弾なので、シンプルに距離を測っているのみですが、OpenSiv3Dには豊富な2D衝突検出機能が搭載されているので、大抵の形状の衝突は検出できると思います。
その他の関数は、それぞれ弾幕を止めたり、開始したり、リセットしたりするメンバ関数です。

Q : あなた、難しいことはしないって言いましたよね?
[[maybe_unused]]
これは何ですか? 知りませんが???

A : 申し訳ございませんでした...
(これは属性構文というものです。これを使うと、コンパイラに自分が書いたコードの目的を明確に伝えられます。[[maybe_unused]] ←これは、その引数を関数内で使わない可能性があるということをコンパイラに伝えて、警告を出さないようにしてもらうものです。弾幕を作るとき、自機の座標や敵の座標は使う場合と使わない場合があるため、これを使用してconst参照で渡しています。)

より大規模にするなら もうちょっと関数化したり、弾ごとにインタフェース作って委譲したりするというのも大規模な開発ではアリだと思います。 このコードから発展させていくなら、BulletCurtainクラスをインタフェースにして、いろいろ弾幕を作って敵に持たせていけば本格的な弾幕シューティングも作れると思います。

##Main関数
Main関数はこのようになりました。元のサンプルをかなり大幅に変更しています。改造というより破壊かもしれません。

void Main()
{
	Scene::Resize(SceneWidth, SceneHeight);

	while (System::Update())
	{
		if (Scene::Time() > 1)
			break;
	}

	// 背景色
	Scene::SetBackground(ColorF{ 0.1, 0.2, 0.7 });

	// 自機テクスチャ
	const Texture playerTexture{ U"🤖"_emoji };
	// 敵テクスチャ
	const Texture enemyTexture{ U"👾"_emoji };

	// 自機
	Vec2 playerPos{ SceneWidth / 2, SceneHeight / 5 * 4 };
	// 敵
	Vec2 enemy = GenerateEnemy();

	// 自機のスピード
	constexpr double playerSpeed = 450.0;
	// 自機の当たり判定の大きさ
	constexpr double playerHitSize = 4.0;

	// 自機ショットのクールタイム(秒)
	constexpr double playerShotCoolTime = 0.1;

	// エフェクト
	Effect effect;

	// 弾幕作成
	BulletCurtain bulletCurtain;
	// 開始
	bulletCurtain.start();

	while (System::Update())
	{
		// ゲームオーバー判定
		bool gameover = false;

		const double deltaTime = Scene::DeltaTime();

		//-------------------
		//
		// 移動
		//

		// 自機の移動
		const Vec2 move = Vec2{ (KeyRight.pressed() - KeyLeft.pressed()), (KeyDown.pressed() - KeyUp.pressed()) }
		.setLength(deltaTime * playerSpeed * (KeyShift.pressed() ? 0.5 : 1.0));
		playerPos.moveBy(move).clamp(Scene::Rect());

		if (KeyP.pressed())
			bulletCurtain.pause();
		if (KeyEnter.pressed())
			bulletCurtain.start();

		bulletCurtain.update(playerPos, enemy);

		//-------------------
		//
		// 攻撃判定
		//

		// 敵ショット vs 自機
		if (bulletCurtain.checkHit(playerPos, playerHitSize))
		{
			//爆発エフェクトを追加
			effect.add([pos = playerPos](double t)
			{
				const double t2 = (1.0 - t);
				Circle{ pos, 10 + t * 70 }.drawFrame(20 * t2, AlphaF(t2 * 0.5));
				return (t < 1.0);
			});

			gameover = true;
		}
	
		// ゲームオーバーならリセット
		if (gameover)
		{
			playerPos = Vec2{ SceneWidth / 2, SceneHeight / 5 * 4 };
			bulletCurtain.clear();
			bulletCurtain.start();
		}

		//-------------------
		//
		// 描画
		//

		// 背景のアニメーション
		for (auto i : step(12))
		{
			const double a = Periodic::Sine0_1(2s, Scene::Time() - (2.0 / 12 * i));
			Rect{ 0, (i * 50), 800, 50 }.draw(ColorF(1.0, a * 0.2));
		}

		// 自機の描画
		playerTexture.resized(80).flipped().drawAt(playerPos);

		// 当たり判定の描画(低速時のみ)
		if (KeyShift.pressed())
			Circle{ playerPos, playerHitSize }.draw(Palette::Red);

		// 敵の描画
		enemyTexture.resized(60).drawAt(enemy);
		
		// 弾幕の描画
		bulletCurtain.draw();

		effect.update();

	}
}

自機が動いて表示される部分以外をほとんど取り除きました。
代わりに(?)自機の当たり判定を低速時のみ表示しています。
また、元のサンプルでは敵に命中させたときに発生していたエフェクトを、自機が被弾した場合のエフェクトに流用しています。

#おわりに
この記事を見てくださっている方々も、ふと「こんな弾幕あったらいいな」と考えることがあると思います。ありますよね?
この記事のコードを使って、OpenSiv3Dでお手軽に弾幕を作っていただけると嬉しいです。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?