プロジェクトシートK(仮)

技術の吐き溜めなどなど

【10】Siv3D シーン遷移によるタイトル画面の作成と、ポーズ機能の実装をする

ゲームに必要不可欠である、タイトル画面を実装してみます。ゲームを起動して間髪入れずにゲーム始まってしまうと、拍子抜けするというか、いろいろと物足りないし理不尽です。そこで、

タイトル画面でアクションを起こす(ゲームスタートキーを押す) → ゲームが始まる

のような、シーン遷移をさせましょう。このようなシーン遷移を実装するのに普通はかなり手間が掛かってしまいますが、それが標準で用意されているのがSiv3Dです。厳密にいうならば、Siv3Dに付属しているフレームワークであるHamFrameworkをインクルードし、ham::SceneManagerを使うことで比較的簡単にシーン遷移をさせることができます。

参考記事:
Siv3Dでのシーン遷移をサポートするSceneManagerの使い方とその応用 - Qiita


シーン遷移のための準備

シーンの管理をするために、HamFramework.hppインクルードしましょう。そしてシーン管理の為のSceneManagerを任意の名前で作成します。シーンは文字列数値などで管理でき、それをテンプレートの型とします。例えばタイトル画面のシーンは"Title"、ゲームのシーンは"Game"のような感じです。

そしてシーンを作成するために、シーンの基底クラスSceneManager::Sceneを継承してシーンを定義をします。基底クラスにはinit(), update(), draw()が定義されており、これらをoverrideして定義していきます。

init 関数では,シーンを切り替えた時に一度だけ呼ばれます(省略可).update 関数と draw 関数は毎フレーム,update,draw の順番で呼ばれます.update と draw の主な違いとしては,update が普通のメンバ関数であるのに対し,draw は const メンバ関数であることです.つまり,draw 関数内でメンバ変数の値の変更はできません.

引用:Siv3Dでのシーン遷移をサポートするSceneManagerの使い方とその応用 - Qiita

# include <Siv3D.hpp>
# include <HamFramework.hpp>

class Scene1 : public SceneManager<String>::Scene
{
public:
    void init() override
    {
        //シーン遷移時、一度だけ実行される
    }

    void update() override
    {
        //毎サイクル呼び出される
    }

    void draw() const override
    {
        //毎サイクル呼び出される、update()の次に処理されconstメンバ関数である
    }
};

void Main()
{
    SceneManager<String> manager;
    manager.add<Scene1>(L"Scene1");

    while (System::Update())
    {
        manager.updateAndDraw();
    }
}

ここまで準備できれば、あとはシーン毎に処理を記述してあげればオッケーです。今までMain関数内に記述してきたものをシーン単位でメンバ変数、init, update, draw関数に移植してあげましょう。


移植についてですが、Main()に入ってからWhile(System::Update())部に入るまでに記述していた部分はクラスのメンバ変数に、メンバ変数の初期化はinit関数に、While(System::Update())部のシステム的な処理をしている部分をupdate関数内に、描写の処理はdraw関数内に移植してやると綺麗な作りになると思いました。

ポーズ機能の実装

ゲームをやっている途中に電話が掛かってきたり来客が来たりすれば、当然ゲームを中断することになります。シューティングゲームのような少しでも目を離すとやられてしまうようなゲームでは、ポーズ機能というものが重要になってきます。STARTボタンを押すとゲームが一時的に中断され、ゲームを続行するかやめるか選択できるといった機能ですね。こいつを実装してみましょう。

発想としては分かりやすいもので、ポーズをしているかしていないかの変数を用意し、ポーズをしていればシステムの処理をせず、ポーズをしていなければ通常通りシステムの処理を行えばよいです。ということで、ポーズ判断用の変数isPauseを用意しましょう。

それではあるキーがクリックされればポーズの状態を変えるようにしてみましょう。例えばSpaceキーが押されればポーズが作動・解除されるようにします。これは、update関数の最初にこのように記述してあげればよいですね。そして、ポーズが押されていればシステム的な処理をしてはいけないので、各種処理がされる前にupdate関数を早期リターンさせちゃいましょう。

if (Input::KeySpace.clicked) {
	isPause = !isPause;
}
if (isPause) return;

ここで気を付けたいのが、ポーズをしている時、update関数内に描写の処理を入れてしまうとその処理までされなくなってしまうということです。先ほどupdate関数にはシステム的な処理を、draw関数には描写の処理を記述すると綺麗な作りになると書きましたが、このような機能を実装する際にモロ響いてきます。update関数が早々に終了しても、drawで記述をしてやれば動作に違和感がなくなります。updateとdrawを明確に使い分けるとこのような利点が出てきます。

コード

# include <Siv3D.hpp>
# include <HamFramework.hpp>
struct MyCircle {
private:
	int radius;
	Vec2 place;
	int color;
	Vec2 velocity = Vec2(0, 0);
public:
	MyCircle(int r, Vec2 p, int c) {
		radius = r; place = p; color = c;
	}
	void setRadius(int r) { radius = r; }
	void setPlace(Vec2 p) { place += p; }
	void setColor(int c) { color = c; }
	void setVelocity(Vec2 v) { velocity = v; }
	int getRadius() const { return radius; }
	Vec2 getPlace() const { return place; }
	int getColor() const { return color; }
	bool isVisible() const { return InRange(place.x,-10.0,650.0) && InRange(place.y,-10.0,490.0);}
	Vec2 getVelocity() const { return velocity; }
};

struct Player {
private:
	Vec2 place;
	int radius;
	int hp = 3;
	int shotFlg = 2;
public:
	Player(int r, Vec2 p) {
		radius = r; place = p;
	}
	void setRadius(int r) { radius = r; }
	void setPlace(Vec2 p) {
		place += p; 
		place.x = Clamp(place.x, 10.0, 630.0);
		place.y = Clamp(place.y, 10.0, 470.0);
	}
	void hit() { --hp; if (hp == 0) { place = Vec2(1000, 1000); } }
	int getRadius() const{ return radius; }
	Vec2 getPlace() const{ return place; }
	int getHP() const{ return hp; }
	bool shot() {
		++shotFlg;
		if (shotFlg == 3) {
			shotFlg = 0;
			return true;
		}
		return false;
	}
};

struct Enemy {
private:
	Vec2 place;
	int radius;
	int hp = 300;
	int dir = 0;
public:
	Enemy(int r, Vec2 p) {
		radius = r; place = p;
	}
	void setRadius(int r) { radius = r; }
	void setPlace(void) {
			place.x += dir;
		if(place.x < 40 || place.x > 580) {
			dir = -dir;
		}
	}
	void hit(void) {
		--hp;
		if (hp == 200) { dir = 2; }
		if (hp == 100) { dir = dir * 3; }
		if (hp == 0) { place = Vec2(2000, 2000); } 
	}
	int getRadius(void) const { return radius; }
	Vec2 getPlace(void) const { return place; }
	int getHP(void) const { return hp; }
};

class Title : public SceneManager<String>::Scene
{
	const Texture texture = Texture(L"Example/texture.png"); //背景テクスチャ
	int selected = 0;
	const Font font1 = Font(40, L"よもぎフォント");
	const Font font2 = Font(40, L"よもぎフォント");
	const Font fontSelected = Font(40);
	const Array<String> texts =
	{
		L"GameStart",
		L"Exit",
	};
	
	void init() override {
		if (!FontManager::Register(L"Example/YomogiFont.ttf"))
		{
			// 失敗したら終了
			return;
		}
	}

	void update() override {
		if (Input::KeyUp.clicked || Input::KeyDown.clicked) {
			selected += 1;
			selected = selected % 2;
		}
		if (Input::KeyS.clicked) {
			switch (selected) {
			case 0:
				changeScene(L"Game");
				break;
			case 1:
				System::Exit();
				break;
			default:break;
			}
		}
	}
	void draw() const override {
		texture.map(640, 480).draw();

		int i = 0;
		for (auto& text : texts) {
			const bool isSelected = selected == i;
			if (isSelected) {
				const int32 w = font1(text).region().w / 2;
				font1(text).draw(320 - w, 150 + i * font1.height, Palette::Red);
				fontSelected(L"▶").draw(320 - w - 60, 150 + i*font1.height, Palette::Red);
			}
			else {
				const int32 w = font2(text).region().w / 2;
				font2(text).draw(320 - w, 150 + i * font2.height, Palette::White);
			}
			++i;
		}
	}
};


class Game : public SceneManager<String>::Scene
{
	Array<MyCircle> array;
	Array<Vec2> shots;
	int radius = 10;
	int count = 0;
	Vec2 spawn = Vec2(340, 60);
	int speed = 5;
	int sceneChangeCount = 100;

	Player player = Player(20, Vec2(320, 400));
	Triangle playerView = Triangle(player.getPlace(), player.getRadius());
	Enemy enemy = Enemy(40, spawn);

	const Rect frame = Rect(10, 10, 620, 460); //枠
	const Texture texture = Texture(L"Example/texture.png"); //背景テクスチャ
	const Texture star = Texture(L"Example/star.png"); //球テクスチャ
	const Texture chara = Texture(L"Example/Enemy.png"); //敵テクスチャ

	Font font = Font(50);
	bool isPause = false;
	String pause = L"PAUSE";

	void update() override {

		if (Input::KeySpace.clicked) {
			isPause = !isPause;
		}
		if (isPause) return;

		//敵
		enemy.setPlace();
		Circle ene(enemy.getPlace(), 100); //敵の当たり判定用

		//球の発生
		MyCircle cir(radius, enemy.getPlace(), 100);
		cir.setVelocity(Vec2(-cos(count / Pi), sin(count / Pi)).setLength(speed));
		array.push_back(cir);

		//プレイヤー処理
		if (player.getHP() != 0) {
			//キーボード入力部
			Vec2 direction(Input::KeyRight.pressed - Input::KeyLeft.pressed, Input::KeyDown.pressed - Input::KeyUp.pressed);
			if (!direction.isZero())
				player.setPlace(direction.setLength(5));
			//Sキーでショット
			if (Input::KeyS.pressed) {
				if (player.shot()) {
					shots.push_back(player.getPlace());
				}
			}
		}
		playerView = Triangle(player.getPlace(), player.getRadius());

		//自分が出したショットの処理
		for (auto& shot : shots) {
			if (shot.intersects(ene)) {
				enemy.hit();
				shot = Vec2(-1000, -1000);
			}
			shot += Vec2(0, -10);
		}
		Erase_if(shots, [](const Vec2& s) { return s.y < -10.0; });

		//オブジェクトの描写・セット・当たり判定チェック
		for (auto& object : array) {
			//場所のセット
			object.setPlace(object.getVelocity());
			//オブジェクトの定義・描写
			Circle c(object.getPlace(), object.getRadius());
			 //当たり判定チェック
			if (playerView.intersects(c)) {
				object.setPlace(Vec2(1000, 1000));
				player.hit();
			}
		}

		//オブジェクトの削除
		Erase_if(array, [](const MyCircle& c) { return !c.isVisible(); });

		count++;
		count = count % 180;

		if (player.getHP() == 0 || enemy.getHP() == 0) {
			--sceneChangeCount;
		}
		if (sceneChangeCount == 0) changeScene(L"Title");
	}

	void draw() const override {
		//背景の描写
		texture.draw(10, 10);
		//敵の描写
		chara.draw(enemy.getPlace() + Vec2(-50, -60));
		//プレイヤーの描写
		playerView.draw(Palette::Red);
		//自分が出したショットの描写
		for (auto& shot : shots) {
			Circle c(shot, 4);
			c.draw(Palette::Gray);
		}
		//オブジェクトの描写
		for (auto& object : array) {
			star.draw(object.getPlace() - Vec2(30, 30)); //テクスチャ表示
		}
		//敵HPゲージ
		Rect(410, 18, 200, 10).draw(Palette::Gray);
		Rect(410, 18, enemy.getHP() * 200 / 300, 10).draw(Palette::Red);
		//自機の数
		int hp = player.getHP();
		for (auto i = 0; i < hp; ++i) {
			PutText(L"★").from(15 + i * 20, 12);
		}
		//枠の表示
		frame.drawFrame(2, 0, Palette::Yellow);
		frame.drawFrame(0, 10, Palette::Black);
		//ポーズ表示
		if (isPause) {
			Rect(10, 200, 620, 100).draw(Palette::Black);
			const int w = font(pause).region().w / 2;
			font(pause).draw(320 - w, 200, Palette::White);
		}
	}
};

void Main()
{
	SceneManager<String> manager;

	manager.add<Title>(L"Title");
	manager.add<Game>(L"Game");

	while (System::Update())
	{
		manager.updateAndDraw();
	}
}

・シーンはタイトル画面と、ゲームそのものの2つ

・シーンの遷移をするにはchangeScene("シーンの名前")を実行します

・タイトル画面でGameStartを選択するとゲームシーンに遷移

・ゲームシーンで自機が無くなるか、敵のHPが無くなれば、タイトルシーンに遷移

・ポーズがされているとき、専用のポーズエフェクトを描写

www.youtube.com