Skip to content

Commit

Permalink
Add support for 3D sprites, bump version to 0.3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
merwaaan committed Aug 26, 2024
1 parent 41e9502 commit 186b31b
Show file tree
Hide file tree
Showing 20 changed files with 731 additions and 87 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.3.0 - 2024-08-26

### Added

- Add support for 3D sprites

## 0.2.0 - 2024-07-06

### Added
Expand Down
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"

Expand All @@ -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.
#
Expand Down
55 changes: 41 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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");

Expand All @@ -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
Expand Down Expand Up @@ -178,7 +179,7 @@ fn spawn_enemies(mut commands: Commands, mut library: ResMut<SpritesheetLibrary>
let animation_id = library.new_animation(|animation| { /* ... */ });

commands.spawn((
SpriteSheetBundle { /* .... */ },
SpriteBundle { /* .... */ },
SpritesheetAnimation::from_id(animation_id),
));
}
Expand Down Expand Up @@ -206,8 +207,8 @@ fn create_animation(mut library: ResMut<SpritesheetLibrary>) {
//
// #[derive(Resource)]
// struct GameAnimations {
// enemy_running: Option<AnimationId>,
// enemy_firing: Option<AnimationId>,
// enemy_running: AnimationId,
// enemy_firing: AnimationId,
// ... and so on ...
// }

Expand All @@ -221,26 +222,52 @@ fn spawn_enemies(mut commands: Commands, library: Res<SpritesheetLibrary>) {
if let Some(animation_id) = libray.animation_with_name("enemy running") {
for _ in 0..100 {
commands.spawn((
SpriteSheetBundle { /* .... */ },
SpriteBundle { /* .... */ },
SpritesheetAnimation::from_id(animation_id),
));
}
}
}
```

## 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<SpritesheetLibrary>) {

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

Expand Down
Binary file added example3d.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
181 changes: 181 additions & 0 deletions examples/3d.rs
Original file line number Diff line number Diff line change
@@ -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<SpritesheetLibrary>,
mut atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
assets: Res<AssetServer>,
) {
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<ButtonInput<KeyCode>>, 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<Time>, mut query: Query<(&Orbit, &mut Transform)>) {
for (orbit, mut transform) in &mut query {
transform.translation.x = (orbit.start_angle + time.elapsed_seconds()).cos() * 1500.0;
transform.translation.z = (orbit.start_angle + time.elapsed_seconds()).sin() * 1500.0;
}
}

fn draw_gizmos(mut gizmos: Gizmos, sprites: Query<&Transform, With<Sprite3D>>) {
for &transform in &sprites {
gizmos.axes(transform, 100.0);
}
}
2 changes: 1 addition & 1 deletion examples/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ fn spawn_sprite(
let animation_id = library.animation_with_name("walk");

if let Some(id) = animation_id {
// Spawn a sprite with Bevy's built-in SpriteSheetBundle
// Spawn a sprite with Bevy's built-in SpriteBundle

commands.spawn((
SpriteBundle {
Expand Down
8 changes: 4 additions & 4 deletions examples/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use rand::Rng;
const DEFAULT_WINDOW_WIDTH: f32 = 1280.0;
const DEFAULT_WINDOW_HEIGHT: f32 = 720.0;

/// Returns the position of the nth item in a grid
pub fn grid_position(columns: u32, rows: u32, n: u32) -> Vec3 {
/// Returns the screen-space position of the nth item in a grid
pub fn grid_position(columns: u32, rows: u32, n: usize) -> Vec3 {
const MARGIN: f32 = 100.0;

let width = DEFAULT_WINDOW_WIDTH - MARGIN * 2.0;
Expand All @@ -14,8 +14,8 @@ pub fn grid_position(columns: u32, rows: u32, n: u32) -> Vec3 {
let xgap = width / columns.saturating_sub(1) as f32;
let ygap = height / rows.saturating_sub(1) as f32;

let x = (n % columns) as f32;
let y = (n / columns) as f32;
let x = (n as u32 % columns) as f32;
let y = (n as u32 / columns) as f32;

Vec3::new(
x * xgap - width / 2.0,
Expand Down
4 changes: 2 additions & 2 deletions examples/composition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ fn setup(
.set_default_repeat(1);
});

let anim_id = library.new_animation(|anim| {
let animation_id = library.new_animation(|anim| {
anim.add_stage(idle_clip_id.into())
.add_stage(run_clip_id.into())
.add_stage(shoot_clip_id.into())
Expand All @@ -80,7 +80,7 @@ fn setup(
layout,
..default()
},
SpritesheetAnimation::from_id(anim_id),
SpritesheetAnimation::from_id(animation_id),
));
}

Expand Down
2 changes: 1 addition & 1 deletion examples/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ fn setup(
.spawn((
SpriteBundle {
texture: texture.clone(),
transform: Transform::from_translation(grid_position(6, 6, index as u32)),
transform: Transform::from_translation(grid_position(6, 6, index)),
..default()
},
TextureAtlas {
Expand Down
Loading

0 comments on commit 186b31b

Please sign in to comment.