Siv3Dを用いた全方位STGの実装について

Siv3D Advent Calendar 2015 - Qiitaの21日目の記事です。
この記事では、現在制作中の全方位STGの実装を大雑把に解説します。
コードはGitHubにて公開してます。yashihei/Alone · GitHub
記事内のコードは一部解説用に、実際のコードから書き換えてる部分もあります。

どんな感じのゲームなのさ

f:id:yashihei:20151221010427g:plain

Geometry Wars弾幕要素が加わった感じでしょうか。AREA 2048に影響を受けたようにも感じられます。

オブジェクト管理

@language_43に質問された部分なので、最初に書いておきます。
基本的に、各オブジェクト(Enemy、Bulletなど)の基底クラスが次のActorクラスです。

class Actor {
public:
	Actor() = default;
	virtual ~Actor() = default;
	virtual void update() = 0;
	virtual void draw() = 0;

	void kill() { enable = false; }
	bool isEnabled() const { return enable; }
private:
	bool enable = true;
};

敵や、弾は多数出るのが前提なので、それらを管理するManagerクラスが必要になります。
コンテナを持っており、コンテナ内の要素を一斉に更新、描画します。更新するついでに生存フラグが立ってない要素をコンテナから削除します。
また、コンテナには実体を入れずに参照を持たせています。

template<typename Type>
class ActorManager {
public:
	ActorManager() = default;
	virtual ~ActorManager() = default;
    
	void add(std::shared_ptr<Type> actor) {
		actors.push_back(actor);
	}
	void clear() {
		actors.clear();
	}
	int size() {
		return actors.size();
	}
	virtual void update() {
		for (auto& actor : actors) {
			actor->update();
		}
		//生存してない場合、要素を削除
		//Erase_ifはsiv3dのUtilityに含まれている
		Erase_if(actors, [](std::shared_ptr<Type> actor) { return !actor->isEnabled(); });
	}
	virtual void draw() {
		for (auto& actor : actors) {
			actor->draw();
		}
	}
    
	//range based forを使う為、beginとendを定義
	typename std::list<std::shared_ptr<Type>>::const_iterator begin() { return actors.begin(); }
	typename std::list<std::shared_ptr<Type>>::const_iterator end() { return actors.end(); }
private:
	std::list<std::shared_ptr<Type>> actors;
};

ActorManagerを実際に使うと次のようなコードになります。template化が必要だったのは、各クラスのメンバを呼び出したいからです、Actorのポインタで管理するとダウンキャストする必要がありちょいと面倒なので避けたいです。

using BulletManager = ActorManager<Bullet>;
auto bulletManager = std::make_shared<BulletManager>();

//弾発射の際(インスタンス生成→マネージャーに追加)
auto bullet = std::make_shared<Bullet>(pos, Color(255, 100, 100), rad);
bulletManager->add(bullet);

//毎フレームの更新
bulletManager->update();
bulletManager->draw();

//各弾の座標を参照(当たり判定とかで)
for (auto& bullet : *bulletManager) {
	bullet->getPos();
}

自機

今回はGeometry Warsライクな操作を実現したかったので、左スティックで自機の移動を、右スティックでショットを打つようにしました。入力にはXInputを用いてます。
自機の移動をコードに表すと次のようになります。

auto pad = XInput(0);
//デッドゾーンの設定
pad.setLeftThumbDeadZone();
pad.setRightThumbDeadZone();

//左スティックが傾いてる場合
if (!Vec2(pad.leftThumbX, pad.leftThumbY).isZero) {
	//スティックの傾いた方へ自機を進める
	rad = Atan2(-pad.leftThumbY, pad.leftThumbX);
	pos += Vec2(Cos(rad), Sin(rad)) * 7.5;
}
//ステージから出ないように、Clampで座標を制御する
pos = Vec2(Clamp(pos.x, 0.0, 1000.0, Clamp(pos.y, 0.0, 1000.0));

ClampはSiv3DのUtilityの中にあります、HPやプレイヤーの座標など、一定範囲内に収めたい値がある場合、Clampを使うと効率的です。
ユーティリティ - Play Siv3D!

当たり判定

Siv3Dの図形による当たり判定を使っています。基本的には円と、円で見てます。弾がかなり速いゲームとかだと、線分で見る必要が出てくると思います。

void Player::checkBulletHit(Game* game) {
	auto bulletManager = game->getBulletManager();
	for (auto& bullet : *bulletManager) {
		//プレイヤーの当たり判定(座標から半径1.0の円)に敵弾の当たり判定(円)が重なった場合
		if (Circle(pos, 1.0).intersects(Circle(bullet->getPos(), bullet->getSize()))) {
			bullet->kill();
			damage();
		}
	}
}

スクロール

Siv3Dでは2D描画時の変換行列を設定出来るので、オフセット値を取って移動変換させることで、マップのスクロールを実現しています。

//ウィンドウの大きさとプレイヤーの座標からオフセット座標を求める
offset = Vec2(WINDOW_WIDTH / 2 - player->getPos().x, WINDOW_HEIGHT / 2 - player->getPos().y);
Graphics2D::SetTransform(Mat3x2::Translate(offset));

弾幕生成

各Enemyから、Bulletのインスタンスを生成しては、発射してます。

//固定弾(360° 5way ぐるぐる)
frameCount++;

auto bulletManager = game->getBulletManager();
if (fireCount % 2 == 0) {
	//5分割にする
	const int sep = 5;
	for (auto i : step(sep)) {
		const double fireRad = Radians(frameCount * 2) + TwoPi / sep * i;
		//弾を発射した際の座標、角度、スピードを持たせる
		auto bullet = std::make_shared<Bullet>(pos, shotRad, 5.0);
		bulletManager->add(bullet);
	}
}

次の様な弾幕が生成されます。

f:id:yashihei:20151221112636g:plain

左下のログ

左下のログ表示が個人的には、結構気に入ってる部分だったりします。大したことはしてないですが、std::dequeを有効活用した感じが嬉しいです。

std::deque<String> logStrs;

//ログ追加、ログの数が10個を超えたら吐き出す
logStrs.push_front(str);
if (logStrs.size() > 10) logStrs.pop_back();

//描画
int i = 0;
for (const auto& str : logStrs) {
	FontAsset(L"small").draw(str, Vec2(0.0, -10 * i), HSV(Palette::Lightgreen).toColorF(1.0 - 0.1 * i));
	i++;
}

グラフィックについて

今回は一切画像を使わずに、Siv3Dの図形描画だけで、グラフィックを描画しています。その際加算合成を使うと、良い感じになるのでオススメです。

Graphics2D::SetBlendState(BlendState::Additive);

フォントは、Orbitronを使っています。緑に染めると中々サイバーな感じになります。

課題

各部品を作るのは楽しかったので良かったですが、これをゲームにしようと思った際、あれ?どうゲームにすれば良いんだと悩んでそのまま手を付けられずに居ます。プログラムを書く技術とゲームを作る技術はまた違ったものなので、難しいです…。

明日は@hamukun8686さんの記事です。よろしくお願いします。