diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44d1e604..55347dff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,17 @@ jobs: - name: Run cargo check run: cargo check + docs: + name: Check Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Run cargo doc + run: cargo doc --workspace --no-deps --document-private-items --keep-going + env: + RUSTDOCFLAGS: "-D warnings" + test: name: Test Suite strategy: @@ -31,7 +42,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Run cargo test - run: cargo test --no-default-features --features enhanced-determinism,collider-from-mesh,serialize,debug-plugin,avian2d/2d,avian3d/3d,avian2d/f64,avian3d/f64,default-collider,parry-f64,bevy_scene + run: cargo test --no-default-features --features enhanced-determinism,collider-from-mesh,serialize,debug-plugin,avian2d/2d,avian3d/3d,avian2d/f64,avian3d/f64,default-collider,parry-f64,bevy_scene,bevy_picking lints: name: Lints diff --git a/README.md b/README.md index dc540640..68e41e9a 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ fn setup( Collider::cuboid(1.0, 1.0, 1.0), AngularVelocity(Vec3::new(2.5, 3.5, 1.5)), PbrBundle { - mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)), + mesh: meshes.add(Cuboid::from_length(1.0)), material: materials.add(Color::srgb_u8(124, 144, 255)), transform: Transform::from_xyz(0.0, 4.0, 0.0), ..default() @@ -146,7 +146,7 @@ fn setup( } ``` -![A spinning cube falling onto a circular platform](https://github.com/Jondolf/avian/assets/57632562/d53197fc-e142-4eb9-a762-dc16f6cdb1dd) +![A spinning cube falling onto a circular platform](https://github.com/user-attachments/assets/14d25e7e-9d46-467c-9fe6-dc408cd23398) ## More examples @@ -165,9 +165,10 @@ cargo run --example cubes --no-default-features --features "3d f64 parry-f64" ## Supported Bevy versions -| Bevy | Avian | -| ---- | ----- | -| 0.14 | 0.1 | +| Bevy | Avian | +| ------- | ----- | +| 0.15 RC | main | +| 0.14 | 0.1 |
Bevy XPBD versions (the predecessor of Avian) @@ -179,6 +180,7 @@ cargo run --example cubes --no-default-features --features "3d f64 parry-f64" | 0.12 | 0.3 | | 0.11 | 0.2 | | 0.10 | 0.1 | +
## Future features @@ -199,7 +201,7 @@ For larger changes and additions, it's better to open an issue or ask me for inp before making a pull request. You can also ask for help or ask questions on the [Bevy Discord](https://discord.com/invite/gMUk5Ph) -server's `Avian Physics` thread in `#crate-help`. My username on the Discord is `Jondolf` (`@jondolfdev`). +server's Avian Physics topic in `#ecosystem-crates`. My username on the Discord is `Jondolf` (`@jondolfdev`). ## Acknowledgements diff --git a/crates/avian2d/Cargo.toml b/crates/avian2d/Cargo.toml index e6e97a1a..58d51bf0 100644 --- a/crates/avian2d/Cargo.toml +++ b/crates/avian2d/Cargo.toml @@ -12,7 +12,15 @@ keywords = ["gamedev", "physics", "simulation", "bevy"] categories = ["game-development", "science", "simulation"] [features] -default = ["2d", "f32", "parry-f32", "debug-plugin", "parallel", "bevy_scene"] +default = [ + "2d", + "f32", + "parry-f32", + "debug-plugin", + "parallel", + "bevy_scene", + "bevy_picking", +] 2d = [] f32 = [] f64 = [] @@ -25,6 +33,7 @@ enhanced-determinism = [ "parry2d?/enhanced-determinism", "parry2d-f64?/enhanced-determinism", "bevy_math/libm", + "bevy_heavy/libm", ] default-collider = ["dep:nalgebra"] @@ -34,11 +43,14 @@ parry-f32 = ["f32", "dep:parry2d", "default-collider"] parry-f64 = ["f64", "dep:parry2d-f64", "default-collider"] bevy_scene = ["bevy/bevy_scene"] +bevy_picking = ["bevy/bevy_picking"] serialize = [ "dep:serde", "bevy/serialize", + "bevy_transform_interpolation/serialize", "parry2d?/serde-serialize", "parry2d-f64?/serde-serialize", + "bitflags/serde", ] [lib] @@ -49,16 +61,16 @@ bench = false [dependencies] avian_derive = { path = "../avian_derive", version = "0.1" } -bevy = { version = "0.14", default-features = false } -bevy_math = { version = "0.14" } +bevy = { version = "0.15", default-features = false } +bevy_math = { version = "0.15" } +bevy_heavy = { version = "0.1" } +bevy_transform_interpolation = { version = "0.1" } libm = { version = "0.2", optional = true } -parry2d = { version = "0.15", optional = true } -parry2d-f64 = { version = "0.15", optional = true } -nalgebra = { version = "0.32.6", features = [ - "convert-glam027", -], optional = true } +parry2d = { version = "0.17", optional = true } +parry2d-f64 = { version = "0.17", optional = true } +nalgebra = { version = "0.33", features = ["convert-glam029"], optional = true } serde = { version = "1", features = ["derive"], optional = true } -derive_more = "0.99" +derive_more = "1" indexmap = "2.0.0" fxhash = "0.2.1" itertools = "0.13" @@ -67,10 +79,13 @@ bitflags = "2.5.0" [dev-dependencies] examples_common_2d = { path = "../examples_common_2d" } benches_common_2d = { path = "../benches_common_2d" } -bevy_math = { version = "0.14", features = ["approx"] } +bevy_math = { version = "0.15", features = ["approx"] } +bevy_heavy = { version = "0.1", features = ["approx"] } +glam = { version = "0.29", features = ["bytemuck"] } approx = "0.5" +bytemuck = "1.19" criterion = { version = "0.5", features = ["html_reports"] } -insta = "1.0" +bevy_mod_debugdump = { git = "https://github.com/jakobhellermann/bevy_mod_debugdump" } [[example]] name = "dynamic_character_2d" @@ -96,10 +111,18 @@ required-features = ["2d", "default-collider"] name = "custom_collider" required-features = ["2d"] +[[example]] +name = "determinism_2d" +required-features = ["2d", "default-collider", "enhanced-determinism"] + [[example]] name = "fixed_joint_2d" required-features = ["2d", "default-collider"] +[[example]] +name = "interpolation" +required-features = ["2d"] + [[example]] name = "move_marbles" required-features = ["2d", "default-collider"] @@ -116,7 +139,6 @@ required-features = ["2d", "default-collider"] name = "revolute_joint_2d" required-features = ["2d", "default-collider"] -[[bench]] -name = "pyramid" -required-features = ["2d", "default-collider"] -harness = false +[[example]] +name = "debugdump_2d" +required-features = ["2d"] diff --git a/crates/avian2d/examples/ccd.rs b/crates/avian2d/examples/ccd.rs index 42f7686c..e48aad66 100644 --- a/crates/avian2d/examples/ccd.rs +++ b/crates/avian2d/examples/ccd.rs @@ -22,7 +22,7 @@ //! 2. Non-linear: Considers both translation and rotation. More expensive. use avian2d::{math::*, prelude::*}; -use bevy::{prelude::*, sprite::MaterialMesh2dBundle}; +use bevy::prelude::*; use examples_common_2d::ExampleCommonPlugin; fn main() { @@ -41,25 +41,22 @@ fn main() { } #[derive(Component)] -struct SpeculativeCollisionText; +struct SpeculativeCollisionEnabledText; #[derive(Component)] -struct SweptCcdText; +struct SweptCcdModeText; fn setup(mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); // Add two kinematic bodies spinning at high speeds. commands.spawn(( - SpriteBundle { - sprite: Sprite { - color: Color::srgb(0.7, 0.7, 0.8), - custom_size: Some(Vec2::new(1.0, 400.0)), - ..default() - }, - transform: Transform::from_xyz(-200.0, -200.0, 0.0), + Sprite { + color: Color::srgb(0.7, 0.7, 0.8), + custom_size: Some(Vec2::new(1.0, 400.0)), ..default() }, + Transform::from_xyz(-200.0, -200.0, 0.0), RigidBody::Kinematic, AngularVelocity(25.0), Collider::rectangle(1.0, 400.0), @@ -68,15 +65,12 @@ fn setup(mut commands: Commands) { SweptCcd::default(), )); commands.spawn(( - SpriteBundle { - sprite: Sprite { - color: Color::srgb(0.7, 0.7, 0.8), - custom_size: Some(Vec2::new(1.0, 400.0)), - ..default() - }, - transform: Transform::from_xyz(200.0, -200.0, 0.0), + Sprite { + color: Color::srgb(0.7, 0.7, 0.8), + custom_size: Some(Vec2::new(1.0, 400.0)), ..default() }, + Transform::from_xyz(200.0, -200.0, 0.0), RigidBody::Kinematic, AngularVelocity(-25.0), Collider::rectangle(1.0, 400.0), @@ -86,39 +80,46 @@ fn setup(mut commands: Commands) { )); // Setup help text. - let text_style = TextStyle { + let font = TextFont { font_size: 20.0, ..default() }; - commands.spawn(( - SpeculativeCollisionText, - TextBundle::from_sections([ - TextSection::new("(1) Speculative Collision: ", text_style.clone()), - TextSection::new("On", text_style.clone()), - ]) - .with_style(Style { - position_type: PositionType::Absolute, - top: Val::Px(30.0), - left: Val::Px(10.0), - ..default() - }), - )); - commands.spawn(( - SweptCcdText, - TextBundle::from_sections([ - TextSection::new("(2) Swept CCD: ", text_style.clone()), - TextSection::new( - "Non-linear (considers both translation and rotation)", - text_style, - ), - ]) - .with_style(Style { - position_type: PositionType::Absolute, - top: Val::Px(50.0), - left: Val::Px(10.0), - ..default() - }), - )); + commands + .spawn(( + Text::default(), + Node { + position_type: PositionType::Absolute, + top: Val::Px(30.0), + left: Val::Px(10.0), + ..default() + }, + )) + .with_children(|children| { + children.spawn((TextSpan::new("(1) Speculative Collision: "), font.clone())); + children.spawn(( + TextSpan::new("On"), + font.clone(), + SpeculativeCollisionEnabledText, + )); + }); + commands + .spawn(( + Text::default(), + Node { + position_type: PositionType::Absolute, + top: Val::Px(50.0), + left: Val::Px(10.0), + ..default() + }, + )) + .with_children(|children| { + children.spawn((TextSpan::new("(2) Swept CCD: "), font.clone())); + children.spawn(( + TextSpan::new("Non-linear (considers both translation and rotation)"), + font, + SweptCcdModeText, + )); + }); } /// Spawns balls moving at the spinning objects at high speeds. @@ -131,17 +132,13 @@ fn spawn_balls( let circle = Circle::new(2.0); // Compute the shooting direction. - let (sin, cos) = - (0.5 * time.elapsed_seconds_f64().adjust_precision().sin() - PI / 2.0).sin_cos(); + let (sin, cos) = (0.5 * time.elapsed_secs_f64().adjust_precision().sin() - PI / 2.0).sin_cos(); let direction = Vector::new(cos, sin).rotate(Vector::X); commands.spawn(( - MaterialMesh2dBundle { - mesh: meshes.add(circle).into(), - transform: Transform::from_xyz(0.0, 350.0, 0.0), - material: materials.add(Color::srgb(0.2, 0.7, 0.9)), - ..default() - }, + Mesh2d(meshes.add(circle)), + MeshMaterial2d(materials.add(Color::srgb(0.2, 0.7, 0.9))), + Transform::from_xyz(0.0, 350.0, 0.0), RigidBody::Dynamic, LinearVelocity(2000.0 * direction), Friction::ZERO.with_combine_rule(CoefficientCombine::Min), @@ -150,11 +147,20 @@ fn spawn_balls( } fn update_config( - mut speculative_collision_text: Query< - &mut Text, - (With, Without), + speculative_collision_text: Single< + &mut TextSpan, + ( + With, + Without, + ), + >, + swept_ccd_mode_text: Single< + &mut TextSpan, + ( + With, + Without, + ), >, - mut swept_ccd_text: Query<&mut Text, (Without, With)>, keys: Res>, mut narrow_phase_config: ResMut, mut ccd_bodies: Query<&mut SweptCcd>, @@ -163,36 +169,35 @@ fn update_config( // Note: This sets the default speculative margin, but it can be overridden // for individual entities with the `SpeculativeMargin` component. if keys.just_pressed(KeyCode::Digit1) { - let mut text = speculative_collision_text.single_mut(); + let mut text = speculative_collision_text; if narrow_phase_config.default_speculative_margin == Scalar::MAX { narrow_phase_config.default_speculative_margin = 0.0; - text.sections[1].value = "Off".to_string(); + text.0 = "Off".to_string(); } else { narrow_phase_config.default_speculative_margin = Scalar::MAX; - text.sections[1].value = "On".to_string(); + text.0 = "On".to_string(); } } // Change the sweep mode and whether swept CCD is enabled. if keys.just_pressed(KeyCode::Digit2) { - let mut text = swept_ccd_text.single_mut(); + let mut text = swept_ccd_mode_text; for mut swept_ccd in &mut ccd_bodies { if swept_ccd.mode == SweepMode::NonLinear && swept_ccd.include_dynamic { swept_ccd.mode = SweepMode::Linear; - text.sections[1].value = "Linear (considers only translation)".to_string(); + text.0 = "Linear (considers only translation)".to_string(); } else if swept_ccd.mode == SweepMode::Linear { // Disable swept CCD for collisions against dynamic bodies. // To disable it completely, you should remove the component. swept_ccd.include_dynamic = false; swept_ccd.mode = SweepMode::NonLinear; - text.sections[1].value = "Off".to_string(); + text.0 = "Off".to_string(); } else { // Enable swept CCD again. swept_ccd.include_dynamic = true; - text.sections[1].value = - "Non-linear (considers both translation and rotation)".to_string(); + text.0 = "Non-linear (considers both translation and rotation)".to_string(); } } } diff --git a/crates/avian2d/examples/chain_2d.rs b/crates/avian2d/examples/chain_2d.rs index 1da95f35..a9c5c9b8 100644 --- a/crates/avian2d/examples/chain_2d.rs +++ b/crates/avian2d/examples/chain_2d.rs @@ -1,11 +1,7 @@ #![allow(clippy::unnecessary_cast)] use avian2d::{math::*, prelude::*}; -use bevy::{ - prelude::*, - sprite::{MaterialMesh2dBundle, Mesh2dHandle}, - window::PrimaryWindow, -}; +use bevy::{prelude::*, window::PrimaryWindow}; use examples_common_2d::ExampleCommonPlugin; fn main() { @@ -31,11 +27,11 @@ fn setup( mut materials: ResMut>, mut meshes: ResMut>, ) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); let particle_count = 100; let particle_radius = 1.2; - let particle_mesh: Mesh2dHandle = meshes.add(Circle::new(particle_radius as f32)).into(); + let particle_mesh = meshes.add(Circle::new(particle_radius as f32)); let particle_material = materials.add(Color::srgb(0.2, 0.7, 0.9)); // Spawn kinematic particle that can follow the mouse @@ -43,11 +39,8 @@ fn setup( .spawn(( RigidBody::Kinematic, FollowMouse, - MaterialMesh2dBundle { - mesh: particle_mesh.clone(), - material: particle_material.clone(), - ..default() - }, + Mesh2d(particle_mesh.clone()), + MeshMaterial2d(particle_material.clone()), )) .id(); @@ -56,17 +49,10 @@ fn setup( let current_particle = commands .spawn(( RigidBody::Dynamic, - MassPropertiesBundle::new_computed(&Collider::circle(particle_radius), 1.0), - MaterialMesh2dBundle { - mesh: particle_mesh.clone(), - material: particle_material.clone(), - transform: Transform::from_xyz( - 0.0, - -i as f32 * (particle_radius as f32 * 2.0 + 1.0), - 0.0, - ), - ..default() - }, + MassPropertiesBundle::from_shape(&Circle::new(particle_radius as f32), 1.0), + Mesh2d(particle_mesh.clone()), + MeshMaterial2d(particle_material.clone()), + Transform::from_xyz(0.0, -i as f32 * (particle_radius as f32 * 2.0 + 1.0), 0.0), )) .id(); @@ -93,7 +79,7 @@ fn follow_mouse( if let Some(cursor_world_pos) = window .cursor_position() - .and_then(|cursor| camera.viewport_to_world_2d(camera_transform, cursor)) + .and_then(|cursor| camera.viewport_to_world_2d(camera_transform, cursor).ok()) { follower_position.translation = cursor_world_pos.extend(follower_position.translation.z); diff --git a/crates/avian2d/examples/collision_layers.rs b/crates/avian2d/examples/collision_layers.rs index e23fc824..83f723f3 100644 --- a/crates/avian2d/examples/collision_layers.rs +++ b/crates/avian2d/examples/collision_layers.rs @@ -1,7 +1,7 @@ #![allow(clippy::unnecessary_cast)] use avian2d::{math::*, prelude::*}; -use bevy::{prelude::*, sprite::MaterialMesh2dBundle}; +use bevy::prelude::*; use examples_common_2d::ExampleCommonPlugin; fn main() { @@ -33,19 +33,16 @@ fn setup( mut materials: ResMut>, mut meshes: ResMut>, ) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); // Spawn blue platform that belongs on the blue layer and collides with blue commands.spawn(( - SpriteBundle { - sprite: Sprite { - color: Color::srgb(0.2, 0.7, 0.9), - custom_size: Some(Vec2::new(500.0, 25.0)), - ..default() - }, - transform: Transform::from_xyz(0.0, -50.0, 0.0), + Sprite { + color: Color::srgb(0.2, 0.7, 0.9), + custom_size: Some(Vec2::new(500.0, 25.0)), ..default() }, + Transform::from_xyz(0.0, -50.0, 0.0), RigidBody::Static, Collider::rectangle(500.0, 25.0), CollisionLayers::new([Layer::Blue], [Layer::Blue]), @@ -53,15 +50,12 @@ fn setup( // Spawn red platform that belongs on the red layer and collides with red commands.spawn(( - SpriteBundle { - sprite: Sprite { - color: Color::srgb(0.9, 0.3, 0.3), - custom_size: Some(Vec2::new(500.0, 25.0)), - ..default() - }, - transform: Transform::from_xyz(0.0, -200.0, 0.0), + Sprite { + color: Color::srgb(0.9, 0.3, 0.3), + custom_size: Some(Vec2::new(500.0, 25.0)), ..default() }, + Transform::from_xyz(0.0, -200.0, 0.0), RigidBody::Static, Collider::rectangle(500.0, 25.0), CollisionLayers::new([Layer::Red], [Layer::Red]), @@ -75,16 +69,13 @@ fn setup( for x in -6..6 { for y in 0..4 { commands.spawn(( - MaterialMesh2dBundle { - mesh: marble_mesh.clone().into(), - material: blue_material.clone(), - transform: Transform::from_xyz( - x as f32 * 2.5 * marble_radius, - y as f32 * 2.5 * marble_radius + 200.0, - 0.0, - ), - ..default() - }, + Mesh2d(marble_mesh.clone()), + MeshMaterial2d(blue_material.clone()), + Transform::from_xyz( + x as f32 * 2.5 * marble_radius, + y as f32 * 2.5 * marble_radius + 200.0, + 0.0, + ), RigidBody::Dynamic, Collider::circle(marble_radius as Scalar), CollisionLayers::new([Layer::Blue], [Layer::Blue]), @@ -97,16 +88,13 @@ fn setup( for x in -6..6 { for y in -4..0 { commands.spawn(( - MaterialMesh2dBundle { - mesh: marble_mesh.clone().into(), - material: red_material.clone(), - transform: Transform::from_xyz( - x as f32 * 2.5 * marble_radius, - y as f32 * 2.5 * marble_radius + 200.0, - 0.0, - ), - ..default() - }, + Mesh2d(marble_mesh.clone()), + MeshMaterial2d(red_material.clone()), + Transform::from_xyz( + x as f32 * 2.5 * marble_radius, + y as f32 * 2.5 * marble_radius + 200.0, + 0.0, + ), RigidBody::Dynamic, Collider::circle(marble_radius as Scalar), CollisionLayers::new([Layer::Red], [Layer::Red]), diff --git a/crates/avian2d/examples/custom_collider.rs b/crates/avian2d/examples/custom_collider.rs index 9e063813..a8a89539 100644 --- a/crates/avian2d/examples/custom_collider.rs +++ b/crates/avian2d/examples/custom_collider.rs @@ -1,7 +1,9 @@ //! An example demonstrating how to make a custom collider and use it for collision detection. +#![allow(clippy::unnecessary_cast)] + use avian2d::{math::*, prelude::*}; -use bevy::{prelude::*, sprite::MaterialMesh2dBundle}; +use bevy::prelude::*; use examples_common_2d::ExampleCommonPlugin; fn main() { @@ -55,23 +57,8 @@ impl AnyCollider for CircleCollider { ColliderAabb::new(position, Vector::splat(self.radius)) } - fn mass_properties(&self, density: Scalar) -> ColliderMassProperties { - // In 2D, the Z length is assumed to be 1.0, so volume = area - let volume = PI * self.radius.powi(2); - let mass = density * volume; - let inertia = mass * self.radius.powi(2) / 2.0; - - ColliderMassProperties { - mass: Mass(mass), - inverse_mass: InverseMass(mass.recip()), - inertia: Inertia(inertia), - inverse_inertia: InverseInertia(inertia.recip()), - center_of_mass: CenterOfMass::default(), - } - } - // This is the actual collision detection part. - // It compute all contacts between two colliders at the given positions. + // It computes all contacts between two colliders at the given positions. fn contact_manifolds( &self, other: &Self, @@ -124,6 +111,25 @@ impl AnyCollider for CircleCollider { } } +// Implement mass computation for the collider shape. +// This is needed for physics to behave correctly. +impl ComputeMassProperties2d for CircleCollider { + fn mass(&self, density: f32) -> f32 { + // In 2D, the Z length is assumed to be `1.0`, so volume == area. + let volume = std::f32::consts::PI * self.radius.powi(2) as f32; + density * volume + } + + fn unit_angular_inertia(&self) -> f32 { + // Angular inertia for a circle, assuming a mass of `1.0`. + self.radius.powi(2) as f32 / 2.0 + } + + fn center_of_mass(&self) -> Vec2 { + Vec2::ZERO + } +} + // Note: This circle collider only supports uniform scaling. impl ScalableCollider for CircleCollider { fn scale(&self) -> Vector { @@ -146,7 +152,7 @@ fn setup( mut materials: ResMut>, mut meshes: ResMut>, ) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); let center_radius = 200.0; let particle_radius = 5.0; @@ -158,11 +164,8 @@ fn setup( // Spawn rotating body at the center. commands .spawn(( - MaterialMesh2dBundle { - mesh: meshes.add(Circle::new(center_radius)).into(), - material: materials.add(Color::srgb(0.7, 0.7, 0.8)).clone(), - ..default() - }, + Mesh2d(meshes.add(Circle::new(center_radius))), + MeshMaterial2d(materials.add(Color::srgb(0.7, 0.7, 0.8)).clone()), RigidBody::Kinematic, CircleCollider::new(center_radius.adjust_precision()), CenterBody, @@ -174,12 +177,9 @@ fn setup( for i in 0..count { let pos = Quat::from_rotation_z(i as f32 * angle_step) * Vec3::Y * center_radius; c.spawn(( - MaterialMesh2dBundle { - mesh: particle_mesh.clone().into(), - material: red.clone(), - transform: Transform::from_translation(pos).with_scale(Vec3::ONE * 5.0), - ..default() - }, + Mesh2d(particle_mesh.clone()), + MeshMaterial2d(red.clone()), + Transform::from_translation(pos).with_scale(Vec3::ONE * 5.0), CircleCollider::new(particle_radius.adjust_precision()), )); } @@ -192,16 +192,13 @@ fn setup( for x in -x_count / 2..x_count / 2 { for y in -y_count / 2..y_count / 2 { commands.spawn(( - MaterialMesh2dBundle { - mesh: particle_mesh.clone().into(), - material: blue.clone(), - transform: Transform::from_xyz( - x as f32 * 3.0 * particle_radius - 350.0, - y as f32 * 3.0 * particle_radius, - 0.0, - ), - ..default() - }, + Mesh2d(particle_mesh.clone()), + MeshMaterial2d(blue.clone()), + Transform::from_xyz( + x as f32 * 3.0 * particle_radius - 350.0, + y as f32 * 3.0 * particle_radius, + 0.0, + ), RigidBody::Dynamic, CircleCollider::new(particle_radius.adjust_precision()), LinearDamping(0.4), @@ -215,7 +212,7 @@ fn center_gravity( mut particles: Query<(&Transform, &mut LinearVelocity), Without>, time: Res