Add insert_state to App. (#11043)

# Objective

Fix #10731.

## Solution

Rename `App::add_state<T>(&mut self)` to `init_state`, and add
`App::insert_state<T>(&mut self, state: T)`. I decided on these names
because they are more similar to `init_resource` and `insert_resource`.

I also removed the `States` trait's requirement for `Default`. Instead,
`init_state` requires `FromWorld`.

---

## Changelog

- Renamed `App::add_state` to `init_state`.
- Added `App::insert_state`.
- Removed the `States` trait's requirement for `Default`.

## Migration Guide

- Renamed `App::add_state` to `init_state`.
This commit is contained in:
Doonv 2023-12-21 16:09:24 +02:00 committed by GitHub
parent 42f721382c
commit ba0f8f996f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 67 additions and 18 deletions

View file

@ -348,6 +348,10 @@ impl App {
self.plugins_state = PluginsState::Cleaned;
}
/// Initializes a [`State`] with standard starting values.
///
/// If the [`State`] already exists, nothing happens.
///
/// Adds [`State<S>`] and [`NextState<S>`] resources, [`OnEnter`] and [`OnExit`] schedules
/// for each state variant (if they don't already exist), an instance of [`apply_state_transition::<S>`] in
/// [`StateTransition`] so that transitions happen before [`Update`](crate::Update) and
@ -360,8 +364,44 @@ impl App {
///
/// Note that you can also apply state transitions at other points in the schedule
/// by adding the [`apply_state_transition`] system manually.
pub fn add_state<S: States>(&mut self) -> &mut Self {
self.init_resource::<State<S>>()
pub fn init_state<S: States + FromWorld>(&mut self) -> &mut Self {
if !self.world.contains_resource::<State<S>>() {
self.init_resource::<State<S>>()
.init_resource::<NextState<S>>()
.add_systems(
StateTransition,
(
run_enter_schedule::<S>.run_if(run_once_condition()),
apply_state_transition::<S>,
)
.chain(),
);
}
// The OnEnter, OnExit, and OnTransition schedules are lazily initialized
// (i.e. when the first system is added to them), and World::try_run_schedule is used to fail
// gracefully if they aren't present.
self
}
/// Inserts a specific [`State`] to the current [`App`] and
/// overrides any [`State`] previously added of the same type.
///
/// Adds [`State<S>`] and [`NextState<S>`] resources, [`OnEnter`] and [`OnExit`] schedules
/// for each state variant (if they don't already exist), an instance of [`apply_state_transition::<S>`] in
/// [`StateTransition`] so that transitions happen before [`Update`](crate::Update) and
/// a instance of [`run_enter_schedule::<S>`] in [`StateTransition`] with a
/// [`run_once`](`run_once_condition`) condition to run the on enter schedule of the
/// initial state.
///
/// If you would like to control how other systems run based on the current state,
/// you can emulate this behavior using the [`in_state`] [`Condition`].
///
/// Note that you can also apply state transitions at other points in the schedule
/// by adding the [`apply_state_transition`] system manually.
pub fn insert_state<S: States>(&mut self, state: S) -> &mut Self {
self.insert_resource(State::new(state))
.init_resource::<NextState<S>>()
.add_systems(
StateTransition,
@ -1068,7 +1108,7 @@ mod tests {
#[test]
fn add_systems_should_create_schedule_if_it_does_not_exist() {
let mut app = App::new();
app.add_state::<AppState>()
app.init_state::<AppState>()
.add_systems(OnEnter(AppState::MainMenu), (foo, bar));
app.world.run_schedule(OnEnter(AppState::MainMenu));
@ -1079,7 +1119,7 @@ mod tests {
fn add_systems_should_create_schedule_if_it_does_not_exist2() {
let mut app = App::new();
app.add_systems(OnEnter(AppState::MainMenu), (foo, bar))
.add_state::<AppState>();
.init_state::<AppState>();
app.world.run_schedule(OnEnter(AppState::MainMenu));
assert_eq!(app.world.entities().len(), 2);

View file

@ -5,6 +5,7 @@ use std::ops::Deref;
use crate as bevy_ecs;
use crate::change_detection::DetectChangesMut;
use crate::prelude::FromWorld;
#[cfg(feature = "bevy_reflect")]
use crate::reflect::ReflectResource;
use crate::schedule::ScheduleLabel;
@ -40,7 +41,7 @@ pub use bevy_ecs_macros::States;
/// }
///
/// ```
pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug + Default {}
pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug {}
/// The label of a [`Schedule`](super::Schedule) that runs whenever [`State<S>`]
/// enters this state.
@ -72,12 +73,8 @@ pub struct OnTransition<S: States> {
/// [`apply_state_transition::<S>`] system.
///
/// The starting state is defined via the [`Default`] implementation for `S`.
#[derive(Resource, Default, Debug)]
#[cfg_attr(
feature = "bevy_reflect",
derive(bevy_reflect::Reflect),
reflect(Resource, Default)
)]
#[derive(Resource, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct State<S: States>(S);
impl<S: States> State<S> {
@ -94,6 +91,12 @@ impl<S: States> State<S> {
}
}
impl<S: States + FromWorld> FromWorld for State<S> {
fn from_world(world: &mut World) -> Self {
Self(S::from_world(world))
}
}
impl<S: States> PartialEq<S> for State<S> {
fn eq(&self, other: &S) -> bool {
self.get() == other
@ -113,7 +116,7 @@ impl<S: States> Deref for State<S> {
/// To queue a transition, just set the contained value to `Some(next_state)`.
/// Note that these transitions can be overridden by other systems:
/// only the actual value of this resource at the time of [`apply_state_transition`] matters.
#[derive(Resource, Default, Debug)]
#[derive(Resource, Debug)]
#[cfg_attr(
feature = "bevy_reflect",
derive(bevy_reflect::Reflect),
@ -121,6 +124,12 @@ impl<S: States> Deref for State<S> {
)]
pub struct NextState<S: States>(pub Option<S>);
impl<S: States> Default for NextState<S> {
fn default() -> Self {
Self(None)
}
}
impl<S: States> NextState<S> {
/// Tentatively set a planned state transition to `Some(state)`.
pub fn set(&mut self, state: S) {

View file

@ -12,7 +12,7 @@ use bevy::{asset::LoadedFolder, prelude::*, render::texture::ImageSampler};
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) // fallback to nearest sampling
.add_state::<AppState>()
.init_state::<AppState>()
.add_systems(OnEnter(AppState::Setup), load_textures)
.add_systems(Update, check_textures.run_if(in_state(AppState::Setup)))
.add_systems(OnEnter(AppState::Finished), setup)

View file

@ -33,7 +33,7 @@ struct LevelUnload;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_state::<AppState>()
.init_state::<AppState>()
.add_systems(Startup, setup_system)
.add_systems(
Update,

View file

@ -10,7 +10,7 @@ use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_state::<AppState>()
.init_state::<AppState>() // Alternatively we could use .insert_state(AppState::Menu)
.add_systems(Startup, setup)
// This system runs when we enter `AppState::Menu`, during the `StateTransition` schedule.
// All systems from the exit schedule of the state we're leaving are run first,

View file

@ -23,7 +23,7 @@ fn main() {
5.0,
TimerMode::Repeating,
)))
.add_state::<GameState>()
.init_state::<GameState>()
.add_systems(Startup, setup_cameras)
.add_systems(OnEnter(GameState::Playing), setup)
.add_systems(

View file

@ -34,7 +34,7 @@ fn main() {
.insert_resource(DisplayQuality::Medium)
.insert_resource(Volume(7))
// Declare the game state, whose starting value is determined by the `Default` trait
.add_state::<GameState>()
.init_state::<GameState>()
.add_systems(Startup, setup)
// Adds the plugins for each state
.add_plugins((splash::SplashPlugin, menu::MenuPlugin, game::GamePlugin))
@ -261,7 +261,7 @@ mod menu {
// At start, the menu is not enabled. This will be changed in `menu_setup` when
// entering the `GameState::Menu` state.
// Current screen in the menu is handled by an independent state from `GameState`
.add_state::<MenuState>()
.init_state::<MenuState>()
.add_systems(OnEnter(GameState::Menu), menu_setup)
// Systems to handle the main menu screen
.add_systems(OnEnter(MenuState::Main), main_menu_setup)