Compare commits

..

14 Commits

Author SHA1 Message Date
Zynh0722 e259bf6019 better window bounding boxes
I also switch to using generalized Rect instead of an app for places where i was using the window boundary
2024-02-17 02:38:20 -08:00
Zynh0722 4c36162aa5 walls! 2024-02-17 02:29:06 -08:00
Zynh0722 8cde3ba399 switch to using a generic rect 2024-02-17 02:28:54 -08:00
Zynh0722 1e90b2c514 comments 2: electric boogaloo 2024-02-17 02:27:32 -08:00
Zynh0722 5bba4f019e comments 2024-02-17 02:15:33 -08:00
Zynh0722 b7a5277a11 no more overlapping balls! 2024-02-17 02:12:04 -08:00
Zynh0722 be7c6763f0 more cleanup 2024-02-17 01:51:56 -08:00
Zynh0722 9d5ced0539 actually remove particles 2024-02-17 01:13:02 -08:00
Zynh0722 8d822448f8 emulate old reset behaviour 2024-02-17 00:37:07 -08:00
Zynh0722 ab92ac9430 many balls 2024-02-16 23:34:02 -08:00
Zynh0722 76758fa2a8 removing an inaccurate underscore 2024-02-16 23:14:44 -08:00
Zynh0722 d10985909b deleting an old unused comment 2024-02-16 23:14:21 -08:00
Zynh0722 a663efd150 we have drawing physics 2024-02-16 23:13:40 -08:00
Zynh0722 c8ae692c33 rapier engine testing (don't like) 2024-02-16 19:18:24 -08:00
6 changed files with 1097 additions and 2054 deletions

2840
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,12 +6,9 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bevy = { version = "0.12.1" }
bevy_dylib = "0.12.1"
bevy_rapier2d = "*"
[profile.dev]
opt-level = 1
nalgebra = { version = "0.32.3", features = ["glam017"] }
nannou = "0.19.0"
rapier2d = "0.18.0"
[profile.dev.package."*"]
opt-level = 3

View File

@ -1,2 +0,0 @@
[toolchain]
channel = "nightly"

56
src/drawable.rs Normal file
View File

@ -0,0 +1,56 @@
use nannou::prelude::*;
use rapier2d::geometry::{Collider, ColliderSet, TypedShape};
pub(crate) trait Drawable {
fn draw(&self, draw: &Draw);
}
impl<T> Drawable for Vec<T>
where
T: Drawable,
{
fn draw(&self, draw: &Draw) {
self.iter().for_each(|s| s.draw(draw))
}
}
impl Drawable for Collider {
fn draw(&self, draw: &Draw) {
match self.shape().as_typed_shape() {
TypedShape::Ball(ball) => {
draw.ellipse()
.radius(ball.radius)
.xy((*self.translation()).into());
}
TypedShape::Cuboid(cube) => {
draw.rect()
.w(cube.half_extents.x * 2.0)
.h(cube.half_extents.y * 2.0)
.xy((*self.translation()).into());
}
_ => eprintln!("Attempted to draw a shape with no draw impl"),
}
}
}
impl Drawable for ColliderSet {
fn draw(&self, draw: &Draw) {
self.iter().for_each(|(_, collider)| draw.draw(collider))
}
}
pub(crate) trait DrawShape<T>
where
T: Drawable,
{
fn draw(&self, drawable: &T);
}
impl<T> DrawShape<T> for Draw
where
T: Drawable,
{
fn draw(&self, drawable: &T) {
drawable.draw(self);
}
}

50
src/engine.rs Normal file
View File

@ -0,0 +1,50 @@
use rapier2d::{
dynamics::{
CCDSolver, ImpulseJointSet, IntegrationParameters, IslandManager, MultibodyJointSet,
RigidBodySet,
},
geometry::{BroadPhase, ColliderSet, NarrowPhase},
math::Vector,
pipeline::{PhysicsPipeline, QueryPipeline},
};
#[derive(Default)]
pub struct PhysicsEngine {
pub state: PhysicsState,
pub pipeline: PhysicsPipeline,
}
#[derive(Default)]
pub struct PhysicsState {
pub islands: IslandManager,
pub broad_phase: BroadPhase,
pub narrow_phase: NarrowPhase,
pub bodies: RigidBodySet,
pub colliders: ColliderSet,
pub joints: ImpulseJointSet,
pub multibody_joints: MultibodyJointSet,
pub ccd_solver: CCDSolver,
pub query_pipeline: QueryPipeline,
pub integration_parameters: IntegrationParameters,
pub gravity: Vector<f32>,
}
impl PhysicsEngine {
pub fn step(&mut self) {
self.pipeline.step(
&self.state.gravity,
&self.state.integration_parameters,
&mut self.state.islands,
&mut self.state.broad_phase,
&mut self.state.narrow_phase,
&mut self.state.bodies,
&mut self.state.colliders,
&mut self.state.joints,
&mut self.state.multibody_joints,
&mut self.state.ccd_solver,
Some(&mut self.state.query_pipeline),
&(),
&(),
)
}
}

View File

@ -1,44 +1,168 @@
use bevy::prelude::*;
use bevy_rapier2d::prelude::*;
mod drawable;
mod engine;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(100.0))
.add_plugins(RapierDebugRenderPlugin::default())
.add_systems(Startup, setup_graphics)
.add_systems(Startup, setup_physics)
.add_systems(Update, print_ball_altitude)
.run();
use drawable::DrawShape;
use engine::{PhysicsEngine, PhysicsState};
use nalgebra::vector;
use nannou::prelude::*;
use rapier2d::{
dynamics::{RigidBodyBuilder, RigidBodyHandle, RigidBodySet},
geometry::{ColliderBuilder, ColliderSet},
};
const WINDOW_WIDTH: u32 = 512;
const WINDOW_HEIGHT: u32 = WINDOW_WIDTH;
const PARTICLE_COUNT: u32 = 200;
const PARTICLE_SIZE: f32 = 5.;
struct Model {
// Store the window ID so we can refer to this specific window later if needed.
window: WindowId,
// particles: Vec<Particle>,
engine: PhysicsEngine,
}
fn setup_graphics(mut commands: Commands) {
// Add a camera so we can see the debug-render.
commands.spawn(Camera2dBundle::default());
fn random_vec_in_rect(boundary: &Rect) -> nalgebra::Vector2<f32> {
let x = random_range(-100., 100.);
let y = random_range(-150., boundary.top());
vector![x, y]
}
fn setup_physics(mut commands: Commands) {
/* Create the ground. */
commands
.spawn(Collider::cuboid(500.0, 50.0))
.insert(TransformBundle::from(Transform::from_xyz(0.0, -100.0, 0.0)));
fn fill_particles(boundary: &Rect, colliders: &mut ColliderSet, bodies: &mut RigidBodySet) {
// Keeping track of already placed balls to avoid overlap
// Need to look into a way to do this with rapier directly
let mut positions: Vec<nalgebra::Vector2<f32>> = Vec::new();
/* Create the bouncing ball. */
commands
.spawn(RigidBody::Dynamic)
.insert(Collider::ball(10.0))
.insert(Restitution::coefficient(2.0))
.insert(TransformBundle::from(Transform::from_xyz(0.0, 400.0, 0.0)));
for _ in 0..PARTICLE_COUNT {
let mut xy = random_vec_in_rect(boundary);
while !positions
.iter()
.all(|pos| pos.metric_distance(&xy) > PARTICLE_SIZE * 2.)
{
xy = random_vec_in_rect(boundary)
}
positions.push(xy);
commands
.spawn(RigidBody::Dynamic)
.insert(Collider::ball(10.0))
.insert(Restitution::coefficient(2.0))
.insert(TransformBundle::from(Transform::from_xyz(0.0, 600.0, 0.0)));
}
fn print_ball_altitude(positions: Query<&Transform, With<RigidBody>>) {
for transform in positions.iter() {
println!("Ball altitude: {}", transform.translation.y);
/* Create the bouncing ball. */
let rigid_body = RigidBodyBuilder::dynamic().translation(xy).build();
let collider = ColliderBuilder::ball(PARTICLE_SIZE)
.restitution(0.1)
.build();
let ball_body_handle = bodies.insert(rigid_body);
colliders.insert_with_parent(collider, ball_body_handle, bodies);
}
}
fn model(app: &App) -> Model {
// One thing that tripped me up when begginning nannou was realizing
// that view and event methods are per window, and update functions are
// per app
// Create a new window! Store the ID so we can refer to it later.
let window = app
.new_window()
.size(WINDOW_WIDTH, WINDOW_HEIGHT)
.title("nannou")
.view(view) // The function that will be called for presenting graphics to a frame.
.event(event) // The function that will be called when the window receives events.
.build()
.unwrap();
let mut engine = PhysicsEngine {
state: PhysicsState {
gravity: vector![0., -9.81 * 10.],
..Default::default()
},
..Default::default()
};
let boundary = app.window(window).unwrap().rect();
/* Create the ground. */
let collider = ColliderBuilder::cuboid(boundary.w(), 4.)
.translation(vector![0., boundary.bottom()])
.build();
engine.state.colliders.insert(collider);
/* Create the walls. */
let collider = ColliderBuilder::cuboid(4., boundary.h())
.translation(vector![boundary.left(), 0.])
.build();
engine.state.colliders.insert(collider);
let collider = ColliderBuilder::cuboid(4., boundary.h())
.translation(vector![boundary.right(), 0.])
.build();
engine.state.colliders.insert(collider);
/* Create the ceiling. */
let collider = ColliderBuilder::cuboid(boundary.w(), 4.)
.translation(vector![0., boundary.top()])
.build();
engine.state.colliders.insert(collider);
fill_particles(
&boundary,
&mut engine.state.colliders,
&mut engine.state.bodies,
);
Model { window, engine }
}
// Handle events related to the window and update the model if necessary
fn event(app: &App, model: &mut Model, event: WindowEvent) {
if let KeyReleased(Key::Escape) = event {
app.quit()
}
if let Resized(_) = event {
// Rust borrowing rules means I need to first gather a list of handles,
// then delete them all
let handles: Vec<RigidBodyHandle> = model
.engine
.state
.bodies
.iter()
.map(|(handle, _)| handle)
.collect();
for handle in handles {
model.engine.state.bodies.remove(
handle,
&mut model.engine.state.islands,
&mut model.engine.state.colliders,
&mut model.engine.state.joints,
&mut model.engine.state.multibody_joints,
true,
);
}
fill_particles(
&app.window(model.window).unwrap().rect(),
&mut model.engine.state.colliders,
&mut model.engine.state.bodies,
);
}
}
// Update the state of your application here. By default, this gets called right before `view`.
fn update(_app: &App, model: &mut Model, _update: Update) {
model.engine.step();
}
// Draw the state of your `Model` into the given `Frame` here.
fn view(app: &App, model: &Model, frame: Frame) {
let canvas = app.draw();
canvas.background().color(CORNFLOWERBLUE);
canvas.draw(&model.engine.state.colliders);
// I don't think there is even a fail condition in this function, but it returns a result?
canvas.to_frame(app, &frame).unwrap();
}
fn main() {
nannou::app(model).update(update).run();
}