#Saving bundles to spawn later

36 messages · Page 1 of 1 (latest)

next compass
#

I have some spawning code that randomly adds entities to a level, and I want to be able to save that level to a resource so it can be restarted by nuking it and re-spawning the same stuff in the same configuration. I tried saving:

  • a Vec of bundles. Doesn't work unless the bundles have contents with a specific known type, and mine are heterogeneously typed.
  • a random seed. Doesn't seem like a good option with nondeterministically ordered usage across multiple systems.
  • a Vec of closures that run spawn commands. This finally worked, after some finagling with the type system.

Is there a simpler way to do this? A subset of my code for reference:

#[derive(Resource, Default)]
struct Level {
  // A list of closures that spawn entities.
  spawn: Vec<Box<dyn Fn(&mut Commands) + Send + Sync>>,
}

fn add_foos_to_level(level: ResMut<Level>) {
  let bundle = (Foo, ...);
  level.spawn.push(Box::new(move |commands: &mut Commands| {
    commands.spawn(bundle.clone());
  }));
}

fn add_bars_to_level(level: ResMut<Level>) {
  let bundle = (Bar, ...);
  level.spawn.push(Box::new(move |commands: &mut Commands| {
    commands.spawn(bundle.clone());
  }));
}

fn spawn_level(mut commands: Commands, level: ResMut<Level>) {
  for spawn in level.spawn.iter() {
    spawn(&mut commands);
  }
}
bronze hull
#

To make sure I understand correctly, you're only trying to reproduce the initial state, and this isn't after some amount of gameplay?

#

Your phrasing of the question doesn't really make sense though, bundles aren't specific sets of instances of components, they are specific sets of types of components

#

You could also have a different rng source per thing that has deterministic order internally 🤔

soft ember
#

You can use sets of EntityCommands, that way you can insert a variety of bundles to a single entity. Mind you it wont care if you override a component in a later command.

#

And you wont need multiple "add_foobars_to_level".

soft ember
#

You could also label entities with empty components and reset them when they are supposed to be active again.

#

Leaving entities "spawned" but inactive is a common solution.

next compass
#

Yeah maybe I got the terminology mixed up. My use case is I'm trying to add a time loop component to the game, so I want to be able to revert the world back to a previous state, while carrying some specific state back in time.

So the cycle per level is:

  • create a new random level
  • for n time loops:
    • player plays through the level, record their actions as they go
    • reset the level to original state, but with an extra player character
    • replay the player's moves while the player does a new set of moves with the new character

And I'm trying to figure out the 'reset the level to the original state' bit.

Now that you mention it, there is a good chance I'll want to add some kind of incremental rewind/replay functionality, so perhaps a more general solution than just resetting the level would be better

#

I guess I'm confused about what a bundle was, I thought it was a tuple of components that we pass in to spawn commands. That's what I wanted to save in any case.

soft ember
#

That's exactly what a bundle is, a tuple of components, just not exclusive to just spawn commands. Anything that adds components to an entity can take a bundle.

next compass
#

Okay, cool, then I do want to be able to store bundles for later. The hard part is doing that in a way that make the type system happy without specifying which components are in the bundle at compile time

soft ember
#

To save bundles you'll need to impl a new trait for Bundles, because Bundle itself can't be converted into a vtable.

next compass
#
Vec<Box<dyn Fn(&mut Commands) + Send + Sync>>
```  does work, it is more analogous to a command pattern than actually saving bundles directly, but it does work for my use case
soft ember
#

That'll work. This'll work too:

trait BundleBox { ... }
impl<T: Bundle> BundleBox for T { ... }
next compass
#

oh, cool, I see what you mean there, that's very cool

soft ember
#

BundleBox might not be the correct name, but there is actually no more to it. It's a simplest way to wrap any trait .

#

traits are just constraints and promises, and Bundle doesn't make realistic promises for dynamic dispatch, so you just need to change the promises.

next compass
#

I get the idea but I think I may be a bit too new to Rust to turn it into working code. I'll give it a try though !

#

Would the ultimate type for a saved level then be e.g. a Vec<BundleBox> ? And to spawn a BundleBox I'd get a bundle of some kind out of it and pass it to commands.spawn() ?

#

Mostly what I'm getting at is, would this handle a mixture of bundle types in one vector? Like I suppose this is basically a type variance issue of some kind

soft ember
#

BundleBox would have to provide a function that takes self and Commands/EntityCommands (for .spawn() or .insert()). And the Vec would still have to be Vec<Box<dyn BundleBox>>.
Because Bundles and subsequently BundleBox can be any size, they need to be Boxed as they aren't a predictable size for Vec<>, and trait implementations are variadic by nature so must be marked dyn.

#

The nice thing is that you only will have to make this function once and the system or systems for generating them once.

next compass
#

Okay, I think I've got it working!

soft ember
#

Nice

next compass
#

trait BundleBox {
    fn apply_bundle(&self, commands: &mut Commands);
}
impl<T: Bundle + Clone> BundleBox for T {
    fn apply_bundle(&self, commands: &mut Commands) {
        commands.spawn(self.clone());
    }
}

#[derive(Resource, Default)]
struct Level {
    spawn: Vec<Box<dyn BundleBox + Send + Sync>>,
}

fn add_candies_to_level(mut level: ResMut<Level>, ...) {
    let mut rng = rand::thread_rng();
    for _ in 0..NUM_CANDIES {
        let bundle = (...); // Make a random candy
        level.spawn.push(Box::new(bundle));
    }
}

fn add_fuel_to_level(mut level: ResMut<Level>, ...) {
    let mut rng = rand::thread_rng();
    for _ in 0..NUM_FUEL {
        let bundle = (...); // Make a random fuel
        level.spawn.push(Box::new(bundle));
    }
}

fn spawn_level(mut commands: Commands, level: ResMut<Level>) {
    for spawn in level.spawn.iter() {
        spawn.apply_bundle(&mut commands);
    }
}
#

This is what you were suggesting basically? It is a lot like what I was doing, but manages to ditch the extra step of capturing the bundle in a closure.

soft ember
#

Yeah, avoids the extra work of having to wrap every single bundle you end up producing and wraps the logic so it's change once, works everywhere and much more extensible.

#

You can even take it one step further.

soft ember
#

Not supposed to be functional; just give the gist of it.

trait BundleBox {
    fn insert(&self, entity_commands: EntityCommands);
    fn spawn(&self, commands: Commands);
}
impl<T: Bundle + Clone> BundleBox for T {
    fn insert(&self, mut entity_commands: EntityCommands) {
        entity_commands.insert((*self).clone());
    }
    fn spawn(&self, mut commands: Commands) {
        commands.spawn((*self).clone());
    }
}

fn add_enemies(...) {
    // Just here for type illustration purposes
    let enemies = Vec<Vec<Box<dyn BundleBox>>>::new();
    enemies.resize(NUM_ENEMIES, Vec::new());

    for enemy in enemies {
        //Required bundle of components
        enemy.push(Box::new(EnemyBundle {...}));
        // Bonus bundles of components
        if (...) {
            enemy.push(Box::new(A {...}));
        }
        if (...) {
            enemy.push(Box::new(B {...}));
        }
        if (...) {
            enemy.push(Box::new(C {...}));
        }
    }
}

fn spawn_enemies(...) {
    for enemy in enemies {
        let entity_commands = commands.spawn_empty();
        for bundle in enemy {
            bundle.insert(entity_commands);
        }
    }
}
#

It just lets you give enemies multiple bundles of components. Basicly if you are going to manage things like enemy modifiers or enemy types in a different component you can basicly save them together and spawn them together.

#

I think if I go any deeper I'll exit the scope of the original question, so unless you ask for more or have questions; I'll stop my ramblings. 😅

#

In any case; I wish you good luck with your project! 🙂

bronze hull
#

I guess bundles do have specific instances, but they don't stay bundles past insertion

#

It kind of sounds like you want that thing RTS games do with their replays