PeterMemo

主にC++,OpenGL,Siv3Dのプログラミング関連の内容です.

Siv3D Advent Calendar 2018 Siv3Dと物理エンジンBullet Physicsで3Dゲームを作る

Siv3DとBulletでねねっちのゲームを作る

この記事はSiv3D Advent Calendar 2018 13日目の記事です.
qiita.com

今回は描画やゲームを作る際に便利な関数がそろっているSiv3Dと,物理エンジンのBullet Physicsを組み合わせて簡単なゲームを作成してみました.
作るゲームは,アニメ「NEW GAME!!」でねねっちが劇中で作成していた迷路ゲームです!
f:id:peterpanppp:20181212165917g:plain
劇中では上司であるうみこさんから課題を与えられ,3か月足らずで迷路ゲームを作っていました.またこの際ねねっちはゲームエンジンに頼っていないと発言していたのでUnityUE等は使いません.そのため物理演算にはBullet Physicsを使うことにしました.
Siv3DとBulletについては昨年のAdCにも記事を書いてくださった方がいるので良ければ見てください!!
qiita.com


この記事で伝えたいことは

・描画や細かい関数はSiv3Dが便利で楽!
・Bulletの雰囲気
・憧れのねねっちと同じゲームを作れた
・ぼくがC++を勉強し始め,Siv3Dに出会えたのはねねっちのおかげ
・Siv3DはC++入門に最適

です!他の方ほど濃い内容の記事ではありませんが,
暇つぶし程度に読んでもらえると嬉しいです!!


Bullet Physicsについて

Bullet - Wikipedia

代表的な物理演算ライブラリの一つです.
3Dのゲーム制作やシミュレーションを行う上では,
物理演算ライブラリを利用することで作業がかなり楽になるので使い得かと思います.
細かい説明はWikiや昨年のAdCをご覧ください.
いくつか有名なライブラリがあるなかでBulletを選んだ理由は名前がかっこいいからです.
またソースコードが理解しやすかったのも多少理由に含まれます.


作成するゲームの説明

迷路を傾けることでボールをゴールまで運ぶゲームです.
f:id:peterpanppp:20171121090004j:plainf:id:peterpanppp:20171112195236j:plain
©得能正太郎芳文社NEW GAME!!政策委員会
作り方
・立方体を組み合わせて迷路を作る
・立方体同士は2点で点球結合をすることで締結する
・ユニバーサルジョイントを用いて迷路を十字方向に傾けられるようにする
・ボールは普通の球
シンプルなコードでレンダリングができ,ゲームを作る際に必要な関数がそろっているSiv3Dと物理エンジンBulletを組み合わせて作りました.迷路の壁の座標を決める際にはOpenSiv3Dのscript機能がとても役に立ちました.


・ユニバーサルジョイントについて
簡単にいうと2軸方向に回転することができる継手です.
原点となる物体と迷路をユニバーサルジョイントで繋ぐことで前後左右に迷路を動かせるようになります.
各軸をモータで動かせるように設定することでキー入力などで自在に動かせるようになります.


ソース,ライブラリの説明

ゲームのソースコードはこんな感じです.今後自分の研究でも利用すると思い自分用のヘッダを作成し簡単にBulletを扱えるようにしました.

# include "BulletSiv3D.hpp"

void Main()
{
	Window::Resize(640 * 2, 480 * 2);
	Graphics::SetBackground(Color(172, 69, 124));
	s3dbt::btBox originBox(Vec3::Zero, Vec3::One, 0, Color(0, 0, 0, 0), false);
	s3dbt::btBox boardBox(Vec3(0, 3, 0), Vec3(10, 0.25, 10), 100000.0, Color(107, 150, 228));
	s3dbt::btDynamicWorld world;
	world.addRigidBody(originBox);
	world.addRigidBody(boardBox);
	s3dbt::btUniversalJoint uniJoint(originBox, boardBox, Vec3::Zero, Vec3::UnitZ, Vec3::UnitX);
	double range = 0.1;
	uniJoint.setAngularLowerLimit(Vec3(0, -Math::PiF * range, -Math::PiF * range));
	uniJoint.setAngularUpperLimit(Vec3(0, Math::PiF * range, Math::PiF * range));
	world.addJoint(uniJoint);
	uniJoint.setRotationalLimitMotor(s3dbt::btUniversalJoint::AXIS::Z);
	uniJoint.setRotationalLimitMotor(s3dbt::btUniversalJoint::AXIS::Y);
	s3dbt::btSphere ball(Vec3(8.5, 5, 8.5), 0.25, 1, Color(141, 144, 179));
	world.addRigidBody(ball);
	Array<s3dbt::btBox> walls;
	Array<s3dbt::btPoint2PointJoint> wallJoints;
	{
		CSVReader csv(L"../FieldEdit/App/resource_realscale.csv");
		auto size = static_cast<int>(csv.rows);
		const double blocksCenterY = 3. + 0.25 + 0.5;
		auto wallSize = size;
		walls.reserve(size);
		wallJoints.reserve(size * 2);
		for (auto i : step(size))
		{
			auto sizex = csv.get<double>(i, 0);
			auto sizey = csv.get<double>(i, 1);
			auto centerx = csv.get<double>(i, 2);
			auto centery = csv.get<double>(i, 3);
			walls.emplace_back(Vec3(centerx - 10., blocksCenterY, -centery + 10.), Vec3(sizex, 1, sizey) * 0.5, 1);
		}
		for (auto&& wall : walls)
		{
			world.addRigidBody(wall);
			auto boxSize = wall.getSizeSiv3d() * 2;
			auto boxCenter = wall.getPosSiv3d();
			btVector3 btHalfSize(boxSize.x * 0.5, boxSize.y * 0.5, boxSize.z * 0.5);

			const Vec3 plateSize = boardBox.getSizeSiv3d();
			const Vec3 plateCenter = boardBox.getPosSiv3d();
			// ベースから左下手前
			Vec3 pivotInA(boxCenter.x - btHalfSize.x(), plateSize.y, boxCenter.z - btHalfSize.z());
			// 壁の中心から左下手前
			Vec3 pivotInB(-btHalfSize.x(), -btHalfSize.y(), -btHalfSize.z());
			wallJoints.emplace_back(boardBox, wall, pivotInA, pivotInB);
			// ベースから右下奥
			Vec3 pivotInC(boxCenter.x + btHalfSize.x(), plateSize.y, boxCenter.z + btHalfSize.z());
			// 壁の中心から右下奥
			Vec3 pivotInD(btHalfSize.x(), -btHalfSize.y(), btHalfSize.z());
			wallJoints.emplace_back(boardBox, wall, pivotInC, pivotInD);
		}
		for (auto&& joint : wallJoints)
		{
			world.addJoint(joint);
		}
	}

	while (System::Update())
	{
		{
			double force = 100000.;
			if (Input::KeyG.pressed)
			{
				uniJoint.setMotorForce(2, -10, force);
			}
			else if (Input::KeyH.pressed)
			{
				uniJoint.setMotorForce(2, 10, force);
			}
			else
			{
				uniJoint.setMotorForce(2, 0, force);
			}
			if (Input::KeyY.pressed)
			{
				uniJoint.setMotorForce(1, -10, force);
			}
			else if (Input::KeyB.pressed)
			{
				uniJoint.setMotorForce(1, 10, force);
			}
			else
			{
				uniJoint.setMotorForce(1, 0, force);
			}
		}

		world.update();
		world.draw();
	}
}

ここではBullet(今回扱う部分のみ)の簡単な説明をします.
・btDynamicWorld
物理シミュレーション空間のクラスです.
このクラスに定義した物体を登録,更新することで衝突判定や物体の座標と角度を計算してくれます.

・btBox
普通の立方体です.できるだけSiv3Dと同じような感覚で使えるようにしましたが実力不足と時間の都合で完璧な再現はできませんでした.

・btSphere
普通の球です

・btPoint2PointJoint
2つの物体を点でつなぎます.それぞれの物体のローカル座標を指定し,その点同士で固定します.
一定の加重がかかったら破断させるように設定できます.

・btUniversalJoint
上でも説明したユニバーサルジョイントです.
拘束する2つの物体,回転軸の座標(anchor),回転軸(axis)を2つ指定します.
回転軸は必ず直交しなければいけないようです.
この拘束は3つの回転軸がそれぞれ何度まで回るのかを指定できます.
回転させる際は,回転軸に対してモータを設定し,角度を指定し力を加えることで回転させることができます.


下のリンクからCloneしていただければゲームを動かせると思います.
github.com


最後に

ここまで見てくださった人ありがとうございます.ぼくがC++を始めたきっかけはNEW GAME!のねねっちです.おそらく一定数ねねっちに憧れてプログラミング始めた人がいるのではと思っています.
そんな憧れのプログラマが作っていたゲームを自分も作れるようになったのはC++の入門には最適なSiv3Dのおかげです.Ryo Suzukiさんや開発にかかわっている方々には本当に感謝しております.
また昨年のAdCの記事を見ると,OpenSiv3Dで3次元描画やBulletを使えるようになる日がそのうち来るのかなーと思います!そうなればもっと手軽に3Dゲームを作れるようになるので楽しみですね!!