use avian2d::{
math::{AdjustPrecision, PI, Scalar, Vector},
prelude::*,
};
use bevy::{
camera::ScalingMode, color::palettes::tailwind::CYAN_400,
input::common_conditions::input_just_pressed, prelude::*,
};
use bytemuck::{Pod, Zeroable};
const STEP_COUNT: usize = 500;
const ROWS: u32 = 30;
const COLUMNS: u32 = 4;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
PhysicsPlugins::default().with_length_unit(0.5),
PhysicsDebugPlugin,
))
.init_resource::<Step>()
.add_systems(Startup, (setup_scene, setup_ui))
.add_systems(FixedUpdate, update_hash)
.add_systems(
PreUpdate,
(clear_scene, setup_scene)
.chain()
.run_if(input_just_pressed(KeyCode::KeyR)),
)
.run();
}
#[derive(Resource, Default, Deref, DerefMut)]
struct Step(usize);
fn setup_scene(
mut commands: Commands,
mut materials: ResMut<Assets<ColorMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
commands.spawn((
Camera2d,
Projection::from(OrthographicProjection {
scaling_mode: ScalingMode::FixedHorizontal {
viewport_width: 40.0,
},
..OrthographicProjection::default_2d()
}),
Transform::from_xyz(0.0, 7.5, 0.0),
));
let ground_shape = Rectangle::new(40.0, 2.0);
commands.spawn((
Name::new("Ground"),
RigidBody::Static,
Collider::from(ground_shape),
Mesh2d(meshes.add(ground_shape)),
MeshMaterial2d(materials.add(Color::WHITE)),
Transform::from_xyz(0.0, -1.0, 0.0),
));
let half_size = 0.25;
let square_shape = Rectangle::new(2.0 * half_size, 2.0 * half_size);
let square_collider = Collider::from(square_shape);
let square_mesh = meshes.add(square_shape);
let square_material = materials.add(Color::from(CYAN_400));
let offset = 0.4 * half_size;
let delta_x = 10.0 * half_size;
let x_root = -0.5 * delta_x * (COLUMNS as f32 - 1.0);
for col in 0..COLUMNS {
let x = x_root + col as f32 * delta_x;
let mut prev_entity = None;
for row in 0..ROWS {
let entity = commands
.spawn((
Name::new("Square ({col}, {row})"),
RigidBody::Dynamic,
square_collider.clone(),
Mesh2d(square_mesh.clone()),
MeshMaterial2d(square_material.clone()),
Transform::from_xyz(
x + offset * row as f32,
half_size + 2.0 * half_size * row as f32,
0.0,
)
.with_rotation(Quat::from_rotation_z(0.1 * row as f32 - 1.0)),
))
.id();
if row & 1 == 0 {
prev_entity = Some(entity);
} else {
commands.spawn((
Name::new(format!(
"Revolute Joint ({}, {})",
prev_entity.unwrap(),
entity
)),
RevoluteJoint::new(prev_entity.unwrap(), entity)
.with_angle_limits(-0.1 * PI, 0.2 * PI)
.with_point_compliance(0.0001)
.with_local_anchor1(Vec2::splat(half_size).adjust_precision())
.with_local_anchor2(Vec2::new(offset, -half_size).adjust_precision()),
JointCollisionDisabled,
));
prev_entity = None;
}
}
}
}
#[derive(Component)]
struct StepText;
#[derive(Component)]
struct HashText;
fn setup_ui(mut commands: Commands) {
let font = TextFont {
font_size: 20.0,
..default()
};
commands
.spawn((
Text::new("Step: "),
font.clone(),
Node {
position_type: PositionType::Absolute,
top: Val::Px(5.0),
left: Val::Px(5.0),
..default()
},
))
.with_child((TextSpan::new("0"), font.clone(), StepText));
commands
.spawn((
Text::new("Hash: "),
font.clone(),
Node {
position_type: PositionType::Absolute,
top: Val::Px(30.0),
left: Val::Px(5.0),
..default()
},
))
.with_child((TextSpan::default(), font.clone(), HashText));
commands.spawn((
Text::new("Press R to reset scene"),
font.clone(),
Node {
position_type: PositionType::Absolute,
top: Val::Px(5.0),
right: Val::Px(5.0),
..default()
},
));
}
fn clear_scene(
mut commands: Commands,
query: Query<
Entity,
Or<(
With<RigidBody>,
With<Collider>,
With<RevoluteJoint>,
With<Camera>,
)>,
>,
mut step: ResMut<Step>,
) {
step.0 = 0;
for entity in &query {
commands.entity(entity).despawn();
}
}
#[derive(Pod, Zeroable, Clone, Copy)]
#[repr(C)]
struct Isometry {
translation: Vector,
rotation: Scalar,
}
fn update_hash(
transforms: Query<(&Position, &Rotation), With<RigidBody>>,
mut step_text: Single<&mut TextSpan, With<StepText>>,
mut hash_text: Single<&mut TextSpan, (With<HashText>, Without<StepText>)>,
mut step: ResMut<Step>,
) {
step_text.0 = step.to_string();
step.0 += 1;
if step.0 > STEP_COUNT {
return;
}
let mut hash = 5381;
for (position, rotation) in &transforms {
let isometry = Isometry {
translation: position.0,
rotation: rotation.as_radians(),
};
hash = djb2_hash(hash, bytemuck::bytes_of(&isometry));
}
if step.0 == STEP_COUNT {
hash_text.0 = format!("0x{:x} (step {})", hash, step.0);
} else {
hash_text.0 = format!("0x{:x}", hash);
}
}
fn djb2_hash(mut hash: u32, data: &[u8]) -> u32 {
for &byte in data {
hash = (hash << 5).wrapping_add(hash + byte as u32);
}
hash
}