はじめに
クレーンゲームの魅力を増すためには、リアルな物理シミュレーションが不可欠です。今回、OpenSiv3Dを用いてクレーンゲームにこのようなソフトボディの挙動を実装する過程を紹介します。
OpenSiv3DとBox2D
OpenSiv3Dは、デフォルトでBox2Dの機能を提供しており、これを使ってソフトボディの実装に取り組むことにしました。
# include <Siv3D.hpp> // OpenSiv3D v0.6.11
Array<Vec2> ChaikinSmooth(const Array<Vec2>& inputVertices, int iterations = 1)
{
// Error handling for insufficient vertex count
if (inputVertices.size() < 3)
{
throw std::invalid_argument("Input vertices must form a polygon (at least 3 vertices).");
}
// Error handling for invalid iteration count
if (iterations <= 0)
{
throw std::invalid_argument("Iterations must be a positive integer.");
}
Array<Vec2> currentVertices = inputVertices;
for (int k = 0; k < iterations; ++k)
{
Array<Vec2> newVertices;
const size_t vertexCount = currentVertices.size();
for (size_t i = 0; i < vertexCount; ++i)
{
// Get current vertex and next vertex (with wrap-around)
const Vec2& p0 = currentVertices[i];
const Vec2& p1 = currentVertices[(i + 1) % vertexCount];
// Compute new vertices
const Vec2 q = p0 * 0.75 + p1 * 0.25;
const Vec2 r = p0 * 0.25 + p1 * 0.75;
// Add new vertices to the list
newVertices << q << r;
}
// Update current vertices for next iteration (if any)
currentVertices = newVertices;
}
return currentVertices;
}
struct SoftBody
{
Array<P2Body> aroundBodies;
P2Body centerBody;
Array<P2DistanceJoint> softbodyJoints;
double centerBodyRadius;
double outerBodyRadius;
};
SoftBody CreateSoftBody(P2World& world, const Vec2& pos, double centerBodyRadius = 50.0, double outerBodyRadius = 30.0)
{
SoftBody softbody;
P2Material material;
material.friction = 0.5;
softbody.centerBodyRadius = centerBodyRadius;
softbody.outerBodyRadius = outerBodyRadius;
softbody.centerBody = world.createCircle(P2Dynamic, pos, Circle(centerBodyRadius), material);
int32 num = 12;
const double diffAngle = Math::TwoPi / num;
for (int32 i = 0; i < num; ++i)
{
softbody.aroundBodies << world.createCircle(P2Dynamic, pos + Vec2::Right().rotated(diffAngle * i) * (outerBodyRadius + centerBodyRadius), Circle(outerBodyRadius), material);
}
for (int32 i = 0; i < num; ++i)
{
const Vec2 jointPosA = softbody.centerBody.getPos() + (softbody.aroundBodies[i].getPos() - softbody.centerBody.getPos()) * 0.0;
const Vec2 jointPosB = softbody.aroundBodies[i].getPos();
softbody.softbodyJoints << world.createDistanceJoint(softbody.centerBody, jointPosA, softbody.aroundBodies[i], jointPosB, (jointPosB - jointPosA).length()).setLinearStiffness(3.0, 0.5).setMinLength(0.0);
}
for (int32 i = 0; i < num; ++i)
{
softbody.softbodyJoints << world.createDistanceJoint(softbody.aroundBodies[i], softbody.aroundBodies[i].getPos(), softbody.aroundBodies[(i + 1) % num], softbody.aroundBodies[(i + 1) % num].getPos(), (softbody.centerBody.getPos() - softbody.aroundBodies[i].getPos()).length()).setLinearStiffness(1.0, 0.5).setMinLength(0.0);
}
return softbody;
}
void Main()
{
Window::Resize(1280, 720);
// 2D 物理演算のシミュレーションステップ(秒)
constexpr double StepTime = (1.0 / 200.0);
// 2D 物理演算のシミュレーション蓄積時間(秒)
double accumulatedTime = 0.0;
P2World world;
Array<SoftBody> softbodies;
// 地面
Array<P2Body> grounds;
grounds << world.createRect(P2Static, Vec2{ 0, -200 }, SizeF{ 600, 20 });
grounds << world.createLine(P2Static, Vec2{ 0, 0 }, Line{ -500, -150, -300, -50 });
grounds << world.createLineString(P2Static, Vec2{ 0, 0 }, LineString{ Vec2{ 100, -50 }, Vec2{ 200, -50 }, Vec2{ 600, -150 } });
// 2D カメラ(中心座標 (0, -300), 拡大率 1.0)
Camera2D camera{ Vec2{ 0, -300 }, 1.0 };
while (System::Update())
{
for (accumulatedTime += Scene::DeltaTime(); StepTime <= accumulatedTime; accumulatedTime -= StepTime)
{
// 2D 物理演算のワールドを StepTime 秒進める
world.update(StepTime);
// 地面の下に 500 cm 以上落下した物体を削除する
softbodies.remove_if([](const SoftBody& softbody) { return (500 < softbody.centerBody.getPos().y); });
}
// 2D カメラを更新する
camera.update();
{
// 2D カメラから Transformer2D を作成する
const auto t = camera.createTransformer();
// すべての地面を描画する
for (const auto& ground : grounds)
{
ground.draw(Palette::Gray);
}
for (const auto& softbody : softbodies)
{
Array<Vec2> vertices;
for (const auto& body : softbody.aroundBodies)
{
vertices << body.getPos();
}
Polygon(ChaikinSmooth(vertices, 2)).calculateBuffer(softbody.outerBodyRadius).draw(Palette::Blue).drawFrame(5.0, Palette::White);
}
}
// 2D カメラの操作を描画する
camera.draw(Palette::Orange);
if (SimpleGUI::Button(U"Add", Vec2{ 40, 80 }, 120))
{
softbodies << CreateSoftBody(world, Vec2(0.0, -400.0));
}
}
}
ソフトボディの物理
ソフトボディとは、外部からの力に応じて形状が変化し、力が取り除かれると元の形に戻る物体のことです。これにより、よりリアルな物理シミュレーションが可能になります。LiquidFunなどの粒子ベースのアプローチを採らず、Box2DのDistanceJoint
を使って多角形を作成し、ソフトボディの挙動をシミュレートすることにしました。
for (int32 i = 0; i < num; ++i)
{
softbody.aroundBodies << world.createCircle(P2Dynamic, pos + Vec2::Right().rotated(diffAngle * i) * (outerBodyRadius + centerBodyRadius), Circle(outerBodyRadius), material);
}
for (int32 i = 0; i < num; ++i)
{
const Vec2 jointPosA = softbody.centerBody.getPos() + (softbody.aroundBodies[i].getPos() - softbody.centerBody.getPos()) * 0.0;
const Vec2 jointPosB = softbody.aroundBodies[i].getPos();
softbody.softbodyJoints << world.createDistanceJoint(softbody.centerBody, jointPosA, softbody.aroundBodies[i], jointPosB, (jointPosB - jointPosA).length()).setLinearStiffness(3.0, 0.5).setMinLength(0.0);
}
for (int32 i = 0; i < num; ++i)
{
softbody.softbodyJoints << world.createDistanceJoint(softbody.aroundBodies[i], softbody.aroundBodies[i].getPos(), softbody.aroundBodies[(i + 1) % num], softbody.aroundBodies[(i + 1) % num].getPos(), (softbody.centerBody.getPos() - softbody.aroundBodies[i].getPos()).length()).setLinearStiffness(1.0, 0.5).setMinLength(0.0);
}
コードの解説
CreateSoftBody
関数では、中心のcenterBody
と周囲のaroundBodies
を結ぶことで、ソフトボディを形成しています。各DistanceJoint
のsetLinearStiffness
により、ソフトボディの弾性と減衰を制御しています。
問題点と解決策
実装したソフトボディは、aroundBodies
の数が少ないと角ばった外観になってしまいます。この視覚的な問題を解決するために、Chaikinのコーナーカットアルゴリズムを適用しました。
Polygon(ChaikinSmooth(vertices, 2)).calculateBuffer(softbody.outerBodyRadius).draw(Palette::Blue).drawFrame(5.0, Palette::White);
詳しくはこちらで記事にしました。
https://qiita.com/hamukun8686/items/9888d3f18dd33d56a7f6
あと、結構パラメータがシビアです。どんなパラメータをしてしても破綻しないようにうまく設計できればいいのですが、今はそうなっていません。
まとめ
この記事では、OpenSiv3DとBox2Dを用いてクレーンゲームにソフトボディを実装する方法を紹介しました。ソフトボディのリアルな挙動は、ゲーム体験を格段に向上させることができます。