#Structuring a upcoming event-based (instead of a tick-based) game

31 messages ยท Page 1 of 1 (latest)

humble kindle
#

I'm trying to create a basic 4x game with bevy in the style of Aurora (or Stellaris). I decided to base the game around handling upcoming events instead of a tick system. Basically:

  • There's a priority queue of upcoming events (e.g. a spaceship arriving at a star)
  • When the game is updated, (e.g. by 1 day or 5 seconds), a tick event is inserted
  • The game processes events until it reaches that event

Handling events requires a big system that takes &mut World because events have different requirements in terms of queries. (could use EventReader but they be out of order).

Ships are issued commands via the UI. There are two events for each command, a start type and a finish type (e.g. the start type of a GoTo command sets a ShipPosition::Travelling component and the finish type sets ShipPosition::At). Start events happen immediately.

Active commands for ships are stored and reference the relevant event so cancelled commands stop the event. Finished events need to remove the active command.

In the UI system, I need to check if it's possible to issue a command so that the button can be disabled. This is tricky because it requires borrowing the &World while still using EguiContexts etc. Not sure how to do this.

Key bits of code:

struct ActiveCommands(Slab<ActiveCommand>);

struct ActiveCommand {
  command: Command,
  event: u32,
}

struct Command {..}
impl Command {
  fn check_conditions(self, world: &World) -> Result<(), String> {..}
  // Returns the time the event finishes
  fn handle_start(self, world: &mut World, ship: Entity, time: f32) -> Result<f32, String> {..}
  fn handle_finish(self, world: &mut World, ship: Entity, time: f32) {..}
}

enum Event {
  Tick,
  FinishCommand { ship: Entity, command: usize },
  StartCommand { ship: Entity, command: Command },
}

struct Events {..}
impl Events {
  fn pop(&mut self) -> Option<(f32, Event)> {..}
  fn cancel(&mut self, id: u32) {..}
  fn push(&mut self, time: f32, event: Event) -> u32 {..}
}
#

Questions in order:

  • Are there any tools in Bevy that can help with this considerably?
  • Is there an alternative way I could structure my code?
  • Is the bevy ECS even the right tool for this?
vapid ibex
#

@nocturne mirage pinging for a link to your tutorial ๐Ÿ™‚

nocturne mirage
#

Handling events requires a big system that takes &mut World because events have different requirements in terms of queries. (could use EventReader but they be out of order).

The gist of it is to use system scheduling to prevent the EventReader from being out of order

#

This is done by using the .before and .after when registering systems

#

as well as placing the systems in system sets

#

This makes the event order always deterministic

#

Let me know if you would like direct examples @humble kindle

humble kindle
#

Can that handle a case where in a single update tick, the event order goes something like generate resources -> generate resources -> load onto ship -> generate resources? Where they're interleaved and on different timescales?

humble kindle
#

I suppose I could have a while lot of different #[derive(Event)] types that are then immediately handled? But I'm not sure what the point would be

nocturne mirage
#

it will be queued up with Command and run in the same tick

#

run_system_cached

humble kindle
#

I'm not sure a system that queues up events with Command would work. I need to process each event in order to work out whether more events should be processed or if the system should interrupt and pause

vapid ibex
humble kindle
#

do you mean like calling world.run_system?

#

Like at the moment I've got:

fn handle_event(event: Event, time: f32, world: &mut World) -> bool {...}

fn handle_frame_events(world: &mut World, mut commands: Commands) {
    if world.resource_mut::<Paused>().0 {
        return;
    }

    let tick_time = world.resource_mut::<Time>().0 + world.resource_mut::<Advance>().0;
    world.resource_mut::<Events>().push(tick_time, Event::Tick);
    while let Some((new_time, event)) = world.resource_mut::<Events>().pop() {
        world.resource_mut::<Time>().0 = new_time;

        if handle_event(event, new_time, world) {
            break;
        }

        if world.resource_mut::<PauseOnNextEvent>().0 {
            world.resource_mut::<Paused>().0 = true;
            break;
        }
    }
}

I don't love this but I'm not sure if something else would be better really

vapid ibex
humble kindle
#

I'd need to set some resource for the current event and then call that right? Unless there's some way to pass a value to a system

vapid ibex
#

Via the In param

humble kindle
#

oh okay cool, that works

humble kindle
#

I guess my main remaining question is how to get ui verification working nicely. Like how are you best meant to handle a situation where you have a button that does something, but can only be clicked in certain circumstances and should be greyed out at other times?

#

I don't think doing verification in the ui system is the right move. It's probably better to do some parsing of what the button should do in a system beforehand and store that result (or None if invalid)

vapid ibex
#

Which you check during both styling and when evaluating button presses

#

Or drive it so then the check flows to the button state, and also blocks evaluation of the system when run

humble kindle
#

I kinda mean like if you're doing 'parse don't verify' you can have the commands that the buttons do ready to go in something like this:

#[derive(Resource)]
struct AvailableCommands {
    do_stuff: Result<DoStuffCommand, String>,
    unload: Result<UnloadCommand, String>,
    go_to: HashMap<Entity, Result<GoToCommand, String>>
}
#

and then the only place you're doing verification is in the system that sets up those values

humble kindle
#

Okay so a better system I've come up with is to do dry runs of commands with a oneshot system like so:

pub fn start_unload(
    In((ship, dry_run)): In<(Entity, bool)>,
    mut ships: Query<(&ShipPosition, &mut Population)>,
    time: Res<Time>,
    mut finalized_commands: EventWriter<FinalizeCommand>,
) -> Result<(), String> {
    let (position, mut ship_population) = ships.get_mut(ship).unwrap();

    if ship_population.0 == 0 {
        return Err(format!("No pops to unload"));
    }

    let star = match position {
        ShipPosition::At(star) => star,
        ShipPosition::Travelling { .. } => return Err(format!("Cannot unload while travelling")),
    };

    if !dry_run {
        let population = ship_population.0;
        ship_population.0 = 0;

        finalized_commands.write(FinalizeCommand {
            ship,
            finish_time: time.0 + 1.0,
            command: Command::Unload(UnloadCommand {
                star: *star,
                population,
            }),
        });
    }

    Ok(())
}