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

技術の吐き溜めなどなど

【11】Siv3D スコアの実装と、それをシーン間でデータ共有する

ゲームに欠かせない要素がスコアです。ゲームをクリアしていく過程で、特定のアイテムをゲットしたり時間に応じて得点が加算されれば、かなり競技性が増すでしょう。今まで作ってきたシューティングゲームには敵にショットが当たったときのhit処理を既に実装しました。そこにスコア処理を入れてあげれば、スコアシステムができそうです。そこで、その実装例を見てみましょう。


またスコアシステムを実装するのだから、ハイスコアというものをシーンを超えて保存できればなお良いでしょう。古きファミコンソフトなんかでもハイスコアはある程度保存されているかと思います。そこで、シーン間でのデータ共有の方法も同時に実装してみます。


それと、ブログで紹介するにはコードが長くなりすぎているので、細かい解説は省かせていただきます。今回はコード丸々載せますが、次回以降は単体の機能のみを作って紹介したほうがいいですね…許してくだちい。ブログにコードを載せるのでなく、随時GitHubにプルしてきます。

コード

# 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,450.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, 430.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 > 300) {
			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; }
};

struct CommonData {
	int score = 0;
};

using MyApp = SceneManager<String, CommonData>;

class Title : public MyApp::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",
	};
	const Font fontHighScore = Font(20, L"よもぎフォント");
	String 	highScore = L"HIGH_SCORE : ";
	
	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;
		}
		const int32 w = fontHighScore(highScore,m_data->score).region().w;
		fontHighScore(highScore,m_data->score).draw(630 - w, 420, Palette::Yellow);
	}
};


class Game : public MyApp::Scene
{
	Array<MyCircle> objects;
	Array<Vec2> shots;
	int radius = 10;
	int count = 0;
	Vec2 spawn = Vec2(140, 60);
	int speed = 5;
	int sceneChangeCount = 360;

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

	const Rect frame = Rect(10, 10, 420, 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";

	Font fontScore = Font(40);
	Font fontNewRecord = Font(20);
	int64 score = 0;
	String scorePoint = L"SCORE : ";
	String newRecord = L"NewRecord!!";
	bool breakRecord = false;

	void update() override {

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

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

		//球の発生
		MyCircle cir(radius, enemy.getPlace(), 100);
		double rad = (count % 180) / Pi;
		cir.setVelocity(Vec2(-cos(rad), sin(rad)).setLength(speed));
		objects.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);
				score += 1000;
			}
			shot += Vec2(0, -10);
		}
		Erase_if(shots, [](const Vec2& s) { return s.y < -10.0; });

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

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

		count++;

		if (player.getHP() == 0 || enemy.getHP() == 0) {
			--sceneChangeCount;
		}
		if (sceneChangeCount == 300) {
			if(enemy.getHP() == 0)
				score += (3600 - count) * 100 + player.getHP() * 10000;
			if (m_data->score < score) {
				m_data->score = score;
				breakRecord = true;
			}
		}
		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 : objects) {
			star.draw(object.getPlace() - Vec2(30, 30)); //テクスチャ表示
		}
		//敵HPゲージ
		Rect(210, 18, 200, 10).draw(Palette::Gray);
		Rect(210, 18, enemy.getHP() * 200 / 300, 10).draw(Palette::Red);
		//スコア表示
		PutText(L"HIGHSCORE  : ",m_data->score).from(450, 40);
		PutText(L"SCORE      : ", score).from(450, 70);
		//自機の数
		PutText(L"PLAYER     : ").from(450, 100);
		int hp = player.getHP();
		for (auto i = 0; i < hp; ++i) {
			PutText(L"★").from(530 + i * 20, 100);
		}
		//枠の表示
		frame.drawFrame(2, 0, Palette::Yellow);
		frame.drawFrame(0, 210, Palette::Black);
		//最終スコア表示
		if (sceneChangeCount < 240) {
			Rect(10, 200, 620, 100).draw(Palette::Yellow);
			const int w = fontScore(scorePoint,score).region().w / 2;
			fontScore(scorePoint,score).draw(320 - w, 210, Palette::Black);
			if (breakRecord) fontNewRecord(newRecord).draw(320-w,190,HSV(count));
		}
		//ポーズ表示
		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()
{
	MyApp manager;

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

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

簡単な解説

スコア機能

Gameシーン(クラス)にてスコア管理用の変数を以下の通りいくつか定義しました。

Font fontScore = Font(40);
Font fontNewRecord = Font(20);
int64 score = 0;
String scorePoint = L"SCORE : ";
String newRecord = L"NewRecord!!";
bool breakRecord = false;

scoreはプレイヤーが敵に弾が当たれば加算されていきます。敵を倒せば、残り残機と経過時間に応じて追加で得点が入ります。breakRecordはハイスコアか否かを管理するbool型の変数です。記録されているスコアよりも高いスコアを出すことができれば、NewRecordとしてハイスコアが更新されます。ハイスコア機能に関しては以下を参照のこと。

シーン間のデータ共有

SceneManagerのテンプレート第二引数に共有データの為の構造体を指定してやることで、その変数がシーンにかかわらず共有されます。今回はint型のscoreのみを定義した構造体Commondataを定義し、共有データとしました。

struct CommonData {
	int score = 0;
};

ここで指定された共有データは、シーンのクラス内でポインタm_dataを経由してアクセスすることができます。例えば、m_data->countで共有データにアクセスすることができます。

usingを用いた省略形

using MyApp = SceneManager;
の記述をすることで、MyAppSceneManagerとして利用することができます。クラスを継承したりオブジェクトを作る際にいちいち長い記述をするのも非効率のため、このような省略形を使いましょう。

f:id:konkea:20161211230842p:plain

f:id:konkea:20161211230851p:plain

f:id:konkea:20161211230857p:plain


次回は、同一シーン内でのシーン管理を実装してみようと思います!具体的には、ゲーム開始してから30秒後に中ボスが出て、それを倒したら大ボスが出て…みたいなやつです!