docs/screenshot.png
+0
-0
diff --git a/docs/screenshot.png b/docs/screenshot.png
index ace83be..4334494 100644
Binary files a/docs/screenshot.png and b/docs/screenshot.png differ
src/confetti.rs
+112
-0
diff --git a/src/confetti.rs b/src/confetti.rs
new file mode 100644
index 0000000..b60e40a
@@ -0,0 +1,112 @@
use std::ops::Range;
use bevy::{
app::{App, Update},
asset::Assets,
camera::Camera3d,
color::Color,
ecs::{
component::Component,
entity::Entity,
event::Event,
observer::On,
query::With,
system::{Commands, Query, Res, ResMut, Single},
},
math::{Dir3, FloatPow, Quat, Vec3, primitives::Plane3d},
mesh::{Mesh, Mesh3d, Meshable},
pbr::{MeshMaterial3d, StandardMaterial},
time::Time,
transform::components::Transform,
};
use rand::RngExt;
use crate::Rng;
pub trait ConfettiSystems {
fn add_confetti_systems(&mut self) -> &mut Self;
}
impl ConfettiSystems for App {
fn add_confetti_systems(&mut self) -> &mut Self {
self.add_observer(spawn_confetti)
.add_systems(Update, update)
}
}
#[derive(Event)]
pub struct SpawnConfetti {
pub origin: Vec3,
pub direction: Dir3,
pub count: usize,
}
#[derive(Component)]
struct Confetti {
velocity: Vec3,
rotation: Quat,
}
const CONE_ANGLE: f32 = std::f32::consts::FRAC_PI_8;
const INITIAL_VELOCITY: Range<f32> = 7.5..12.5;
const ROTATION_VELOCITY: Range<f32> = std::f32::consts::FRAC_PI_8..std::f32::consts::PI;
fn spawn_confetti(
confetti: On<SpawnConfetti>,
camera: Single<&Transform, With<Camera3d>>,
mut rng: ResMut<Rng>,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
(0..confetti.count).for_each(|_| {
let velocity = sample_cone(&mut rng, confetti.direction, CONE_ANGLE)
* rng.random_range(INITIAL_VELOCITY);
let rotation = Quat::from_axis_angle(
(camera.translation - confetti.origin).normalize(),
rng.random_range(ROTATION_VELOCITY),
);
commands.spawn((
Confetti { velocity, rotation },
Transform::from_translation(confetti.origin).with_rotation(rotation),
Mesh3d(meshes.add(Plane3d::default().mesh().size(0.1, 0.2))),
MeshMaterial3d(materials.add(StandardMaterial::from_color(Color::hsl(
rng.random_range(0.0..360.0),
1.0,
0.5,
)))),
));
});
}
fn sample_cone(rng: &mut Rng, direction: Dir3, angle: f32) -> Dir3 {
let rotation = Quat::from_rotation_arc(Vec3::Z, *direction);
let z = rng.random_range(angle.cos()..=1.0);
let theta = rng.random_range(0.0..std::f32::consts::TAU);
rotation
* Dir3::new_unchecked(Vec3::new(
(1.0 - z.squared()).sqrt() * theta.cos(),
(1.0 - z.squared()).sqrt() * theta.sin(),
z,
))
}
fn update(
mut commands: Commands,
time: Res<Time>,
mut confetti: Query<(Entity, &mut Transform, &mut Confetti)>,
) {
confetti
.iter_mut()
.for_each(|(entity, mut transform, mut confetti)| {
if transform.translation.y < 0.0 {
commands.entity(entity).despawn();
return;
}
transform.translation += confetti.velocity * time.delta_secs();
transform.rotation *= confetti.rotation;
confetti.velocity.y -= 9.82 * time.delta_secs();
});
}
src/goal.rs
+94
-14
diff --git a/src/goal.rs b/src/goal.rs
index 6e92ff4..6cf18b4 100644
@@ -1,16 +1,25 @@
use std::{
ops::{Add, DerefMut, RangeInclusive},
time::Duration,
};
use bevy::{
app::{App, Startup, Update},
asset::Assets,
color::Color,
ecs::{
children,
component::Component,
query::With,
hierarchy::Children,
query::{With, Without},
system::{Commands, Query, Res, ResMut, Single},
},
math::{Quat, primitives::Circle},
log::warn,
math::{Dir3, Quat, Vec3, primitives::Circle},
mesh::{Mesh, Mesh3d},
pbr::{MeshMaterial3d, StandardMaterial},
text::{TextFont, TextSpan},
time::Time,
transform::components::Transform,
ui::widget::Text,
};
@@ -19,6 +28,7 @@ use rand::RngExt;
use crate::{
Rng,
bunnies::{Bunny, BunnyLocator},
confetti::SpawnConfetti,
};
pub trait GoalSystems {
@@ -31,16 +41,28 @@ impl GoalSystems for App {
}
}
const GOAL_AREA_WIDTH: RangeInclusive<f32> = -10.0..=10.0;
const GOAL_AREA_HEIGHT: RangeInclusive<f32> = -10.0..=10.0;
const GOAL_RADIUS: f32 = 5.0;
const GOAL_DURATION: f32 = 1.0;
const CELEBRATION_DURATION: f32 = 3.0;
#[derive(Component)]
struct Goal;
#[derive(Component, Default)]
enum Goal {
#[default]
NotAchieved,
InProgress {
start: Duration,
},
Achieved {
start: Duration,
},
}
#[derive(Component)]
struct GoalText;
fn setup(
mut rng: ResMut<Rng>,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
@@ -48,13 +70,14 @@ fn setup(
commands.spawn((
Mesh3d(meshes.add(Circle::new(GOAL_RADIUS))),
MeshMaterial3d(materials.add(Color::linear_rgb(0.0, 1.0, 0.0))),
Transform::from_xyz(
rng.random_range(-25.0..=25.0),
0.0,
rng.random_range(-25.0..=25.0),
)
.with_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
Goal,
Transform::from_xyz(10.0, 0.0, 10.0)
.with_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
Goal::default(),
children![(
Mesh3d(meshes.add(Circle::new(GOAL_RADIUS))),
MeshMaterial3d(materials.add(Color::linear_rgb(0.0, 0.0, 1.0))),
Transform::from_xyz(0.0, 0.0, 0.1).with_scale(Vec3::new(0.0, 0.0, 1.0)),
)],
));
commands
.spawn((
@@ -75,12 +98,69 @@ fn setup(
}
fn update(
mut commands: Commands,
locator: Res<BunnyLocator>,
bunnies: Query<(), With<Bunny>>,
goal: Single<&Transform, With<Goal>>,
time: Res<Time>,
mut rng: ResMut<Rng>,
mut goal: Single<(&mut Transform, &mut Goal, &Children)>,
mut goal_text: Single<&mut TextSpan, With<GoalText>>,
mut transforms: Query<&mut Transform, Without<Goal>>,
) {
let (transform, goal, children) = goal.deref_mut();
let count = bunnies.count();
let in_goal = locator.get_nearby_count(goal.translation, GOAL_RADIUS);
let in_goal = locator.get_nearby_count(transform.translation, GOAL_RADIUS);
**goal_text = format!("{in_goal}/{count}").into();
match (in_goal == count, goal.as_ref()) {
(_, Goal::Achieved { start }) => {
if start.add(Duration::from_secs_f32(CELEBRATION_DURATION)) < time.elapsed() {
**goal = Goal::NotAchieved;
transform.translation = Vec3::new(
rng.random_range(GOAL_AREA_WIDTH),
0.0,
rng.random_range(GOAL_AREA_HEIGHT),
);
}
}
(true, Goal::NotAchieved) => {
**goal = Goal::InProgress {
start: time.elapsed(),
}
}
(true, Goal::InProgress { start }) => {
let Some(Ok(mut child)) = children.first().map(|c| transforms.get_mut(*c)) else {
warn!("Cannot get child of goal");
return;
};
if start.add(Duration::from_secs_f32(GOAL_DURATION)) < time.elapsed() {
commands.trigger(SpawnConfetti {
origin: transform.translation,
direction: Dir3::Y,
count: 1000,
});
**goal = Goal::Achieved {
start: time.elapsed(),
};
child.scale = Vec3::ZERO;
} else {
let scale = 1.0
- (start.add(Duration::from_secs_f32(GOAL_DURATION)) - time.elapsed())
.as_secs_f32()
/ GOAL_DURATION;
child.scale = Vec3::new(scale, scale, 1.0);
}
}
(false, Goal::NotAchieved) => {}
(false, Goal::InProgress { .. }) => {
let Some(Ok(mut child)) = children.first().map(|c| transforms.get_mut(*c)) else {
warn!("Cannot get child of goal");
return;
};
**goal = Goal::NotAchieved;
child.scale = Vec3::new(0.0, 0.0, 1.0);
}
}
}
src/main.rs
+3
-1
diff --git a/src/main.rs b/src/main.rs
index 1cb0fc0..ed19ffd 100644
@@ -1,4 +1,5 @@
mod bunnies;
mod confetti;
mod dog;
mod goal;
mod obstacles;
@@ -23,7 +24,7 @@ use bevy::{
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
use crate::{bunnies::BunnySystems, dog::DogSystems, goal::GoalSystems};
use crate::{bunnies::BunnySystems, confetti::ConfettiSystems, dog::DogSystems, goal::GoalSystems};
fn main() {
App::new()
@@ -33,6 +34,7 @@ fn main() {
.add_dog_systems()
.add_bunny_systems()
.add_goal_systems()
.add_confetti_systems()
.run();
}