M crates/unified/src/client/mod.rs => crates/unified/src/client/mod.rs +1 -0
@@ 7,6 7,7 @@ mod starfield;
mod ui;
mod planet;
mod zoom;
+mod particles;
use crate::client::incoming_particles::replicated_particles_plugin;
use crate::client::incoming_parts::incoming_parts_plugin;
M crates/unified/src/client/particles/mod.rs => crates/unified/src/client/particles/mod.rs +9 -0
@@ 0,0 1,9 @@
+use bevy::app::{App, Plugin};
+
+pub struct ParticlePlugin;
+
+impl Plugin for ParticlePlugin {
+ fn build(&self, app: &mut App) {
+
+ }
+}
A crates/unified/src/particle_editor/ecs.rs => crates/unified/src/particle_editor/ecs.rs +30 -0
@@ 0,0 1,30 @@
+use std::time::Duration;
+
+use bevy::{asset::Handle, ecs::component::Component, render::mesh::Mesh, sprite::ColorMaterial, time::{Timer, TimerMode}};
+
+use crate::particles::ParticleEffect;
+
+#[derive(Component)]
+pub struct Particle;
+
+#[derive(Component)]
+pub struct LifetimeTimer(pub Timer);
+impl LifetimeTimer {
+ pub fn new(lifetime: f32) -> LifetimeTimer {
+ LifetimeTimer(Timer::new(Duration::from_secs_f32(lifetime), TimerMode::Once))
+ }
+}
+
+#[derive(Component)]
+pub struct SpawnDelayTimer(pub Timer);
+impl SpawnDelayTimer {
+ pub fn new(delay: f32) -> SpawnDelayTimer {
+ SpawnDelayTimer(Timer::new(Duration::from_secs_f32(delay), TimerMode::Once))
+ }
+}
+
+#[derive(Component)]
+pub struct CircleMesh(pub Handle<Mesh>, pub Handle<ColorMaterial>);
+
+#[derive(Component)]
+pub struct ParentEffect(pub ParticleEffect);
A crates/unified/src/particle_editor/hooks.rs => crates/unified/src/particle_editor/hooks.rs +18 -0
@@ 0,0 1,18 @@
+use bevy::prelude::*;
+
+use crate::{particle_editor::ecs::SpawnDelayTimer, particles::ParticleEffect};
+
+pub fn hooks_plugin(app: &mut App) {
+ app.add_systems(Update, init_particle_effect);
+}
+
+fn init_particle_effect(
+ mut commands: Commands,
+ particle_effect: Query<(Entity, &ParticleEffect), Added<ParticleEffect>>,
+) {
+ for (entity, effect) in particle_effect {
+ commands.get_entity(entity).unwrap().insert(
+ SpawnDelayTimer::new(effect.batch_spawn_delay_seconds.sample(&mut rand::rng()))
+ );
+ }
+}
M crates/unified/src/particle_editor/mod.rs => crates/unified/src/particle_editor/mod.rs +13 -4
@@ 1,16 1,24 @@
use std::collections::BTreeMap;
use bevy::prelude::*;
use bevy_egui::{egui, EguiContexts, EguiPlugin, EguiPrimaryContextPass};
+use bevy_rapier2d::plugin::{NoUserData, RapierPhysicsPlugin};
use ordered_float::OrderedFloat;
use ron::ser::PrettyConfig;
-use crate::particles::{LifetimeCurve, ParticleEffect, RandF32, RandUsize, RandVec2};
+use crate::{particle_editor::{hooks::hooks_plugin, spawn::spawn_plugin}, particles::{LifetimeCurve, ParticleEffect, RandF32, RandUsize, RandVec2}};
+
+mod spawn;
+mod hooks;
+mod ecs;
pub fn particle_editor_plugin(app: &mut App) {
app.add_plugins(DefaultPlugins);
app.add_plugins(EguiPlugin::default());
+ app.add_plugins(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(10.0));
app.add_systems(Startup, setup_camera_system);
app.add_systems(EguiPrimaryContextPass, editor_ui);
app.add_systems(Startup, setup_editor_effect);
+ app.add_plugins(spawn_plugin);
+ app.add_plugins(hooks_plugin);
app.insert_resource(EditorResource {
ser_field: String::new(),
status: "Ready".to_string(),
@@ 50,12 58,13 @@ fn setup_editor_effect(mut commands: Commands) {
color: LifetimeCurve::new(&[
(0.0f32, Srgba::new(1.0, 0.0, 0.0, 1.0).into()),
]),
- }
+ },
+ Transform::from_xyz(0.0, 0.0, 0.0),
));
}
fn setup_camera_system(mut commands: Commands) {
- commands.spawn(Camera2d);
+ commands.spawn((Camera2d, Transform::from_scale(Vec3::splat(0.1))));
}
#[derive(Resource)]
@@ 229,4 238,4 @@ fn draw_rand_usize(v: &mut RandUsize, l: &str, ui: &mut egui::Ui) {
ui.label("variance:");
ui.add(egui::DragValue::new(&mut v.randomness).speed(0.1));
ui.end_row();
-}>
\ No newline at end of file
+}
A crates/unified/src/particle_editor/spawn.rs => crates/unified/src/particle_editor/spawn.rs +61 -0
@@ 0,0 1,61 @@
+use std::time::Duration;
+
+use bevy::prelude::*;
+use bevy_rapier2d::prelude::{RigidBody, Velocity};
+
+use crate::{particle_editor::ecs::{CircleMesh, LifetimeTimer, ParentEffect, Particle, SpawnDelayTimer}, particles::ParticleEffect};
+
+pub fn spawn_plugin(app: &mut App) {
+ app.add_systems(Update, spawn_particles);
+ app.add_systems(Update, lifetime_particles);
+}
+
+fn spawn_particles(
+ mut commands: Commands,
+ particle_effects: Query<(&Transform, &ParticleEffect, &mut SpawnDelayTimer)>,
+ time: ResMut<Time>,
+ mut meshes: ResMut<Assets<Mesh>>,
+ mut materials: ResMut<Assets<ColorMaterial>>,
+) {
+ for (transform, effect, mut delay_timer) in particle_effects {
+ delay_timer.0.tick(time.delta());
+ let circle = CircleMesh(meshes.add(Circle::new(1.0)),
+ materials.add(effect.color.sample(effect.color.clamp_time(0.0).unwrap()).unwrap()));
+ if delay_timer.0.just_finished() {
+ commands.spawn((
+ RigidBody::Dynamic,
+ Particle,
+ transform.with_scale(Vec3::splat(1.0)),
+ Mesh2d(circle.0.clone()),
+ MeshMaterial2d(circle.1.clone()),
+ Velocity {
+ linvel: effect.initial_linear_velocity.sample(&mut rand::rng()),
+ angvel: effect.initial_angular_velocity.sample(&mut rand::rng()),
+ },
+ LifetimeTimer(Timer::from_seconds(effect.lifetime_seconds.sample(&mut rand::rng()), TimerMode::Once)),
+ circle,
+ ParentEffect(effect.clone()),
+ ));
+ delay_timer.0.set_duration(Duration::from_secs_f32(effect.batch_spawn_delay_seconds.sample(&mut rand::rng())));
+ delay_timer.0.reset();
+ }
+ }
+}
+
+fn lifetime_particles(
+ mut commands: Commands,
+ mut particles: Query<(Entity, &mut LifetimeTimer, &CircleMesh, &ParentEffect), With<Particle>>,
+ time: ResMut<Time>,
+ mut meshes: ResMut<Assets<Mesh>>,
+ mut materials: ResMut<Assets<ColorMaterial>>,
+) {
+ for (entity, mut timer, circle, parent) in &mut particles {
+ timer.0.tick(time.delta());
+ materials.get_mut(&circle.1).unwrap().color = parent.0.color.sample(parent.0.color.clamp_time(timer.0.elapsed_secs()).unwrap()).unwrap();
+ if timer.0.just_finished() {
+ commands.entity(entity).despawn();
+ meshes.remove(&circle.0);
+ materials.remove(&circle.1);
+ }
+ }
+}
M crates/unified/src/particles.rs => crates/unified/src/particles.rs +12 -5
@@ 8,7 8,7 @@ use ordered_float::{FloatCore, OrderedFloat};
use rand::Rng;
use serde::{Deserialize, Serialize};
-#[derive(Deserialize, Serialize, Component)]
+#[derive(Deserialize, Serialize, Component, Clone)]
pub struct ParticleEffect {
// -- lifetime / spawning -- //
@@ 37,7 37,7 @@ pub struct ParticleEffect {
pub color: LifetimeCurve<Color>,
}
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, Clone)]
pub struct LifetimeCurve<P: Lerp>(pub BTreeMap<OrderedFloat<f32>, P>);
impl<P: Lerp + Copy> LifetimeCurve<P> {
pub fn new<'a>(points: impl IntoIterator<Item = &'a (f32, P)>) -> Self where P: 'a {
@@ 63,6 63,13 @@ impl<P: Lerp + Copy> LifetimeCurve<P> {
// Lerp
Some(a_val.lerp(b_val, t))
}
+
+ pub fn clamp_time(&self, time: f32) -> Option<f32> {
+ let Some(first) = self.0.iter().nth(0) else { return None };
+ let Some(last) = self.0.iter().last() else { return None };
+
+ Some(time.clamp(first.0.0, last.0.0))
+ }
}
@@ 83,7 90,7 @@ impl Lerp for Color {
}
-#[derive(Deserialize, Serialize)]
+#[derive(Deserialize, Serialize, Clone, Copy)]
pub struct RandF32 {
pub value: f32,
pub randomness: f32
@@ 94,7 101,7 @@ impl RandF32 {
}
}
-#[derive(Deserialize, Serialize)]
+#[derive(Deserialize, Serialize, Clone, Copy)]
pub struct RandUsize {
pub value: usize,
pub randomness: usize
@@ 107,7 114,7 @@ impl RandUsize {
}
-#[derive(Deserialize, Serialize)]
+#[derive(Deserialize, Serialize, Clone, Copy)]
pub struct RandVec2 {
pub x: RandF32,
pub y: RandF32,