MrGVSV / bevy_proto

Create config files for entities in Bevy

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Question: Is it possible to use TextureAtlas?

maor1993 opened this issue · comments

I'm trying to use a spritesheet for one of the protos I'm working on and run into a sang with getting textureatlas

following this:
https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs
and trying to merge the ideas from this example:
https://github.com/MrGVSV/bevy_proto/blob/main/examples/bundles.rs

I created this code:

#[typetag::serde]
impl ProtoComponent for EnemySprite {
    fn insert_self(&self, commands: &mut ProtoCommands, _asset_server: &Res<AssetServer>) {
        // === Get Prepared Assets === //
        let texture: Handle<TextureAtlas> = commands
            .get_handle(self, &self.texture_path)
            .expect("Expected Image handle to have been created");

        // === Generate Bundle === //
        let my_bundle = SpriteSheetBundle  {
            texture_atlas:texture,
            ..default()
        };

        // === Insert Generated Bundle === //
        commands.insert(my_bundle);
    }

    fn prepare(&self, world: &mut World, prototype: &dyn Prototypical, data: &mut ProtoData) {
        // === Load Handles === //
        let asset_server = world.get_resource::<AssetServer>().unwrap();
        let handle:Handle<Image>=  asset_server.load(self.texture_path.as_str());
        let mut textureatlases = world.get_resource_mut::< Assets<TextureAtlas>>().unwrap();

        let textureatlas = TextureAtlas::from_grid(handle,Vec2 { x: 24.0, y: 24.0 },4,1,None,None);

        let texture_atlas_handle = textureatlases.add(textureatlas);

        println!("Built Textureatlas for {:?}",texture_atlas_handle);
        // === Save Handles === //
        data.insert_handle(prototype, self, texture_atlas_handle);
    }

However, get_handle fails as it expects id to be an AssetPathId

is there anything I can do or is this unsupported in bevy_proto?

Thanks.

Could you share the error message, as well as the type definition for EnemySprite?

sure,

#[derive(Clone, Serialize, Deserialize, Component)]
pub struct EnemySprite {
    pub texture_path: HandlePath,
}

regarding the error, insert_self panics at the

       let texture: Handle<TextureAtlas> = commands
           .get_handle(self, &self.texture_path)
           .expect("Expected Image handle to have been created");

due to returning None

Okay so there isn't currently a great way of achieving this right now. The reason is that ProtoCommands stores the handle by its handle ID. So for generated assets, you need the handle to access the handle 😅

You can sorta get this working using some interior mutability:

#[derive(Clone, Serialize, Deserialize, Component)]
pub struct EnemySprite {
  pub texture_path: HandlePath,
  #[serde(skip)]
  pub handle: RwLock<Option<Handle<TextureAtlas>>>, 
}

#[typetag::serde]
impl ProtoComponent for EnemySprite {
  fn insert_self(&self, commands: &mut ProtoCommands, _asset_server: &Res<AssetServer>) {
    let handle_id = self.handle.read().unwrap().as_ref().unwrap().id();
    let texture: Handle<TextureAtlas> = commands
      .get_handle(self, handle_id)
      .expect("Expected Image handle to have been created");
    // ...
  }

  fn prepare(&self, world: &mut World, prototype: &dyn Prototypical, data: &mut ProtoData) {
    // ...
    let texture_atlas_handle = textureatlases.add(textureatlas);
    *self.handle.write().unwrap() = Some(texture_atlas_handle);
  }
}

Ideally, though, we'd be able to map a custom string to the handle rather than relying on the actual asset path (which is a string for loaded assets but the handle ID for generated ones).

Are you working on that project anymore? I'm battling the exact same problem and it's a bit of a bummer, I could submit a PR if you don't have time.

Ideally, though, we'd be able to map a custom string to the handle

Looking at ProtoData this seems straightforward, but I could be missing something. Any comments, @MrGVSV ?

EDIT: spelling

I ended up not using bevy_proto.

@maor1993 What did you end up using?

I’m actually working on reviving #18 for Bevy 0.10. This should hopefully come with better asset management.

First of all, I really loved this add on. Thanks a lot @MrGVSV, having a great fun learning about developing a simple game in Rust/Bevy.

Just checking if anybody recently managed to get an example of sprite sheet animation working. I am trying to implement bevy 2D sprite-sheet example but couldn't figure out how to write the spawning section with ProtoCommands. I haven't really understand the requirements on how to write a proper bundle in *.prototype.ron files.

The original spawn spritesheet example reproduced below, but ProtoCommand.spawn only accepted ID, which I have setup to be "AntagonistSprite" as per "AntagonistSprite.prototype.ron" file.

commands.spawn((
        SpriteSheetBundle {
            texture_atlas: texture_atlas_handle,
            sprite: TextureAtlasSprite::new(animation_indices.first),
            transform: Transform::from_scale(Vec3::splat(6.0)),
            ..default()
        },
        animation_indices,
        AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
    ));

My simple spawn and prototype file (which I adapted from a working SpriteBundle) is as follows:

fn spawn(
    mut commands: ProtoCommands,
    mut proto_entity: Local<Option<Entity>>,
    mut proto_asset_events: EventReader<ProtoAssetEvent>,
    mut is_spawned: ResMut<IsAntagonistRootSpawned>,
) {
    let proto_entity = *proto_entity.get_or_insert_with(|| commands.spawn(PROTO_ID).id());

    // when the children has been de-spawned via despawn command, prepare to re-spawn when invoked via Appstate::Game
    // else re-spawn also when there is some changes to the assets (i.e. AntagonistSprite.prototype.ron)
    // this allows for spawn and despawn when transitioning from and to Appstate::Main_Menu
    if !is_spawned.value {
        commands.entity(proto_entity).insert(PROTO_ID);
        is_spawned.value = true;
    } else {
        for proto_asset_event in proto_asset_events.iter() {
            if proto_asset_event.is_modified(PROTO_ID) {
                commands.entity(proto_entity).insert(PROTO_ID);
            }
        }
    }
}
(
  name: "AntagonistSprite",
  schematics: {
    // For animation, use sprite sheet
    "bevy_proto::custom::SpriteSheetBundle": (
      texture_atlas: AssetPath("antagonist/gabe-idle-run.png"),
      sprite: TextureAtlasSpriteInput(
      // animation_indices.first value is 1, pretty sure this is wrong but don't know the right way for it
        index: 1
      )
    // clueless on where to start to add in animation indices and AnimationTimer
    ),
  }
)

First of all, I really loved this add on. Thanks a lot @MrGVSV, having a great fun learning about developing a simple game in Rust/Bevy.

Just checking if anybody recently managed to get an example of sprite sheet animation working. I am trying to implement bevy 2D sprite-sheet example but couldn't figure out how to write the spawning section with ProtoCommands. I haven't really understand the requirements on how to write a proper bundle in *.prototype.ron files.

The original spawn spritesheet example reproduced below, but ProtoCommand.spawn only accepted ID, which I have setup to be "AntagonistSprite" as per "AntagonistSprite.prototype.ron" file.

Glad you're enjoying it! I have a big update in the works to make working with assets a little nicer (this was going to be part of the initial reflection update, but I decided to split it off so I wasn't blocking a release on it). And as part of that I'm trying to make some of these bundles and assets much easier to define and configure via prototype files.

I'll make a note to include SpriteSheetBundle and friends in that. I'm on vacation right now, but hopefully I can get that finished by the end of the month (in other words, definitely not before Bevy v0.11 is released, but hopefully not too late after that either).

In the meantime, I'd recommend defining your own custom schematic (example). This can include all the data necessary to define and spawn your SpriteSheetBundle. Essentially, the apply method can pretty much look like the setup system in that example you linked.

Hope that helps!

Thank you very much for the kind and informative reply. Will go through the custom schematic further.

Have a great vacation! Again, thank you for the very nice addon.

Sharing on how I use the custom schematic for the SpriteSheetBundle, for reference to others who might have the same issue and search it here.

The code might be a bit convoluted, so my apologies in advance. There are 4 files (ron file for parent and sprite node, mod.rs and schematic.rs). The reason I am having two parent and separate sprite node is to ease despawning via despawn descendent.

// src/game/antagonist/schematic.rs
use super::{AnimationIndices, AnimationTimer};

pub struct SpriteSheetPlugin;

impl Plugin for SpriteSheetPlugin {
    fn build(&self, app: &mut App) {
        app
            // =============== //
        .register_type::<SpriteSheet>()
        // etc
        ;
    }
}

#[derive(Reflect)]
#[reflect(Schematic)]
struct SpriteSheet;

#[derive(Reflect, FromReflect)]
struct SpriteSheetInput {
    // The asset path of the image to load.
    image: String,
    // The first and last index from the sprite sheet
    animation_indices_first: usize,
    animation_indices_last: usize,
    tile_width: f32,
    tile_height: f32,
    atlas_column: usize,
    atlas_row: usize,
}

// This is where we actually define the logic for `SpriteSheet`.
impl Schematic for SpriteSheet {
    // Applying `SpriteSheetInput` type
    type Input = SpriteSheetInput;

    fn apply(input: &Self::Input, context: &mut SchematicContext) {
        // 1. Setup Animation
        let world = context.world_mut();
        // let change_tick = world.change_tick();
        // let last_change_tick = world.last_change_tick();

        let texture_handle = world.resource::<AssetServer>().load(&input.image);
        let texture_atlas = TextureAtlas::from_grid(
            texture_handle,
            Vec2::new(input.tile_width, input.tile_height),
            input.atlas_column,
            input.atlas_row,
            None,
            None,
        );
        let texture_atlas_handle = world
            .resource_mut::<Assets<TextureAtlas>>()
            .add(texture_atlas);
        // Use only the subset of sprites in the sheet that make up the run animation
        let animation_indices = AnimationIndices {
            first: input.animation_indices_first,
            last: input.animation_indices_last,
        };

        context.entity_mut().unwrap().insert((
            SpriteSheetBundle {
                texture_atlas: texture_atlas_handle,
                sprite: TextureAtlasSprite::new(animation_indices.first),
                transform: Transform::from_scale(Vec3::splat(6.0)),
                ..default()
            },
            animation_indices,
            AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
        ));
    }

    fn remove(_input: &Self::Input, context: &mut SchematicContext) {
        // It's important we handle any necessary cleanup when removing a schematic.
        context.entity_mut().unwrap().remove::<SpriteBundle>();
    }

    fn preload_dependencies(input: &mut Self::Input, dependencies: &mut DependenciesBuilder) {
        // This method is optional, but it allows us to preload our assets.
        let _: Handle<Image> = dependencies.add_dependency(input.image.clone());
    }
}
// src/game/antagonist/mod.rs

pub mod schematic;

use bevy::prelude::*;
use bevy_proto::prelude::*;
use schematic::SpriteSheetPlugin;

#[derive(Resource)]
pub struct IsAntagonistRootSpawned {
    pub value: bool,
}

impl Default for IsAntagonistRootSpawned {
    fn default() -> IsAntagonistRootSpawned {
        IsAntagonistRootSpawned { value: false }
    }
}

#[derive(Component, Schematic, Reflect, FromReflect, Debug)]
#[reflect(Schematic)]
pub struct AntagonistRoot;

#[derive(Component)]
struct AnimationIndices {
    first: usize,
    last: usize,
}

#[derive(Component, Deref, DerefMut)]
struct AnimationTimer(Timer);

pub struct AntagonistPlugin;

impl Plugin for AntagonistPlugin {
    fn build(&self, app: &mut App) {
        app
            // =============== //
            .register_type::<AntagonistRoot>()
            .add_plugin(SpriteSheetPlugin)
            .add_startup_system(load)
            .add_systems((
                spawn.run_if(prototype_ready("AntagonistRoot").and_then(run_once())),
                animate_sprite,
            ))
            // etc
            ;
    }
}

fn load(mut prototypes: PrototypesMut) {
    prototypes.load("schematics/antagonist/AntagonistRoot.prototype.ron");
}

fn spawn(mut commands: ProtoCommands) {
    commands.spawn("AntagonistRoot");
}

fn animate_sprite(
    time: Res<Time>,
    mut query: Query<(
        &AnimationIndices,
        &mut AnimationTimer,
        &mut TextureAtlasSprite,
    )>,
) {
    for (indices, mut timer, mut sprite) in &mut query {
        timer.tick(time.delta());
        if timer.just_finished() {
            sprite.index = if sprite.index == indices.last {
                indices.first
            } else {
                sprite.index + 1
            };
        }
    }
}
// assets/schematics/antagonist/AntagonistRoot.rs
(
  name: "AntagonistRoot",
  schematics: {
    "deck_proto::game::antagonist::AntagonistRoot": (),
    "bevy_proto::custom::SpatialBundle": (),
  },
  children: ["AntagonistSprite"]
)
// assets/schematics/antagonist/AntagonistCustom.rs
(
  name: "AntagonistCustom",
  schematics: {
    // Custom schematic to handle sprite sheet bundle
    "deck_proto::game::antagonist::schematic::SpriteSheet": (
      image: "antagonist/slime.png",
      animation_indices_first: 15,
      animation_indices_last: 21,
      tile_width: 32.0,
      tile_height: 32.0,
      atlas_column: 7,
      atlas_row: 5,
      
    )
  }
)