All code mentioned in this blog post can be accessed in https://git.stvnliu.me/homelab/martian-rescue-rs.git/tree/
Recently I’d been involved in a game-jam-esque event the school organised, from which, while a working/playable copy of the game could not be completed on time, I was able to simulate rudimentary gravitational physics between stellar bodies. In the context of the game proposal, this was used to create a revolving system in which the player spaceship must take advantage of to obtain enough linear velocity to reach the objective planet.
To get started, we need to make sure that physically, we understand what’s going on. To evaluate the magnitude of a gravitational force
This formula is the mathematical basis upon which we proceed with development.
To simulate these interactions, we can take advantage of avian2d
real-time physics plugin for the Bevy engine.
use bevy::prelude::*;
// the avian library is split into avian2d and avian3d. For simplicity, let's use avian2d for now, although most other things are the same.
use avian2d::prelude::*;
fn main() {
let app = App::new()
.insert_resource(WinitSettings::game())
.add_plugins(DefaultPlugins)
.add_plugins(PhysicsPlugins::default())
.add_systems(Startup, (
/* TODO we will add these systems later */
))
.add_systems(Update, (
/* TODO we will add these systems later */
))
.run();
}
Great! Now we have an interface ready for implementing the physics objects. We use two systems, one triggering in the Startup
phase, setting up all the stellar bodies we want to simulate; and one triggering every Update
phase, updating the physical properties.
Due to the way that Bevy Entities are queried in systems, it would be good if we had custom bevy Components that are set up such that planetary data can be accessed in a contained way (instead of being splattered all over the place).
use bevy::prelude::*;
#[derive(Component)]
struct Planet {
display_name: String,
planet_mass_kg: f32,
radius: f32,
}
#[derive(Component)]
struct Player {
display_name: String,
velocity: f32,
mass_kg: f32,
}
The reason we may want to do this is to use queries like Query<(&Sprite, &Transform), (With<&Planet>, Without<&Player>)>
, to be more accurate and precise when indicating what query we would like to fetch from the world.
use bevy::prelude::*;
use avian2d::prelude::*;
pub fn setup_planet(mut cmd: Commands) {
let start_planet = Planet {
display_name: String::from("Earth"),
planet_mass_kg: 15000.0,
radius: 50.0,
};
let collider_radius = (&start_planet).radius;
let planet_mass_kg = (&start_planet).planet_mass_kg;
cmd.spawn((
start_planet,
RigidBody::Static,
Transform::from_xyz(0.0, 0.0, 0.0),
Sprite::from_color(Color::WHITE, Vec2 { x: 50.0, y: 50.0 }),
Collider::rectangle(collider_radius, collider_radius),
Mass(planet_mass_kg),
));
}
In this code, we setup a new planet (aka. new stellar object), and use components belonging to the planet to declare a RigidBody
, Collider
, and to set a Mass
component for it to become a physics object. We might prefer to let it be a stable point of reference, so it will be a RigidBody::Static
that is not affected by any physical force.
Whereas for the player, it will be experiencing forces, so we add an extra component ExternalForce
to be applied to the player. Initially this will have a value of zero (ExternalForce::ZERO
because verbosity can be good), but we don’t need to worry about it too much because soon we will also modify it to apply our own external forces (gravitation).
Note: We set non-persistent force, because avian2d
defaults to a constant external force that persistently acts.
use bevy::prelude::*;
use avian2d::prelude::*;
pub fn setup_player(mut commands: Commands) {
commands.spawn((
Player {
velocity: 20000.0,
display_name: Text2d("Main player".to_string()),
mass_kg: 100.,
},
RigidBody::Dynamic,
Transform::from_xyz(0., 70., 0.),
ExternalForce::ZERO.with_persistence(false),
Sprite::from_color(Color::BLACK, Vec2 { x: 10., y: 10. }),
Collider::rectangle(10., 10.),
));
}
Time for the meat. Now we can apply a force by calculating its magnitude, then applying it to the normalized vector towards the center of the large mass. Let’s make a helper function that does this:
use bevy::prelude::*;
fn calculate_force_vector(
mass_actor: f32,
mass_incident: f32,
pos_actor: Vec2,
pos_incident: Vec2,
) -> Vec2 {
let grav_const = 15.0;
(grav_const * mass_actor * mass_incident / (pos_actor.distance_squared(pos_incident)))
* Vec2 {
x: pos_actor.x - pos_incident.x,
y: pos_actor.y - pos_incident.y,
}
.normalize()
}
Now we just need to supply the two masses, their transform position on the “universal grid”, and the things will be calculated for us. We can then apply this force by:
fn update_gravitational_forces(
mut query_planet: Query<(Entity, &Planet, &Transform)>,
mut query_object: Query<(Entity, &Player, &mut ExternalForce, &Transform)>,
) {
let (_es, planet, transform_planet) = query_planet.single_mut();
let (_ed, player, mut player_force, transform_player) = query_object.single_mut();
let f0 = calculate_force_vector(
planet.planet_mass_kg,
player.mass_kg,
transform_planet.translation.xy(),
transform_player.translation.xy(),
);
exd.apply_force(f0);
}
Now we just put everything together, spawn in a Camera2d
to observe the sprites moving about, and we are done with adding gravitation to our game!
Made with ❤ and at Delft, the Netherlands.