diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a6af20..d3e2498 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.3.0 - 2024-08-26 + +### Added + +- Add support for 3D sprites + ## 0.2.0 - 2024-07-06 ### Added diff --git a/Cargo.toml b/Cargo.toml index c0fe4d5..2ef330a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_spritesheet_animation" -version = "0.2.0" +version = "0.3.0" description = "A Bevy plugin for animating sprites" repository = "https://github.com/merwaaan/bevy_spritesheet_animation" readme="README.md" @@ -14,7 +14,7 @@ exclude = [ ] [dependencies] -bevy = { version = "0.14.0", default-features = false, features = ["bevy_sprite"] } +bevy = { version = "0.14.0", default-features = false, features = ["bevy_pbr", "bevy_sprite"] } itertools = "0.12.1" visibility = "0.1.0" @@ -24,6 +24,9 @@ bevy = { version = "0.14.0", default-features = true } criterion = "0.5.1" rand = "0.8.5" +[profile.test] +inherits = "release" + [features] # Enable this feature so that the tests/doctests get access to some private constructors. # diff --git a/README.md b/README.md index 8fc0ad3..74fe92c 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ bevy_spritesheet_animation is a [Bevy](https://bevyengine.org/) plugin for anima # Features +- Animate 2D and [3D sprites](#3d-sprites)! 🎉 - A single Bevy [component](https://docs.rs/bevy_spritesheet_animation/latest/bevy_spritesheet_animation/component/struct.SpritesheetAnimation.html) to add to your entities to play animations. - Tunable parameters: [duration](https://docs.rs/bevy_spritesheet_animation/latest/bevy_spritesheet_animation/animation/enum.AnimationDuration.html), [repetitions](https://docs.rs/bevy_spritesheet_animation/latest/bevy_spritesheet_animation/animation/enum.AnimationRepeat.html), [direction](https://docs.rs/bevy_spritesheet_animation/latest/bevy_spritesheet_animation/animation/enum.AnimationDirection.html), [easing](https://docs.rs/bevy_spritesheet_animation/latest/bevy_spritesheet_animation/easing/enum.Easing.html). - [Composable animations](https://docs.rs/bevy_spritesheet_animation/latest/bevy_spritesheet_animation/animation/struct.Animation.html) from multiple clips. @@ -58,7 +59,7 @@ fn setup( // See the `composition` example for more details. }); - // Spawn a sprite using Bevy's built-in SpriteSheetBundle + // Spawn a sprite using Bevy's built-in SpriteBundle let texture = assets.load("character.png"); @@ -71,12 +72,12 @@ fn setup( )); commands.spawn(( - SpriteSheetBundle { + SpriteBundle { texture, - atlas: TextureAtlas { - layout, - ..default() - }, + ..default() + }, + TextureAtlas { + layout, ..default() }, // Add a SpritesheetAnimation component that references our newly created animation @@ -178,7 +179,7 @@ fn spawn_enemies(mut commands: Commands, mut library: ResMut let animation_id = library.new_animation(|animation| { /* ... */ }); commands.spawn(( - SpriteSheetBundle { /* .... */ }, + SpriteBundle { /* .... */ }, SpritesheetAnimation::from_id(animation_id), )); } @@ -206,8 +207,8 @@ fn create_animation(mut library: ResMut) { // // #[derive(Resource)] // struct GameAnimations { - // enemy_running: Option, - // enemy_firing: Option, + // enemy_running: AnimationId, + // enemy_firing: AnimationId, // ... and so on ... // } @@ -221,7 +222,7 @@ fn spawn_enemies(mut commands: Commands, library: Res) { if let Some(animation_id) = libray.animation_with_name("enemy running") { for _ in 0..100 { commands.spawn(( - SpriteSheetBundle { /* .... */ }, + SpriteBundle { /* .... */ }, SpritesheetAnimation::from_id(animation_id), )); } @@ -229,18 +230,44 @@ fn spawn_enemies(mut commands: Commands, library: Res) { } ``` +## 3D sprites + +![A dozen of 3D sprites moving in 3D space](https://github.com/merwaaan/bevy_spritesheet_animation/raw/main/example3d.gif) + +This crate also makes it easy to integrate 3D sprites into your games, which is not supported by Bevy out of the box. + +[Sprite3DBundle](https://docs.rs/bevy_spritesheet_animation/latest/bevy_spritesheet_animation/components/sprite3d/struct.Sprite3DBundle.html) contains all the necesary components to enable 3D sprites. Use [Sprite3DBuilder](https://docs.rs/bevy_spritesheet_animation/latest/bevy_spritesheet_animation/components/sprite3d/struct.Sprite3DBuilder.html) to easily create one of those. + +Animating a 3D sprite is the same as animating 2D sprites: simply attach a [SpritesheetAnimation](https://docs.rs/bevy_spritesheet_animation/latest/bevy_spritesheet_animation/component/struct.SpritesheetAnimation.html) component to your entity. + +```rust +fn spawn_character(mut commands: Commands, mut library: ResMut) { + + let animation_id = library.new_animation(|animation| { /* ... */ }); + + commands.spawn(( + Sprite3DBuilder::from_image(texture.clone()) + .with_atlas(atlas_layout_handle) + .with_anchor(Anchor::BottomRight) + .build(), + SpritesheetAnimation::from_id(animation_id) + )); +} +``` + # More examples For more examples, browse the [examples/](examples) directory. | Example | Description | | -------------------------------------- | ------------------------------------------------------------------------ | -| [basic](examples/basic.rs) | Minimal example showing how to create an animated sprite | -| [composition](examples/composition.rs) | Advanced example showing how to create an animation with multiple stages | -| [parameters](examples/parameters.rs) | Shows the effect of each parameter | +| [basic](examples/basic.rs) | Shows how to create an animated sprite | +| [3d](examples/3d.rs) | Shows how to create 3D sprites | +| [composition](examples/composition.rs) | Shows how to create an animation with multiple stages | +| [parameters](examples/parameters.rs) | Shows the effect of each animation parameter | | [character](examples/character.rs) | Shows how to create a controllable character with multiple animations | | [events](examples/events.rs) | Shows how to react to animations reaching points of interest with events | -| [stress](examples/stress.rs) | A stress test with thousands of animated sprites | +| [stress](examples/stress.rs) | Stress test with thousands of animated sprites | # Compatibility diff --git a/example3d.gif b/example3d.gif new file mode 100644 index 0000000..4a9a1a7 Binary files /dev/null and b/example3d.gif differ diff --git a/examples/3d.rs b/examples/3d.rs new file mode 100644 index 0000000..b5c1d77 --- /dev/null +++ b/examples/3d.rs @@ -0,0 +1,181 @@ +// This example illustrates how to create 3D sprites. + +#[path = "./common/mod.rs"] +pub mod common; + +use bevy::{prelude::*, sprite::Anchor}; +use bevy_spritesheet_animation::prelude::*; +use rand::{seq::SliceRandom, Rng}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) + .add_plugins(SpritesheetAnimationPlugin) + .add_systems(Startup, setup) + .add_systems(Update, (update_on_keypress, orbit, draw_gizmos)) + .run(); +} + +fn setup( + mut commands: Commands, + mut library: ResMut, + mut atlas_layouts: ResMut>, + assets: Res, +) { + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(0.0, 0.0, 4000.0), + ..default() + }); + + // Create an animation + + let clip_id = library.new_clip(|clip| { + clip.push_frame_indices(Spritesheet::new(8, 8).row(3)); + }); + + let animation_id = library.new_animation(|animation| { + animation.add_stage(clip_id.into()); + }); + + // Create an image and an atlas layout like you would for any Bevy sprite + + let texture = assets.load("character.png"); + + let atlas_layout_handle = atlas_layouts.add(TextureAtlasLayout::from_grid( + UVec2::new(96, 96), + 8, + 8, + None, + None, + )); + + // Spawn 3D sprites + + // Orbiting sprites with various parameters + + let sprite_builders = [ + Sprite3DBuilder::from_image(texture.clone()), + Sprite3DBuilder::from_image(texture.clone()).with_flip(true, false), + Sprite3DBuilder::from_image(texture.clone()).with_flip(false, true), + Sprite3DBuilder::from_image(texture.clone()).with_flip(true, true), + Sprite3DBuilder::from_image(texture.clone()).with_anchor(Anchor::BottomLeft), + Sprite3DBuilder::from_image(texture.clone()).with_anchor(Anchor::BottomCenter), + Sprite3DBuilder::from_image(texture.clone()).with_anchor(Anchor::BottomRight), + Sprite3DBuilder::from_image(texture.clone()).with_anchor(Anchor::CenterLeft), + Sprite3DBuilder::from_image(texture.clone()).with_anchor(Anchor::Center), + Sprite3DBuilder::from_image(texture.clone()).with_anchor(Anchor::CenterRight), + Sprite3DBuilder::from_image(texture.clone()).with_anchor(Anchor::TopLeft), + Sprite3DBuilder::from_image(texture.clone()).with_anchor(Anchor::TopCenter), + Sprite3DBuilder::from_image(texture.clone()).with_anchor(Anchor::TopRight), + Sprite3DBuilder::from_image(texture.clone()).with_custom_size(Vec2::new(100.0, 400.0)), + ]; + + for (i, builder) in sprite_builders.iter().enumerate() { + commands.spawn(( + builder + .clone() + .with_atlas(atlas_layout_handle.clone()) + .build(), + SpritesheetAnimation::from_id(animation_id), + Orbit { + start_angle: i as f32 * std::f32::consts::TAU / sprite_builders.len() as f32, + }, + )); + } + + // Non-animated sprite in the center + + commands.spawn( + Sprite3DBuilder::from_image(texture.clone()) + .with_atlas(atlas_layout_handle.clone()) + .with_color(Color::linear_rgb(1.0, 0.0, 0.0)) + .build(), + ); + + // Text + + commands.spawn(TextBundle::from_section( + "C: random colors\nX: flip on X\nY: flip on Y\nA: random anchors\nS: random sizes\nR: reset", + TextStyle { + font_size: 30.0, + ..default() + }, + )); +} + +fn update_on_keypress(keyboard: Res>, mut sprites: Query<&mut Sprite3D>) { + let mut rng = rand::thread_rng(); + + for mut sprite in &mut sprites { + // Random color + + if keyboard.just_pressed(KeyCode::KeyC) { + sprite.color = Color::linear_rgb(rng.gen(), rng.gen(), rng.gen()); + } + + // Flip + + if keyboard.just_pressed(KeyCode::KeyX) { + sprite.flip_x = !sprite.flip_x; + } + + if keyboard.just_pressed(KeyCode::KeyY) { + sprite.flip_y = !sprite.flip_y; + } + + // Random anchors + + if keyboard.just_pressed(KeyCode::KeyA) { + static ANCHORS: [Anchor; 9] = [ + Anchor::BottomLeft, + Anchor::BottomCenter, + Anchor::BottomRight, + Anchor::CenterLeft, + Anchor::Center, + Anchor::CenterRight, + Anchor::TopLeft, + Anchor::TopCenter, + Anchor::TopRight, + ]; + + sprite.anchor = ANCHORS.choose(&mut rng).unwrap().clone(); + } + + // Random size + + if keyboard.just_pressed(KeyCode::KeyS) { + sprite.custom_size = Some(Vec2::new( + rng.gen_range(100.0..1000.0), + rng.gen_range(100.0..1000.0), + )); + } + + // Reset + + if keyboard.just_pressed(KeyCode::KeyR) { + sprite.color = Color::WHITE; + sprite.flip_x = false; + sprite.flip_y = false; + sprite.custom_size = None; + sprite.anchor = Anchor::default(); + } + } +} + +#[derive(Component)] +struct Orbit { + start_angle: f32, +} + +fn orbit(time: Res