Bevyを使った個人でゲーム開発を行なっている登尾(のぼりお)です。
今回はRust用の物理エンジンであるRapierを使った2Dの物理シミュレーションを実装してみましょう。
クレートを追加しよう
Bevy用でかつ2D版は、以下のクレートを追加すると利用できます。
cargo add bevy_rapier2d
Bevyのプラグインに追加しよう
BevyのAppへは以下のようにプラグインを追加しましょう。
.add_plugins(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(100.0))
pixels_per_meterへ渡す値はメートルあたりピクセル数かという、物理演算での単位とBevy上での単位のスケール変換に使われます。今回の場合は1メートルあたり100ピクセル、と指定しています。
地面を準備しよう
今回の例ではボールが上から順に降ってくるものとしたいので、そのボールが貯まる地面を用意しましょう。
.add_systems(Startup, (camera, spawn_world))
というようにAppに追加し、いつもやっていることですが先にカメラを追加します。
fn camera(mut commands: Commands) {
commands.spawn(Camera2d);
}
spawn_world関数は以下のとおりです。
const GROUND_W: f32 = 1000.0;
const GROUND_H: f32 = 10.0;
const GROUND_Y: f32 = -300.0;
const GROUND_COLOR: Color = Color::srgb(1.0, 0.7, 0.4);
fn spawn_world(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
let ground_mesh = Mesh::from(Rectangle::new(GROUND_W, GROUND_H));
commands.spawn((
Mesh2d(meshes.add(ground_mesh).into()),
MeshMaterial2d(materials.add(GROUND_COLOR)),
Transform::from_xyz(0.0, GROUND_Y, -0.1),
RigidBody::Fixed,
Collider::cuboid(GROUND_W * 0.5, GROUND_H * 0.5),
Friction::coefficient(0.6),
));
}
Mesh2d、MeshMaterial2d、TransformはBevy上のもので、それぞれ形、色、位置を渡しています。
そこに加えて、RigidBody、Collier、Frictionがbevy_rapier2d経由のもので、以下の役割があります。
-
RigidBody::Fixed
物体を固定の状態、不動のものとして扱います。重力の影響を受けません。 -
Collider::cuboid(GROUND_W * 0.5, GROUND_H * 0.5)
直方体の当たり判定を付けます。想定する大きさの、半分の高さ、半分の幅で指定します。(ここでは幅 GROUND_Wの高さ GROUND_Hの長方形)。当たり判定があることでの、ボールとの衝突を期待します。 -
Friction::coefficient(0.6)
摩擦係数を 0.6 に設定しています。0 なら完全に滑り、1 以上で強い摩擦になります。
Collierについては以下にその他の形についての説明があり参考になるかと思います。
https://rapier.rs/docs/user_guides/rust/colliders/#shapes
ボールを定期的に生成させよう
spawn_ballを以下のようにシステムとして追加します。ボールの生成する間隔が短すぎる場合は Duration::from_millis
の引数を調整して下さい。
.add_systems(Update, spawn_ball.run_if(on_timer(Duration::from_millis(40))))
spawn_ball関数は以下の通りです。
const BALL_R: f32 = 10.0;
const BALL_START_Y: f32 = 400.0;
fn spawn_ball(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
let ball_mesh = Mesh::from(Circle::new(BALL_R));
let mut r = rand::rng();
let x = r.random_range(-20.0..20.0);
let color = Color::srgb(r.random::<f32>() * 0.3 + 0.7, r.random::<f32>() * 0.3 + 0.7, 1.0);
commands.spawn((
Mesh2d(meshes.add(ball_mesh).into()),
MeshMaterial2d(materials.add(color)),
Transform::from_xyz(x, BALL_START_Y, 0.1),
RigidBody::Dynamic,
Collider::ball(BALL_R),
Friction {
coefficient: 0.9,
combine_rule: CoefficientCombineRule::Min,
},
Restitution {
coefficient: 0.4,
combine_rule: CoefficientCombineRule::Max,
},
Damping {
linear_damping: 0.01,
angular_damping: 0.05,
},
));
}
地面を作った時との大きな違いは、
-
Collider::ballを使った球体の当たり判定をつけています。
-
Friction { coefficient: 0.9, combine_rule: CoefficientCombineRule::Min }
接触面での摩擦係数です。 -
Restitution { coefficient: 0.4, combine_rule: CoefficientCombineRule::Max }
反発係数(バウンドのしやすさ)です。0 だと全く弾まず、1 に近いほど弾みます。 -
Damping { linear_damping: 0.01, angular_damping: 0.05 }
物体の速度に比例して掛かる空気抵抗的な減速をしてくれます。 -
Transformに渡すx、yとMeshMaterial2dに渡す色をランダムに決定しています。
以上のような実装によって以下のようにアニメーションされます。
また、一部掲載していないコードもありますのが、全体像は以下で確認できます。
おしまい
今回もこれまで同様以下の個人リポジトリで公開しています。
cloneした後に、
% cargo run --example bounce_balls
で挙動を確認できます。
Mesh2dの代わりにSpriteを一緒にspawnさせれば画像が動くように実装できますので、色々と遊べると思います。