bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
36.24k stars 3.57k forks source link

Alternate reversable fixed-timestep Schedule and `Time` instance #12886

Closed CHATALOT1 closed 5 months ago

CHATALOT1 commented 7 months ago

What problem does this solve or what need does it fill?

What am I making?

The main game described in this issue will be a 2D top-down rogue-like inspired by the classic Snake game that I am currently working on, as that is the game that caused me to think of this suggestion. Within the game, I am using the fixed-timestep functionality that Bevy provides to represent 'game' time. Many things happen based on these units of time. For example:

Using Time::<Virtual>::pause, I intend to allow players to pause the advancement of this 'game time' at any time to think and plan, during which the game will still accept inputs, similar to functionality within the game FTL: Faster than Light. This serves both as a gameplay and accessibility feature in one.

Using Time::<Fixed>::set_timestep, I intend to allow players to adjust the speed of this 'game time', serving similar purposes.

The problem

I would also like to be able to provide functionality for re-winding, reversing the 'game time' described above. This could be useful in this game as a way of rewinding a few units of game time when a player dies (e.g. by hitting a wall), or to provide playback controls when viewing a replay of a save that involves re-simulating (similar to functionality provided by Factorio)

Who else would it help?

I don't think this functionality would only help my limited use case, however. Here are some other existing games that clearly rely on or include functionality similar to this:

If anyone has a suggestion to add to the above list, let me know.

What solution would you like?

My proposed solution is to implement the below additions, potentially behind a feature-gate:

This way, potentially with convenience methods provided by an extension trait on App, users could add two systems for each piece of functionality: One to do the simulation, and one to undo it. These would be added to the AdvanceReversableFixed and ReverseReversableFixed schedules respectively.

What alternative(s) have you considered?

There are alternate ways of providing this functionality, such as keeping track of World history and reverting it step-by-step, but this idea seems the most efficient, and the most widely useful

This functionality could be considered too niche to add to Bevy. In that case, I will likely create a library providing this functionality instead, as I am sure mine is not the only use-case.

Additional context

I am more than willing to implement this feature myself, if the maintainers decide it is worthwhile.

CHATALOT1 commented 7 months ago

I have thought of a much simpler solution that should meet my game's use case, at least. I am curious as to the efficiency differences between the two ideas, if any. Instead of creating two schedules and adding a version of the system to each, I have created a set of States:

/// Whether time is currently flowing, and its direction if so.
#[derive(States, Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum TimeFlowState {
    #[default]
    Paused,
    Advancing,
    Reversing,
}

An app extension then provides a convenience method that allows one to add systems that do and undo the desired reversable functionality:

impl ReversableTimeAppExt for App {
    /// Convenience method similar to `App::add_systems`, but automatically confines the provided
    /// `advancing_systems` to run only when in State `TimeFlowState::Advancing`, and the
    /// `reversing_systems` to run only when in State `TimeFlowState::Reversing`.
    ///
    /// It is assumed, although not enforced, that the systems passed as `reversing_systems` do the
    /// reverse of the systems passed as `advancing_systems`.
    fn add_reversable_systems<M>(
        &mut self,
        schedule: impl ScheduleLabel + Clone,
        advancing_systems: impl IntoSystemConfigs<M>,
        reversing_systems: impl IntoSystemConfigs<M>,
    ) -> &mut Self {
        self.add_systems(
            schedule.clone(),
            advancing_systems.run_if(in_state(TimeFlowState::Advancing)),
        )
        .add_systems(
            schedule,
            reversing_systems.run_if(in_state(TimeFlowState::Reversing)),
        )
    }
}
alice-i-cecile commented 5 months ago

I don't think this complexity is worth complicating the core use cases of FixedTime. I'm closing as Wont-Fix for now: this seems like something that can be readily customized on a per-game basis.