bevy_spritesheet 0.6.0

bevy extensions with support for selecting a sprite index depending on an associated state.
Documentation
#![doc = include_str!("../README.MD")]
#![warn(missing_docs)]

use bevy::prelude::*;

pub mod error;
pub mod multi_layer_sprite;
pub mod prelude;
mod sheet;

/// Plugin that initialise bevy_spritesheet
pub struct SpriteSheetPlugin;

use crate::prelude::*;

impl Plugin for SpriteSheetPlugin {
    fn build(&self, app: &mut App) {
        app.init_asset::<Sheet>()
            .init_asset_loader::<sheet::SheetLoader>()
            .init_asset::<multi_layer_sprite::LayersSource>()
            .init_asset_loader::<multi_layer_sprite::LayersSourceLoader>()
            .init_asset::<multi_layer_sprite::Configuration>()
            .init_asset_loader::<multi_layer_sprite::ConfigurationLoader>()
            .add_systems(PreUpdate, (update, update_children));
    }
}

/// The current value for the state for a sprite, or group of sprites.
/// This is a component that must be added to the sprite, so that `bevy_spritesheet`
/// can update the texture, depending on the state.
#[derive(Debug, Component)]
#[non_exhaustive]
pub struct SpriteStateValue {
    state: crate::prelude::SpriteState,
    sheet_handle: Handle<Sheet>,
}

impl SpriteStateValue {
    /// Create a new sprite state.
    pub fn new(
        state: crate::prelude::SpriteState,
        sheet_handle: &Handle<Sheet>,
    ) -> SpriteStateValue {
        let sheet_handle = sheet_handle.clone();
        SpriteStateValue {
            state,
            sheet_handle,
        }
    }
    /// Set the current state of the sprite, or a group of sprites.
    pub fn set_state(&mut self, state: SpriteState) {
        self.state = state;
    }
    /// Update the state, using the given functor
    pub fn update_state(&mut self, f: impl FnOnce(&SpriteState) -> SpriteState) {
        self.state = f(&self.state)
    }
}

fn update(
    mut query: Query<(&SpriteStateValue, &mut Sprite)>,
    time: Res<Time>,
    sheet_assets: Res<Assets<Sheet>>,
) -> bevy::prelude::Result<()> {
    let elapsed = time.elapsed();
    for (state, mut sprite) in query.iter_mut() {
        if let Some(sheet) = sheet_assets.get(&state.sheet_handle) {
            let indices = sheet.get(&state.state);
            let index = indices[((elapsed.as_millis() / sheet.animation.as_millis())
                % indices.len() as u128) as usize];

            let atlas = sprite.texture_atlas.as_mut().unwrap();
            atlas.index = index;
        }
    }
    Ok(())
}
fn update_children(
    state_query: Query<(&SpriteStateValue, &Children)>,
    mut sprite_query: Query<&mut Sprite>,
    time: Res<Time>,
    sheet_assets: Res<Assets<Sheet>>,
) -> bevy::prelude::Result<()> {
    let elapsed = time.elapsed();
    for (state, children) in state_query.iter() {
        if let Some(sheet) = sheet_assets.get(&state.sheet_handle) {
            let indices = sheet.get(&state.state);
            let index = indices[((elapsed.as_millis() / sheet.animation.as_millis())
                % indices.len() as u128) as usize];
            for child in children.iter() {
                // Query for the sprite
                let mut sprite = sprite_query.get_mut(child)?;

                let atlas = sprite.texture_atlas.as_mut().unwrap();
                atlas.index = index;
            }
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use bevy::{asset::LoadState, prelude::*};
    use std::time::Duration;

    fn is_loading(load_state: LoadState) -> bool {
        matches!(load_state, LoadState::Loading)
    }

    #[test]
    fn test_loading_asset() {
        let mut app = App::new();
        app.add_plugins((
            MinimalPlugins,
            AssetPlugin::default(),
            super::SpriteSheetPlugin,
        ));
        let handle: Handle<crate::Sheet> = app.world().resource::<AssetServer>().load("test.sheet");
        while is_loading(
            app.world()
                .resource::<AssetServer>()
                .get_load_state(&handle)
                .unwrap(),
        ) {
            app.update();
            std::thread::sleep(Duration::from_secs(1));
        }
        let sheet = app
            .world()
            .resource::<Assets<crate::Sheet>>()
            .get(&handle)
            .unwrap();
        use crate::prelude::{Direction, SpriteState};
        assert_eq!(
            *sheet.get(&SpriteState::Walk {
                direction: Direction::Right
            }),
            vec![30, 31]
        );
        assert_eq!(
            *sheet.get(&SpriteState::Idle {
                direction: Direction::Left
            }),
            vec![0]
        );
        assert_eq!(
            *sheet.get(&SpriteState::Run {
                direction: Direction::Bottom
            }),
            vec![15, 16, 17]
        );
    }
    #[test]
    fn test_changing_texture() {
        let mut app = App::new();
        app.add_plugins((
            MinimalPlugins,
            AssetPlugin::default(),
            super::SpriteSheetPlugin,
        ));
        app.init_asset::<TextureAtlasLayout>();

        app.add_systems(
            Startup,
            (
                |mut commands: Commands,
                 mut atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
                 asset_server: Res<AssetServer>| {
                    let atlas_layout = TextureAtlasLayout::new_empty(UVec2::ZERO);
                    let atlas_layout = atlas_layouts.add(atlas_layout);
                    let sheet_handle = asset_server.load("test.sheet");
                    commands.spawn((
                        Sprite {
                            texture_atlas: Some(TextureAtlas {
                                index: 0,
                                layout: atlas_layout.clone(),
                            }),
                            ..Default::default()
                        },
                        crate::SpriteStateValue::new(
                            crate::sheet::SpriteState::Idle {
                                direction: crate::sheet::Direction::Right,
                            },
                            &sheet_handle,
                        ),
                    ));
                    commands
                        .spawn((crate::SpriteStateValue::new(
                            crate::sheet::SpriteState::Idle {
                                direction: crate::sheet::Direction::Top,
                            },
                            &sheet_handle,
                        ),))
                        .with_child(Sprite {
                            texture_atlas: Some(TextureAtlas {
                                index: 0,
                                layout: atlas_layout,
                            }),
                            ..Default::default()
                        });
                },
            ),
        );
        app.update();
        app.add_systems(
            Update,
            (
                |query: Query<(&Sprite, &crate::SpriteStateValue)>| {
                    for (sprite, _) in query.iter() {
                        assert_eq!(sprite.texture_atlas.as_ref().unwrap().index, 1);
                    }
                },
                |state_query: Query<(&crate::SpriteStateValue, &Children)>,
                 sprite_query: Query<&Sprite>| {
                    for (_, children) in state_query.iter() {
                        for child in children.iter() {
                            // Query for the sprite
                            let sprite = sprite_query.get(child).unwrap();
                            assert_eq!(sprite.texture_atlas.as_ref().unwrap().index, 2);
                        }
                    }
                },
            ),
        );
        app.update();
    }
}

// 2) unit test for changing texture atlas