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

技術の吐き溜めなどなど

【7】Siv3D 本格的なシューティングゲームを作るための小技集!

こんばんは。12月に入り、このブログの一企画として一人でAdventCalendar25記事を完成させるという馬鹿企画をしてからなんと一週間が経ちました。

経ってしまいました。

当初は3日坊主で終わる感満載だったのですが、気が付けばもう7記事目。今のところすべてがSiv3Dに関する基礎的な内容ではありますが、続いています。このままいけば25記事完走も夢じゃないのかも…!


そんな記念すべき(?)1週間目の内容ですが、一気に本格的なシューティングゲームに近づけたいと思います。シューティングゲームを作るにあたってあると便利だが、ひとつの記事にするには贅沢というような内容をまとめて紹介しようと思います。


読者のみなさんのモチベーションを上げるためにも、今回紹介する内容でここまでできる!といったスクリーンショットを貼ります。なんかそれっぽいシューティングゲームになってるよね…!?
f:id:konkea:20161207225548p:plain


オブジェクトに速度の情報を与える

オブジェクトに速度の情報があると便利です。例えば「あるオブジェクトAは上から下に移動させたいし、あるオブジェクトは左から右に移動させたい。そしてそれらをまとめて処理したい」といったときです。これを実装するのは比較的簡単で、「オブジェクトを表す構造体に速度を表す変数を用意」すればオッケーです!

具体的に、例を紹介します。以下の構造体は、前回までに用いていたものと同じです。
【6】Siv3Dで、シューティングゲームの自機を実装する - プロジェクトシートK(仮)

struct MyCircle {
private:
	int radius;
	Vec2 place;
	int color;
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; }
	int getRadius(void) { return radius; }
	Vec2 getPlace(void) { return place; }
	int getColor(void) { return color; }
	bool deleteCheck(void) { return ((place.x < -20) || (place.x > 660) || (place.y < -20) || (place.y > 500));}

まず、速度をあらわす変数を用意します。速度は「向き大きさ」を持ちます。これは、おなじみのVector型を用いればよいでしょう。速度は英語でvelocityと言うので、これを変数名としましょう。

次に、速度をセットする関数setVelocityと、速度情報を得るgetVelocityそれぞれを定義します。これで、オブジェクト毎に速度を持たせることができます。

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(void) { return radius; }
	Vec2 getPlace(void) { return place; }
	int getColor(void) { return color; }
	bool deleteCheck(void) { return ((place.x < -20) || (place.x > 660) || (place.y < -20) || (place.y > 500));}
	Vec2 getVelocity(void) { return velocity; } //追加

注意したいのが、このオブジェクトを作成した後は必ず速度をセットしなければいけないことです。今回は初期値をVec2(0,0)としたので速度セットしない限りオブジェクトは動きません。

setPlaceについて

小技というか、今後を見据えて関数setPlaceを変更します。今までは引数に与えられた位置をそのまま反映させていましたが、それを「与えられた引数のパラメータ分だけ動かす」こととします。なぜならば、せっかく速度のパラメータを持たせたため、それをそのままsetPlaceの引数として動かせた方が楽なためです。
変更前: void setPlace(Vec2 p) { place = p; }
変更後: void setPlace(Vec2 p) { place += p; }


三角関数(sin,cos)を使いこなす

シューティングゲーム…というかゲーム全般を取り扱うとき、三角関数(というか数学関数全般)は使わざるを得ない状況が多発するでしょう。Siv3Dでは標準で、sin,cos,tanなんかの関数が用意されており、簡単に利用することができます。

三角関数を使う上で気を付けるべきことがあります。それは引数は弧度法によるものではなく、ラジアンで処理されるということです(sinf関数というものもありますが、もしかしたらこれが弧度法によるものかもしれません…調査します)。また、Piという定数がSiv3D側であらかじめ定義されているため、これも上手く使いこなすとよいでしょう。

簡単な例ですが、sin30°の値が取り出したいときは、sin(30*Pi/180)とすればオッケーです。

※2016/12/09追記
コメントにて情報頂きました。Siv3Dには角度をラジアンに変換してくれるユーザ定義リテラルが用意されているようです。例えば30°をラジアンに変換したいときは30_degとすればいいようです。これは便利。数値でしかユーザ定義リテラルは使用できないため、変数の場合はRadians()関数を使えば、同様にラジアンへの変換が出来るようです。

主要パラメータを一括で管理する

オブジェクトの発生位置を数値直打ちでしたり、速度の大きさなんかも直打ちしてしまうとコードの管理が少々複雑になってしまいます。しかも、挙動を変えたいとなれば、いろいろと大変な思いをするでしょう。

そこで、これらのパラメータを一括で管理しちゃいましょう。パラメータ管理用の変数を用意し、その変数を参照して値のセットをします。こうすることでいろいろなメリットが生まれます。例えば…
・オブジェクトの発生位置を動かしたい
・オブジェクトの発生位置と同じ場所に、敵キャラクターを配置したい
・特定のタイミングでオブジェクトの速さを変えるような処理をしたい

などといったことが簡単にできるようになります。

さて今回は、オブジェクトの発生位置を変数spawnオブジェクトの速さをspeedとして定義することにしましょう。

プレイヤーが画面外にいかない様に制御する

現在のプログラムだとプレイヤーが画面の外にまで動けてしまい、そのまま画面外を永遠と旅するような事ができてしまいます。プレイヤーが見えないゲームなんて何やってるのか分からないし、面白くないです(一部の界隈では目隠しプレイなんかもあるけど…なんだかね…)。

そこで、プレイヤーが動ける範囲というものを制限しちゃいましょう。具体的には、このようにします。

//キーボード入力部
Vec2 direction(Input::KeyRight.pressed - Input::KeyLeft.pressed, Input::KeyDown.pressed - Input::KeyUp.pressed);
if (!direction.isZero()) {
	player.moveBy(direction.setLength(5));
	if (player.x < 0)player.x = 0;
	if (player.y < 0)player.y = 0;
	if (player.x > 640)player.x = 640;
	if (player.y > 480)player.y = 480;
}

つまり、画面外にプレイヤーが位置するとき、無理やり画面ギリギリ外側の位置に固定させます。これで、プレイヤーは画面の外に逃げることができなくなります。
 ___もうこの画面からは逃れられない___

※2016/12/09追記
こちらもコメントにて情報頂きました。Siv3Dにて定義されているClamp関数を使うと、もっと簡潔に処理できるみたいです。具体的に、
if (player.x < 10)player.x = 10;
if (player.x > 630)player.x = 630;

player.x = Clamp(player.x, 10.0, 630.0);
と書くことができるようです。こちらの方がまさにスマート!ですね。


画面に枠を付ける

画面の全てがゲーム画面であっても良いのですが、得点や残機などの情報を表示するための場所を確保したい場合があります。東方なんかを想像してもらえると分かりやすいですね。そのために、枠を描写させましょう。

発想としては、まずプレイヤーを動かすことのできる範囲(メインのゲーム画面)に長方形を設定します。その長方形自体は描写せずに、枠だけを描写すればフレームが完成します。枠を描写するための関数drawFrameが、Siv3Dでは標準で用意されています。Siv3D優秀すぎる…(小声)

ここで気を付けるべきなのが描写の順番です。フレームを描写したのはいいものの、そのフレームの上にオブジェクトが描写されてしまうと違和感が出てしまいます。そこで、フレームに関しては最後に描写しましょう。Siv3Dでは後に描写されるものほど手前に描写されます。つまり、フレームの描写よりも前に描写されたものはすべてフレームの後ろに隠れます

背景にテクスチャを設定する

雰囲気を出すために背景を、一色の無地ではなく画像を使ってみましょう。今回用いるのがこちらの画像です。
f:id:konkea:20161208001511p:plain
すごくよくみる画像ですね。Gimpじゃよくお世話になっています
これを、テクスチャとして画面に張り付けてみます。これも、たったの数行で行うことができます。

# include <Siv3D.hpp>

void Main()
{
	//背景の読み込み
	const Texture texture(L"Example/texture.png");

	while (System::Update())
	{
		//背景の描写
		texture.draw(10, 10);
	}
}

texture.draw(10,10)というのは画面座標(10,10)の位置から描写する、という意味です。これに関しても、特に説明はいらないと思います。一応、画像ファイルは本プロジェクトファイル内Exampleフォルダの中に入れてあります。

オブジェクトにテクスチャを設定する

せっかくいい感じの背景にしたのだから、避けるオブジェクトも加工したいですよね。そこで、以下のテクスチャを使ってみることにしました。
f:id:konkea:20161208002350p:plain
夜空を避けていく…なんてロマンチックなのでしょうか(?)

これも先ほどと同じ発想で、オブジェクトが表示されるべき場所に、その上からテクスチャを描写させます。もともと存在していたオブジェクトの描写は、当たり判定を行うために残しておきます。詳細は下の方のコードにて。

ちょっとしたギミックを入れてみる

せっかくspawnという変数を導入したので、これをいじってみることにします。spawnはオブジェクトの発生する位置を表しています。そこで今回は、マウスカーソルの位置とspawnのx座標を同期してみます。こんな動作でも1行で記述することができます。

コード(今回のまとめ)

# include <Siv3D.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(void) { return radius; }
	Vec2 getPlace(void) { return place; }
	int getColor(void) { return color; }
	bool deleteCheck(void) { return ((place.x < -20) || (place.x > 660) || (place.y < -20) || (place.y > 500));}
	Vec2 getVelocity(void) { return velocity; }
};

void Main()
{
	Array<MyCircle> array;
	int radius = 20;
	int count = 0;
	Circle player({ 320,400 }, 10);
	int hitCount = 0;
	Vec2 spawn(340,20);
	int speed = 5;

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

	while (System::Update())
	{
		//背景の表示
		texture.draw(10, 10);

		//マウスカーソルの位置に応じて球の出現位置を変える
		spawn.x = Mouse::Pos().x;

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

		//キーボード入力部
		Vec2 direction(Input::KeyRight.pressed - Input::KeyLeft.pressed, Input::KeyDown.pressed - Input::KeyUp.pressed);
		if (!direction.isZero()) {
			player.moveBy(direction.setLength(5));
			if (player.x < 10)player.x = 10;
			if (player.y < 10)player.y = 10;
			if (player.x > 630)player.x = 630;
			if (player.y > 470)player.y = 470;
		}

		//オブジェクトの描写・セット・当たり判定チェック
		for (auto& object : array) {
			//場所のセット
			object.setPlace(object.getVelocity());
			//オブジェクトの定義・描写
			Circle c(object.getPlace(), object.getRadius());
			c.draw(HSV(object.getColor()));
			star.draw(object.getPlace()-Vec2(30,30)); //テクスチャ表示
			//当たり判定チェック 当たったとき、オブジェクトが消えるように仕向ける
			if (player.intersects(c)) {
				object.setPlace(Vec2(1000,1000));
				hitCount++;
			}
		}

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

		//プレイヤーの描写
		player.draw(Palette::Red);

		PutText(L"HIT回数:", hitCount).from(10, 10);
		count++;
		count = count % 180;

		//枠の表示
		frame.drawFrame(2, 0, Palette::Yellow);
		frame.drawFrame(0, 10, Palette::Black);
	}
}

www.youtube.com