適度なネタが思いつかなかったので、一番大きな話題ともいえる物理エンジンについて触れておきます。
大抵のアクションゲームでは、たとえばキャラクターと壁の当たり判定や、ジャンプしたときに放物線にそって動く計算などが必要になるでしょう。簡単な動きだけでいいなら自力でコーディングしてもいいですし、実際私もおととし書いたアドベントカレンダーでは自力で当たり判定を書いています。
でも、自力で実装したアクションというのはかなりショボいです。そしてそうやって自力で実装したアクションゲームだと、キャラクターとリフトの摩擦が考慮されておらずリフトに乗ったらリフトだけが移動して取り残されたキャラクターが落下というバグがよく起こったりします。いろんな挙動をなるべく自然に見せたかったら、結局物理エンジンを導入するのが無難かなと思います。
一方で、物理エンジンは設定項目が多く、設定を間違えると変な動きをするわけに原因がさっぱりわからない、と非常に厄介です。かなり覚悟を決めてかかる必要があります。ぶっちゃけアクションゲームはこの物理エンジン設定バトルが大きいので、慣れていない人はアクションゲームを作るのは避けたほうがいい気がします。私はアクションが一番好きなのでどうしてもアクションゲームにしてしまうのですが……。
物理エンジンの選択
BevyやRust界隈だと、Rapierという物理エンジンが最もメジャーだと思います。RapierはBevyとの統合も比較的こなれています。
それ以外の物理エンジンだと、Avianというのがあります。これは別の物理エンジンを作っていた作者が新たにBevy向けに作り直したという新しいエンジンです。かなり期待が持てそうなのですが、まだ出てきて間もないので今回はパスしました。いつか試してみたいです。
bevy_rapier2dの初期化
rapierではプラグインを追加するときにいくつか設定項目があります。デフォルトでは長さ1
が1メートルになっており、たとえば縦横16
ピクセルのスプライトに縦横16
の剛体を設定すると、高さ16メートルのビルが倒れるときのようなゆっくりとした動きに見えます。これでは奇妙なので、pixels_per_meter
で縮尺を調整するといいでしょう。私は0.01
を設定して、縦横16
ピクセルは縦横16
センチメートルにしてあります。
app.add_plugins(
RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(PIXELS_PER_METER)
.in_fixed_schedule()
.with_custom_initialization(RapierContextInitialization::NoAutomaticRapierContext),
)
また、シミュレーションステップ間隔を固定するin_fixed_schedule()
や、Rapierのワールドのコンテキストを自力で設定するwith_custom_initialization
を設定しています。
ワールドの初期化
最新版のRapier 0.27から物理エンジン世界はエンティティとなり、アプリケーション全体で複数のワールドを持てるようになりました。複数のワールドなんてどういうときに使うのか想像つきませんが……。
with_custom_initialization
を呼んで自力で初期化する場合は、以下のようにして RapierContext
をspawnします。このRapierContext
がRapierの物理世界であり、この中にさまざまな剛体がツリー構造で収められているイメージです。デフォルトのコンテキストを指定するため、DefaultRapierContext
も付けておきます。また、この物理世界はRapierConfiguration
で重力を設定できます。デフォルトでは垂直方向に-0.98
の重力加速度が設定されていますが、今回私が作っているゲームは見下ろし型なので重力加速度はゼロとします。
fn setup_rapier_context(mut commands: Commands) {
commands.spawn((
Name::new("default rapier context"),
DefaultRapierContext,
RapierContext::default(),
RapierConfiguration {
gravity: Vec2::ZERO,
physics_pipeline_active: true,
query_pipeline_active: true,
scaled_shape_subdivision: 10,
force_update_from_transform_changes: false,
},
));
}
見下ろし型のゲームでもこの重力を設定すれば、キャラクターが風に流されるような効果を出すこともできるでしょう。
物理シミュレーションの停止
ゲームのポーズなど、物理シミュレーションを一時的に止めたいときがあります。その場合はRapierConfiguration
のphysics_pipeline_active
をfalse
に設定すればOKです。
剛体の設定
剛体の設定については、以前の記事で少しだけ触れました。RigidBody::Dynamic
を設定すれば、それだけでその物体は剛体として振舞うようになり、重力に従って落ちたり、ぶつかったら跳ね返ったりするようになります。
剛体の種類は以下の四種類です。
-
Dynamic
普通の剛体。私のゲームでは、動き回るキャラクターはすべてDynamic
です。また、宝箱や灯篭などの一見動かなそうなエンティティもDynamic
にして、密度を大きくして動きにくくしてあります。なので灯篭を魔法で攻撃するとちょっと動いたりします。なんかいろいろ動いたほうが画面が賑やかで楽しいからです。 -
Fixed
絶対に移動しない物体。壁などはFixed
です。 -
KinematicPositionBased
プログラムで動的に位置を指定する形で移動することがある物体。私のゲームでは扉がKinematicPositionBased
になっています。 -
KinematicVelocityBased
プログラムで速度を指定する形で移動する物体。魔法の弾丸がKinematicVelocityBased
になっています。この剛体は他の物体が当たって移動方向が変わったりしませんので、一定速度で動く弾丸に向いています。速度を細かく制御しやすいので、プレイヤーキャラクターをKinematicVelocityBased
にする手もありそうです。
剛体として設定されたエンティティは、摩擦や密度、外力や衝突グループなど、さまざまな大量の設定項目があります。とても全部は紹介しきれませんが、次回以降に少しずつ紹介していくかもしれません。
参考資料
bevy_rapier2dは Bevy 0.15 に合わせてAPIが破壊的に変更されており、最新版に対応したドキュメントや記事などは、日本語圏はもとより英語圏にも存在しません!頑張ってサンプルコードから学びましょう。