M Cargo.lock => Cargo.lock +107 -0
@@ 440,6 440,26 @@ dependencies = [
]
[[package]]
+name = "arboard"
+version = "3.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227"
+dependencies = [
+ "clipboard-win",
+ "image 0.25.6",
+ "log",
+ "objc2 0.6.1",
+ "objc2-app-kit 0.3.1",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation 0.3.1",
+ "parking_lot",
+ "percent-encoding",
+ "windows-sys 0.59.0",
+ "x11rb",
+]
+
+[[package]]
name = "arg_enum_proc_macro"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 1131,6 1151,45 @@ dependencies = [
]
[[package]]
+name = "bevy_egui"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd67c9ddb60d10926899c3eaa022aee43fb92ba507191c62e1ff935b323c5a98"
+dependencies = [
+ "arboard",
+ "bevy_app 0.16.1",
+ "bevy_asset 0.16.1",
+ "bevy_core_pipeline 0.16.1",
+ "bevy_derive 0.16.1",
+ "bevy_ecs 0.16.1",
+ "bevy_image",
+ "bevy_input 0.16.1",
+ "bevy_log 0.16.1",
+ "bevy_math 0.16.1",
+ "bevy_picking",
+ "bevy_platform",
+ "bevy_reflect 0.16.1",
+ "bevy_render 0.16.1",
+ "bevy_time 0.16.1",
+ "bevy_transform 0.16.1",
+ "bevy_window 0.16.1",
+ "bevy_winit 0.16.1",
+ "bytemuck",
+ "crossbeam-channel",
+ "egui",
+ "encase 0.10.0",
+ "image 0.25.6",
+ "js-sys",
+ "thread_local",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webbrowser",
+ "wgpu-types 24.0.0",
+ "winit 0.30.11",
+]
+
+[[package]]
name = "bevy_encase_derive"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 1587,6 1646,7 @@ dependencies = [
"bevy_ecs 0.16.1",
"bevy_input 0.16.1",
"bevy_math 0.16.1",
+ "bevy_mesh",
"bevy_platform",
"bevy_reflect 0.16.1",
"bevy_render 0.16.1",
@@ 1594,6 1654,7 @@ dependencies = [
"bevy_transform 0.16.1",
"bevy_utils 0.16.1",
"bevy_window 0.16.1",
+ "crossbeam-channel",
"tracing",
"uuid",
]
@@ 2924,6 2985,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
+name = "clipboard-win"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892"
+dependencies = [
+ "error-code",
+]
+
+[[package]]
name = "cmake"
version = "0.1.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 3836,6 3906,12 @@ dependencies = [
]
[[package]]
+name = "error-code"
+version = "3.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
+
+[[package]]
name = "euclid"
version = "0.22.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 5913,6 5989,7 @@ dependencies = [
"bitflags 2.9.1",
"objc2 0.6.1",
"objc2-core-foundation",
+ "objc2-core-graphics",
"objc2-foundation 0.3.1",
]
@@ 5964,6 6041,19 @@ dependencies = [
]
[[package]]
+name = "objc2-core-graphics"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4"
+dependencies = [
+ "bitflags 2.9.1",
+ "dispatch2",
+ "objc2 0.6.1",
+ "objc2-core-foundation",
+ "objc2-io-surface",
+]
+
+[[package]]
name = "objc2-core-image"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 6033,6 6123,17 @@ dependencies = [
]
[[package]]
+name = "objc2-io-surface"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c"
+dependencies = [
+ "bitflags 2.9.1",
+ "objc2 0.6.1",
+ "objc2-core-foundation",
+]
+
+[[package]]
name = "objc2-link-presentation"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 6210,6 6311,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2c1f9f56e534ac6a9b8a4600bdf0f530fb393b5f393e7b4d03489c3cf0c3f01"
dependencies = [
"num-traits",
+ "rand 0.8.5",
+ "serde",
]
[[package]]
@@ 6695,6 6798,7 @@ dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
+ "serde",
]
[[package]]
@@ 6734,6 6838,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.16",
+ "serde",
]
[[package]]
@@ 7651,12 7756,14 @@ dependencies = [
"aeronet_websocket",
"bevy 0.16.1",
"bevy_common_assets",
+ "bevy_egui",
"bevy_enoki",
"bevy_rapier2d 0.30.0",
"bevy_replicon",
"clap",
"console_error_panic_hook",
"getrandom 0.3.3",
+ "ordered-float 5.0.0",
"rand 0.9.1",
"serde",
"tracing-subscriber",
M crates/unified/Cargo.toml => crates/unified/Cargo.toml +6 -1
@@ 46,6 46,10 @@ aeronet_replicon = { version = "0.15", features = ["client"] }
aeronet_websocket = { version = "0.14", features = ["client"] }
aeronet_transport = "0.14"
+bevy_egui = "0.35"
+
+ordered-float = { version = "5", features = ["serde"] }
+
bevy_enoki = "0.4"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
@@ 62,4 66,5 @@ native = [
"aeronet_websocket/server",
"aeronet_replicon/server"
]
-wasm = ["getrandom/wasm_js"]>
\ No newline at end of file
+wasm = ["getrandom/wasm_js"]
+particle_editor = []<
\ No newline at end of file
M crates/unified/src/lib.rs => crates/unified/src/lib.rs +3 -1
@@ 24,4 24,6 @@ pub mod ecs;
pub mod server;
#[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
pub mod server_plugins;
-pub mod shared_plugins;>
\ No newline at end of file
+pub mod shared_plugins;
+#[cfg(all(not(target_arch = "wasm32"), feature = "particle_editor"))]
+pub mod particle_editor;<
\ No newline at end of file
M crates/unified/src/main.rs => crates/unified/src/main.rs +8 -2
@@ 26,6 26,8 @@ enum Cli {
#[arg(short = 'C', long)]
max_clients: usize,
},
+ #[cfg(all(not(target_arch = "wasm32"), feature = "particle_editor"))]
+ ParticleEditor {}
}
fn main() -> AppExit {
@@ 44,6 46,7 @@ fn main() -> AppExit {
match cli {
Cli::Client { server } => {
app.add_plugins(ClientPluginGroup { server });
+ app.add_plugins(SharedPluginGroup);
}
#[cfg(not(target_arch = "wasm32"))]
Cli::Server {
@@ 61,10 64,13 @@ fn main() -> AppExit {
tick_rate,
max_clients,
});
+ app.add_plugins(SharedPluginGroup);
+ },
+ #[cfg(all(not(target_arch = "wasm32"), feature = "particle_editor"))]
+ Cli::ParticleEditor {} => {
+ app.add_plugins(starkingdoms::particle_editor::particle_editor_plugin);
}
}
- app.add_plugins(SharedPluginGroup);
-
app.run()
}
A crates/unified/src/particle_editor/mod.rs => crates/unified/src/particle_editor/mod.rs +46 -0
@@ 0,0 1,46 @@
+use bevy::prelude::*;
+use bevy_egui::{EguiPlugin, EguiPrimaryContextPass};
+use crate::particles::{LifetimeCurve, ParticleEffect, RandF32, RandUsize, RandVec2};
+
+pub fn particle_editor_plugin(app: &mut App) {
+ app.add_plugins(DefaultPlugins);
+ app.add_plugins(EguiPlugin::default());
+ app.add_systems(EguiPrimaryContextPass, editor_ui);
+ app.add_systems(Startup, setup_editor_effect);
+}
+
+fn setup_editor_effect(mut commands: Commands) {
+ commands.spawn((
+ ParticleEffect {
+ lifetime_seconds: RandF32 {
+ value: 2.0,
+ randomness: 0.1,
+ },
+ batch_spawn_delay_seconds: RandF32 { value: 0.1, randomness: 0.05 },
+ particles_in_batch: RandUsize {
+ value: 1,
+ randomness: 1
+ },
+ initial_linear_velocity: RandVec2 {
+ x: RandF32 { value: 0.0, randomness: 0.5 },
+ y: RandF32 { value: 10.0, randomness: 1.0 }
+ },
+ initial_angular_velocity: RandF32 { value: 1.0, randomness: 0.5 },
+ scale: LifetimeCurve::new(&[
+ (0.0, 5.0),
+ (2.0, 0.0)
+ ]),
+ color: LifetimeCurve::new(&[
+ (0.0, Srgba::new(1.0, 0.0, 0.0, 1.0)),
+ (0.5, Srgba::new(0.0, 1.0, 0.0, 1.0)),
+ (1.0, Srgba::new(0.0, 0.0, 1.0, 1.0)),
+ (1.5, Srgba::new(1.0, 0.0, 1.0, 1.0)),
+ (2.0, Srgba::new(1.0, 1.0, 1.0, 1.0))
+ ]),
+ }
+ ));
+}
+
+fn editor_ui(app: &mut App) {
+
+}<
\ No newline at end of file
M crates/unified/src/particles.rs => crates/unified/src/particles.rs +62 -15
@@ 1,10 1,14 @@
-use bevy::color::LinearRgba;
+use std::collections::BTreeMap;
+use std::ops::Bound::{Included, Unbounded};
+use bevy::color::{Color, LinearRgba};
use bevy::math::cubic_splines::LinearSpline;
use bevy::math::Vec2;
+use bevy::prelude::Component;
+use ordered_float::{FloatCore, OrderedFloat};
use rand::Rng;
use serde::{Deserialize, Serialize};
-#[derive(Deserialize, Serialize)]
+#[derive(Deserialize, Serialize, Component)]
pub struct ParticleEffect {
// -- lifetime / spawning -- //
@@ 13,7 17,7 @@ pub struct ParticleEffect {
/// Delay inbetween each batch of particles spawned
pub batch_spawn_delay_seconds: RandF32,
/// Number of distinct particles spawned per batch
- pub particles_in_batch: RandF32,
+ pub particles_in_batch: RandUsize,
// -- velocity -- //
@@ 25,26 29,56 @@ pub struct ParticleEffect {
// -- scale -- //
// Scale curve over the lifetime of the particle
- pub scale: ScaleSpline,
+ pub scale: LifetimeCurve<f32>,
// -- color -- //
// Color curve over the lifetime of the particle
- pub color: ColorSpline,
+ pub color: LifetimeCurve<Color>,
}
-#[derive(Deserialize, Serialize)]
-pub struct ScaleSpline(Vec<f32>);
-impl From<ScaleSpline> for LinearSpline<f32> {
- fn from(value: ScaleSpline) -> Self {
- Self::new(value.0)
+#[derive(Serialize, Deserialize)]
+pub struct LifetimeCurve<P: Lerp>(BTreeMap<OrderedFloat<f32>, P>);
+impl<P: Lerp> LifetimeCurve<P> {
+ pub fn new(points: impl IntoIterator<Item = (f32, P)>) -> Self {
+ Self(BTreeMap::from_iter(points.into_iter().map(|u| (OrderedFloat(u.0), u.1))))
+ }
+
+ /// Sample for the value at T. Returns None if the curve has no points, or if T is outside
+ /// the domain of the points specified.
+ pub fn sample(&self, at: f32) -> Option<P> {
+ if self.0.is_empty() { return None; }
+
+ // Find A, the point "to the left" of `at`
+ let mut r1 = self.0.range((Included(OrderedFloat(at)), Unbounded));
+ let (a_key, a_val) = r1.next()?;
+
+ // Find B, the point "to the right" of `at`
+ let mut r2 = self.0.range((Unbounded, Included(OrderedFloat(at))));
+ let (b_key, b_val) = r2.next()?;
+
+ // Calculate `t` (value from 0 - 1 indicating our progress from A to B)
+ let t = at - *(a_key / (b_key - a_key));
+
+ // Lerp
+ Some(a_val.lerp(b_val, t))
}
}
-#[derive(Deserialize, Serialize)]
-pub struct ColorSpline(Vec<LinearRgba>);
-impl From<ColorSpline> for LinearSpline<LinearRgba> {
- fn from(value: ColorSpline) -> Self {
- Self::new(value.0)
+
+
+pub trait Lerp {
+ fn lerp(&self, other: &Self, t: f32) -> Self;
+}
+impl Lerp for f32 {
+ fn lerp(&self, other: &Self, t: f32) -> Self {
+ (1.0 - t) * self + t * other
+ }
+}
+impl Lerp for Color {
+ fn lerp(&self, other: &Self, t: f32) -> Self {
+ let a: LinearRgba = (*self).into();
+ let b: LinearRgba = (*other).into();
+ Color::from(a * (1.0 - t) + b * t)
}
}
@@ 61,6 95,19 @@ impl RandF32 {
}
#[derive(Deserialize, Serialize)]
+pub struct RandUsize {
+ pub value: usize,
+ pub randomness: usize
+}
+impl RandUsize {
+ pub fn sample(&self, rng: &mut impl Rng) -> usize {
+ let lower_bound = self.value.checked_sub(self.randomness).unwrap_or(0);
+ rng.random_range(lower_bound .. self.value+self.randomness)
+ }
+}
+
+
+#[derive(Deserialize, Serialize)]
pub struct RandVec2 {
pub x: RandF32,
pub y: RandF32,