#How to incapsulate resource access into bundle methods?

48 messages · Page 1 of 1 (latest)

stray mortar
#

I am trying to create a bundle with a sprite sheet which incapsulates most of the creation logic. How do I access textures resource there? Is there a better way to do this?

#[derive(Bundle)]
pub struct PowerUpBundle {
    power_up: PowerUp,
    #[bundle]
    sprite_sheet: SpriteSheetBundle,
    #[bundle]
    sensor: (Collider, Sensor, ActiveEvents),
}

impl PowerUpBundle {
    pub fn new(power_up: PowerUp) -> Self {
        Self {
            power_up,
            sprite_sheet: match power_up {
                PowerUp::AttackSpeed => // get texture atlas
                PowerUp::RunSpeed => // get texture atlas
            },
            sensor: (
                Collider::cuboid(8., 12.),
                Sensor,
                ActiveEvents::COLLISION_EVENTS,
            ),
        }
    }
}
brisk nymph
#

I'm fairly certain your only option is to pass the resource (probably a borrow to it actually) as a parameter to the new function.

stray mortar
#

So what I want is -> inside the world setup function to just do something like PowerUpBundle::new(PowerUp::AttackSpeed, Vec2::new(100.0, 200.0))

#

I understand it's not achievable, but maybe there is another way around it

#

Maybe the problem I describe makes sense, it's just my solution that is wrong

#

Would it make it easier if I share the whole code for my PowerUpPlugin?

brisk nymph
#

Unfortunately, the only way to access a resource is by getting it from the ECS World through a system parameter; you either get it directly by taking a &World, or you use a Res<_> or ResMut<_>.

#

There simply is no way around passing something like that into your new method, or some other method that's later called on the bundle / entity.

stray mortar
#

Hm, maybe I am using whole bundle/plugin story just wrong?

#

It's like I can encapsulate the texture loading inside my plugin, but I can't encapsulate textures usage inside my plugin

#
pub struct PowerUpPlugin;

impl Plugin for PowerUpPlugin {
    fn build(&self, app: &mut App) {
        app.add_startup_system_to_stage(StartupStage::PreStartup, load_textures);
    }
}

#[derive(Resource)]
pub struct PowerUpSheet {
    pub move_speed: Handle<TextureAtlas>,
    pub attack_speed: Handle<TextureAtlas>,
}

fn load_textures(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
) {
    let sheets: [(&str, usize); 2] = [("move_speed", 1), ("attack_speed", 1)];
    let mut handles: [Handle<TextureAtlas>; 2] = Default::default();

    for (i, (sheet, columns)) in sheets.iter().enumerate() {
        let img = asset_server.load(format!("textures/powerups/{sheet}.png"));
        let atlas = TextureAtlas::from_grid(
            img,
            Vec2::splat(30.0),
            *columns,
            1,
            Some(Vec2::splat(2.)),
            None,
        );
        let atlas_handle = texture_atlases.add(atlas);
        handles[i] = atlas_handle;
    }

    let [move_speed, attack_speed] = handles;
    commands.insert_resource(PowerUpSheet {
        move_speed,
        attack_speed,
    });
}
brisk nymph
#

If you're ok adding other systems to the plugin, I can picture an option that I think keeps it contained:

  1. Have a marker component like NeedsTexture that is added as part of the bundle,
  2. Store the texture atlas handle(s) in a resource, which it looks like you're doing already,
  3. Have a system in your plugin that grabs every entity with the NeedsTexture tag, removes the tag, and assigns the right texture handle to it.
stray mortar
#

Oh, that sounds really good. Thank you!

#

Is it uncommon to go for what I am going here?

#

Cause it feels to me like a common problem and I am surprised that there is no "common" solution to this problem

brisk nymph
#

I think the specifically avoiding other inputs to the new function is a bit unusual, but I haven't really seen enough of other's bevy code to know tbh. This sort of temporary marker component pattern is pretty common though, at least as far as I know.

stray mortar
#

Ah, I see

#

Do you happen to have some reference to some repo using this pattern? Would love to dig a bit deeper

brisk nymph
#

You could also probably work something out with the Changed or Added query filters to avoid the marker component completely if you wanted

#

I can't think of one of the top of my head, I'll see if I can remember where I've seen it before for you. It might even be used in some of the examples in the bevy repo?

stray mortar
#

At least I haven't seen anything like it

#

I am also wondering how much performance penalty extra system gives.

Here is what I ended up with. It looks like it doesn't even need any extra data:

fn attach_textures(
    mut commands: Commands,
    power_ups: Query<(Entity, &PowerUp), Without<TextureAtlasSprite>>,
    power_ups_tex: Res<PowerUpSheet>,
) {
    for (entity, power_up) in power_ups.iter() {
        commands.entity(entity).insert(SpriteSheetBundle {
            texture_atlas: match power_up {
                PowerUp::AttackSpeed => power_ups_tex.attack_speed.clone(),
                PowerUp::MoveSpeed => power_ups_tex.move_speed.clone(),
            },
            transform: Transform {
                translation: Vec3::new(-200.0, 0.0, 0.0),
                ..Default::default()
            },
            ..Default::default()
        });
    }
}
brisk nymph
#

That looks like a good solution, for some reason it didn't process for me that you'd be adding a component anyway 😅

#

Just calling a system has very little overhead, particularly if the only thing it does is iterate through a query. When that query is empty it'll take a negligible amount of time

thorny scaffold
#

you can make your own commands extensions that can access resources with &world

brisk nymph
#

I've never messed around with custom commands so I wasn't sure what was possible in them, but if that would work it would probably end up nicer overall

thorny scaffold
#

you can just use even

#
commands.add(|world: &mut World| {
  let asset_server = world.resource::<AssetServer>();
  // .. load whatever resources and spawn the entity
});
#

well that's not any better umn

#
fn spawn_powerup(world: &mut World, powerup_type: PowerUp, translation: Vec3) {
  // .. spawn it using world access
}
#
commands.add(|world| spawn_powerup(world, powerup, translation));
#

is better

#

after you fix the syntax errors hehe

#

performance should be better than the two systems solution and it's less fragile

stray mortar
#

Do you mean the idea is to pass world into the PowerUpBundle::new?

#

It’s just that your function isn’t on the struct

thorny scaffold
#

yes you could do that

#
impl PowerUpBundle {
    pub fn new(world: &mut World, power_up: PowerUp) {
      let powerup_sheet = world.resource::<PowerUpSheet>();
      let sprite_sheet = match power_up {
        PowerUp::AttackSpeed => powerup_sheet.attack_speed.clone(),
        PowerUp::RunSpeed => powerup_sheet.move_speed.clone()
      };
      world.spawn(
        Self {
            power_up,
            sprite_sheet,
            sensor: (
                Collider::cuboid(8., 12.),
                Sensor,
                ActiveEvents::COLLISION_EVENTS,
            ),
        }
    }
}
#

something like that

#

and then

#
commands.add(|world: &mut World|{ PowerUpBundle::new(world, power_up) });
#

you probably wouldn't want to call the function new though I guess, spawn would be more descriptive

stray mortar
#

Oh, I see. Thank you!

stray mortar
#

It seems like I found another way which I like the most now

#
#[derive(Resource)]
pub struct PowerUpSheet {
    pub move_speed: Handle<TextureAtlas>,
    pub attack_speed: Handle<TextureAtlas>,
}

impl PowerUpSheet {
    pub fn new_power_up(&self, power_up: PowerUp, transform: Transform) -> PowerUpBundle {
        let texture_atlas = match power_up {
            PowerUp::AttackSpeed => self.attack_speed.clone(),
            PowerUp::MoveSpeed => self.move_speed.clone(),
        };

        PowerUpBundle {
            power_up,
            sprite_sheet: SpriteSheetBundle {
                texture_atlas,
                transform,
                ..Default::default()
            },
            sensor: (
                Collider::cuboid(8., 12.),
                Sensor,
                ActiveEvents::COLLISION_EVENTS,
            ),
        }
    }
}
#

And then somewhere in the world setup I just do

fn spawn_power_ups(mut commands: Commands, power_up_sheet: Res<PowerUpSheet>) {
    commands.spawn(
        power_up_sheet.new_power_up(PowerUp::MoveSpeed, Transform::from_xyz(250., -60., 0.)),
    );
    commands.spawn(
        power_up_sheet.new_power_up(PowerUp::MoveSpeed, Transform::from_xyz(-250., -60., 0.)),
    );
}
#

The added benefit is that I can change signature to fn spawn_power_ups(mut commands: Commands, power_up_sheet: Option<Res<PowerUpSheet>>) and then spawn is going to work only if plugin is added to the app.

I need to probably come with a better naming though, since Sheet is not like actual texture sheet but is also a power app factory kind of thing.