├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── arrow.png ├── example.gif ├── gabe-idle-run.png ├── projectiles.png └── px.png ├── examples ├── basic.rs ├── directional.rs ├── local_space.rs ├── oneshot.rs ├── shape_emitter.rs ├── texture_atlas.rs ├── time_scaling.rs └── velocity_modifiers.rs └── src ├── components.rs ├── lib.rs ├── systems.rs └── values.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | *.png filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUST_VERSION: 1.79.0 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 30 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Install rust toolchain 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | toolchain: ${{ env.RUST_VERSION }} 23 | override: true 24 | profile: minimal 25 | 26 | - run: cargo check --all-targets 27 | - run: cargo test --no-default-features 28 | - run: cargo test 29 | - run: cargo test --all-features 30 | - run: cargo test --all-features -- --ignored 31 | 32 | code-style: 33 | runs-on: ubuntu-latest 34 | env: 35 | RUSTFLAGS: "-D warnings" 36 | steps: 37 | - uses: actions/checkout@v3 38 | - name: Install rust toolchain 39 | uses: actions-rs/toolchain@v1 40 | with: 41 | toolchain: ${{ env.RUST_VERSION }} 42 | override: true 43 | components: clippy, rustfmt 44 | - run: cargo fmt --all -- --check 45 | - run: cargo clippy --all-features --all-targets 46 | 47 | documentation: 48 | runs-on: ubuntu-latest 49 | env: 50 | RUSTDOCFLAGS: "-D warnings" 51 | steps: 52 | - uses: actions/checkout@v3 53 | - name: Install rust toolchain 54 | uses: actions-rs/toolchain@v1 55 | with: 56 | toolchain: nightly 57 | override: true 58 | profile: minimal 59 | - run: cargo doc --all-features --no-deps 60 | # unused-dependencies: 61 | # runs-on: ubuntu-latest 62 | # steps: 63 | # - uses: actions/checkout@v3 64 | # - uses: actions-rs/toolchain@v1 65 | # with: 66 | # toolchain: nightly 67 | # override: true 68 | # profile: minimal 69 | # - run: cargo install cargo-udeps --locked 70 | # - run: cargo udeps 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_particle_systems" 3 | version = "0.13.0" 4 | edition = "2021" 5 | rust-version = "1.79" 6 | authors = ["Abnormal Brain Studios"] 7 | license = "MIT" 8 | description = "A particle system plugin for Bevy" 9 | repository = "https://github.com/abnormalbrain/bevy_particle_systems" 10 | keywords = ["game", "gamedev", "bevy"] 11 | categories = ["game-development"] 12 | 13 | [profile.release] 14 | debug = true 15 | 16 | [dependencies] 17 | bevy_app = "0.14" 18 | bevy_asset = "0.14" 19 | bevy_ecs = "0.14" 20 | bevy_hierarchy = "0.14" 21 | bevy_math = "0.14" 22 | bevy_render = "0.14" 23 | bevy_color = "0.14" 24 | bevy_sprite = "0.14" 25 | bevy_time = "0.14" 26 | bevy_transform = "0.14" 27 | bevy_reflect = "0.14" 28 | rand = "0.8" 29 | 30 | [dev-dependencies] 31 | bevy = { version = "0.14", default-features=false, features = [ 32 | "bevy_asset", 33 | "bevy_sprite", 34 | "bevy_core_pipeline", 35 | "png", 36 | "bevy_winit", 37 | "x11" 38 | ] } 39 | approx = "0.5" 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Abnormal Brain Studios 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy_particle_systems 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/bevy_particle_systems)](https://crates.io/crates/bevy_particle_systems) 4 | [![docs](https://docs.rs/bevy_particle_systems/badge.svg)](https://docs.rs/bevy_particle_systems/) 5 | [![MIT](https://img.shields.io/crates/l/bevy_particle_systems)](./LICENSE) 6 | 7 | A native and WASM-compatible 2D particle system plugin for [bevy](https://bevyengine.org) 8 | 9 | **Note: This crate is still under development and its API may change between releases.** 10 | 11 | ## Example 12 | 13 | ![](https://github.com/abnormalbrain/bevy_particle_systems/blob/main/assets/example.gif) 14 | 15 | The above was captured running a release build of the `basic` example, `cargo run --example basic --release`, and ran at 190-200 FPS on a 16 | 2019 Intel i9 MacBook Pro, rendering about 10k particles. 17 | 18 | ``` 19 | INFO bevy diagnostic: frame_time : 5.125810ms (avg 5.211673ms) 20 | INFO bevy diagnostic: fps : 206.027150 (avg 204.176718) 21 | INFO bevy diagnostic: entity_count : 11358.713999 (avg 11341.450000) 22 | ``` 23 | 24 | ## Usage 25 | 26 | 1. Add the [`ParticleSystemPlugin`] plugin. 27 | 28 | ```rust 29 | use bevy::prelude::*; 30 | use bevy_particle_systems::ParticleSystemPlugin; 31 | 32 | fn main() { 33 | App::new() 34 | .add_plugins((DefaultPlugins, ParticleSystemPlugin)) // <-- Add the plugin 35 | // ... 36 | .add_systems(Startup, spawn_particle_system) 37 | .run(); 38 | } 39 | 40 | fn spawn_particle_system() { /* ... */ } 41 | ``` 42 | 43 | 2. Spawn a particle system whenever necessary. 44 | ```rust 45 | use bevy::prelude::*; 46 | use bevy_particle_systems::*; 47 | 48 | fn spawn_particle_system(mut commands: Commands, asset_server: Res) { 49 | commands 50 | // Add the bundle specifying the particle system itself. 51 | .spawn(ParticleSystemBundle { 52 | particle_system: ParticleSystem { 53 | max_particles: 10_000, 54 | texture: ParticleTexture::Sprite(asset_server.load("my_particle.png")), 55 | spawn_rate_per_second: 25.0.into(), 56 | initial_speed: JitteredValue::jittered(3.0, -1.0..1.0), 57 | lifetime: JitteredValue::jittered(8.0, -2.0..2.0), 58 | color: ColorOverTime::Gradient(Gradient::new(vec![ 59 | ColorPoint::new(Color::WHITE, 0.0), 60 | ColorPoint::new(Color::srgba(0.0, 0.0, 1.0, 0.0), 1.0), 61 | ])), 62 | looping: true, 63 | system_duration_seconds: 10.0, 64 | ..ParticleSystem::default() 65 | }, 66 | ..ParticleSystemBundle::default() 67 | }) 68 | // Add the playing component so it starts playing. This can be added later as well. 69 | .insert(Playing); 70 | } 71 | ``` 72 | 73 | ## Bevy Versions 74 | 75 | |`bevy_particle_systems`|`bevy`| 76 | |:--|:--| 77 | |0.13|0.14| 78 | |0.12|0.13| 79 | |0.11|0.12| 80 | |0.10|0.11| 81 | |0.9|0.10| 82 | |0.6 - 0.8|0.9| 83 | |0.5|0.8| 84 | |0.4|0.7| 85 | -------------------------------------------------------------------------------- /assets/arrow.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:218fe2891baa333643661508c6dcbafb8b29f88150ff12100bee685b19b63ced 3 | size 4560 4 | -------------------------------------------------------------------------------- /assets/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abnormalbrain/bevy_particle_systems/d19568be43383c386de72b81abba0464eb0b63d4/assets/example.gif -------------------------------------------------------------------------------- /assets/gabe-idle-run.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:48b8ac17973c0b6867c62747bd5c9d36a4fa8430b229e4c997ac7601ba9b2ce8 3 | size 1997 4 | -------------------------------------------------------------------------------- /assets/projectiles.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5885cd4e643dee42d9c81e2146db3badec9dff7552be07589b86aa9ef4cd72cc 3 | size 1187 4 | -------------------------------------------------------------------------------- /assets/px.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:63cb99ad2b5ddbd6044f1df437cdb5be8eed70e85358b540750937f31cc01f87 3 | size 81 4 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | diagnostic::{EntityCountDiagnosticsPlugin, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, 3 | prelude::*, 4 | window::{PresentMode, WindowPlugin}, 5 | DefaultPlugins, 6 | }; 7 | use bevy_color::palettes::basic::*; 8 | use bevy_particle_systems::{ 9 | ColorOverTime, Curve, CurvePoint, JitteredValue, ParticleBurst, ParticleSystem, 10 | ParticleSystemBundle, ParticleSystemPlugin, Playing, VelocityModifier::*, 11 | }; 12 | 13 | fn main() { 14 | App::new() 15 | .insert_resource(ClearColor(Color::BLACK)) 16 | .add_plugins(( 17 | EntityCountDiagnosticsPlugin, 18 | FrameTimeDiagnosticsPlugin, 19 | LogDiagnosticsPlugin::default(), 20 | )) 21 | .add_plugins(DefaultPlugins.set(WindowPlugin { 22 | primary_window: Some(Window { 23 | present_mode: PresentMode::AutoNoVsync, 24 | ..default() 25 | }), 26 | ..default() 27 | })) 28 | .add_plugins(ParticleSystemPlugin) // <-- Add the plugin 29 | .add_systems(Startup, startup_system) 30 | .run(); 31 | } 32 | 33 | fn startup_system(mut commands: Commands, asset_server: Res) { 34 | commands.spawn(Camera2dBundle::default()); 35 | 36 | commands 37 | .spawn(ParticleSystemBundle { 38 | particle_system: ParticleSystem { 39 | max_particles: 50_000, 40 | texture: asset_server.load("px.png").into(), 41 | spawn_rate_per_second: 1000.0.into(), 42 | initial_speed: JitteredValue::jittered(200.0, -50.0..50.0), 43 | velocity_modifiers: vec![Drag(0.01.into())], 44 | lifetime: JitteredValue::jittered(8.0, -2.0..2.0), 45 | color: ColorOverTime::Gradient(Curve::new(vec![ 46 | CurvePoint::new(PURPLE.into(), 0.0), 47 | CurvePoint::new(RED.into(), 0.5), 48 | CurvePoint::new(Color::srgba(0.0, 0.0, 1.0, 0.0), 1.0), 49 | ])), 50 | looping: true, 51 | system_duration_seconds: 10.0, 52 | max_distance: Some(300.0), 53 | scale: 2.0.into(), 54 | bursts: vec![ 55 | ParticleBurst::new(0.0, 1000), 56 | ParticleBurst::new(2.0, 1000), 57 | ParticleBurst::new(4.0, 1000), 58 | ParticleBurst::new(6.0, 1000), 59 | ParticleBurst::new(8.0, 1000), 60 | ], 61 | ..ParticleSystem::default() 62 | }, 63 | ..ParticleSystemBundle::default() 64 | }) 65 | .insert(Playing); 66 | } 67 | -------------------------------------------------------------------------------- /examples/directional.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | diagnostic::{EntityCountDiagnosticsPlugin, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, 3 | prelude::*, 4 | window::{PresentMode, Window, WindowPlugin}, 5 | DefaultPlugins, 6 | }; 7 | use bevy_app::PluginGroup; 8 | use bevy_asset::AssetServer; 9 | use bevy_particle_systems::{ 10 | CircleSegment, JitteredValue, ParticleSystem, ParticleSystemBundle, ParticleSystemPlugin, 11 | Playing, 12 | }; 13 | 14 | fn main() { 15 | App::new() 16 | .insert_resource(ClearColor(Color::BLACK)) 17 | .add_plugins(( 18 | EntityCountDiagnosticsPlugin, 19 | FrameTimeDiagnosticsPlugin, 20 | LogDiagnosticsPlugin::default(), 21 | )) 22 | .add_plugins(DefaultPlugins.set(WindowPlugin { 23 | primary_window: Some(Window { 24 | present_mode: PresentMode::AutoNoVsync, 25 | ..default() 26 | }), 27 | ..default() 28 | })) 29 | .add_plugins(ParticleSystemPlugin) // <-- Add the plugin 30 | .add_systems(Startup, startup_system) 31 | .run(); 32 | } 33 | 34 | fn startup_system(mut commands: Commands, asset_server: Res) { 35 | commands.spawn(Camera2dBundle::default()); 36 | commands 37 | .spawn(ParticleSystemBundle { 38 | particle_system: ParticleSystem { 39 | texture: asset_server.load("arrow.png").into(), 40 | spawn_rate_per_second: 25.0.into(), 41 | initial_speed: JitteredValue::jittered(70.0, -3.0..3.0), 42 | lifetime: JitteredValue::jittered(5.0, -1.0..1.0), 43 | emitter_shape: CircleSegment { 44 | radius: 10.0.into(), 45 | opening_angle: std::f32::consts::PI, 46 | direction_angle: std::f32::consts::PI / 2.0, 47 | } 48 | .into(), 49 | looping: true, 50 | scale: 0.07.into(), 51 | system_duration_seconds: 5.0, 52 | initial_rotation: (-90.0_f32).to_radians().into(), 53 | rotate_to_movement_direction: true, 54 | ..ParticleSystem::default() 55 | }, 56 | ..ParticleSystemBundle::default() 57 | }) 58 | .insert(Playing); 59 | } 60 | -------------------------------------------------------------------------------- /examples/local_space.rs: -------------------------------------------------------------------------------- 1 | //! This example demonstrates the difference between using particles in local and global space. 2 | //! 3 | //! The red colored particles operate in global space. Once they have been spawned they move independently. 4 | //! The green particles operate in local space. You can see that their movement is affected by the movement of the spawn point as well. 5 | use bevy::{ 6 | math::Vec3, 7 | prelude::{App, Camera2dBundle, Color, Commands, Component, Query, Res, Transform, With}, 8 | DefaultPlugins, 9 | }; 10 | use bevy_app::{Startup, Update}; 11 | use bevy_asset::AssetServer; 12 | use bevy_color::palettes::basic::*; 13 | use bevy_math::Quat; 14 | use bevy_time::Time; 15 | 16 | use bevy_particle_systems::{ 17 | CircleSegment, ColorOverTime, Curve, CurvePoint, JitteredValue, ParticleSpace, ParticleSystem, 18 | ParticleSystemBundle, ParticleSystemPlugin, Playing, 19 | }; 20 | 21 | #[derive(Debug, Component)] 22 | pub struct Targets { 23 | pub targets: Vec, 24 | pub index: usize, 25 | pub time: f32, 26 | } 27 | 28 | fn main() { 29 | App::new() 30 | .add_plugins((DefaultPlugins, ParticleSystemPlugin)) // <-- Add the plugin 31 | .add_systems(Startup, startup_system) 32 | .add_systems(Update, circler) 33 | .run(); 34 | } 35 | 36 | fn startup_system(mut commands: Commands, asset_server: Res) { 37 | commands.spawn(Camera2dBundle::default()); 38 | 39 | commands 40 | .spawn(ParticleSystemBundle { 41 | particle_system: ParticleSystem { 42 | max_particles: 500, 43 | emitter_shape: CircleSegment { 44 | opening_angle: std::f32::consts::PI * 0.25, 45 | ..Default::default() 46 | } 47 | .into(), 48 | texture: asset_server.load("px.png").into(), 49 | spawn_rate_per_second: 35.0.into(), 50 | initial_speed: JitteredValue::jittered(25.0, 0.0..5.0), 51 | lifetime: JitteredValue::jittered(3.0, -2.0..2.0), 52 | color: ColorOverTime::Gradient(Curve::new(vec![ 53 | CurvePoint::new(RED.into(), 0.0), 54 | CurvePoint::new(Color::srgba(0.0, 0.0, 0.0, 0.0), 1.0), 55 | ])), 56 | looping: true, 57 | system_duration_seconds: 10.0, 58 | space: ParticleSpace::World, 59 | scale: 8.0.into(), 60 | rotation_speed: 2.0.into(), 61 | ..ParticleSystem::default() 62 | }, 63 | transform: Transform::from_xyz(50.0, 50.0, 0.0), 64 | ..ParticleSystemBundle::default() 65 | }) 66 | .insert(Playing) 67 | .insert(Circler::new(Vec3::new(50.0, 0.0, 0.0), 50.0)); 68 | 69 | commands 70 | .spawn(ParticleSystemBundle { 71 | particle_system: ParticleSystem { 72 | max_particles: 500, 73 | emitter_shape: CircleSegment { 74 | opening_angle: std::f32::consts::PI * 0.25, 75 | direction_angle: std::f32::consts::PI, 76 | ..Default::default() 77 | } 78 | .into(), 79 | texture: asset_server.load("px.png").into(), 80 | spawn_rate_per_second: 35.0.into(), 81 | initial_speed: JitteredValue::jittered(25.0, 0.0..5.0), 82 | lifetime: JitteredValue::jittered(3.0, -2.0..2.0), 83 | color: ColorOverTime::Gradient(Curve::new(vec![ 84 | CurvePoint::new(GREEN.into(), 0.0), 85 | CurvePoint::new(Color::srgba(0.0, 0.0, 0.0, 0.0), 1.0), 86 | ])), 87 | looping: true, 88 | system_duration_seconds: 10.0, 89 | space: ParticleSpace::Local, 90 | scale: 8.0.into(), 91 | rotation_speed: JitteredValue::jittered(0.0, -6.0..0.0), 92 | ..ParticleSystem::default() 93 | }, 94 | transform: Transform::from_xyz(-50.0, 50.0, 0.0), 95 | ..ParticleSystemBundle::default() 96 | }) 97 | .insert(Playing) 98 | .insert(Circler::new(Vec3::new(-50.0, 0.0, 0.0), 50.0)); 99 | } 100 | 101 | #[derive(Component)] 102 | pub struct Circler { 103 | pub center: Vec3, 104 | pub radius: f32, 105 | } 106 | 107 | impl Circler { 108 | pub fn new(center: Vec3, radius: f32) -> Self { 109 | Self { center, radius } 110 | } 111 | } 112 | 113 | pub fn circler( 114 | time: Res