
This adds support for one-to-many non-fragmenting relationships (with planned paths for fragmenting and non-fragmenting many-to-many relationships). "Non-fragmenting" means that entities with the same relationship type, but different relationship targets, are not forced into separate tables (which would cause "table fragmentation"). Functionally, this fills a similar niche as the current Parent/Children system. The biggest differences are: 1. Relationships have simpler internals and significantly improved performance and UX. Commands and specialized APIs are no longer necessary to keep everything in sync. Just spawn entities with the relationship components you want and everything "just works". 2. Relationships are generalized. Bevy can provide additional built in relationships, and users can define their own. **REQUEST TO REVIEWERS**: _please don't leave top level comments and instead comment on specific lines of code. That way we can take advantage of threaded discussions. Also dont leave comments simply pointing out CI failures as I can read those just fine._ ## Built on top of what we have Relationships are implemented on top of the Bevy ECS features we already have: components, immutability, and hooks. This makes them immediately compatible with all of our existing (and future) APIs for querying, spawning, removing, scenes, reflection, etc. The fewer specialized APIs we need to build, maintain, and teach, the better. ## Why focus on one-to-many non-fragmenting first? 1. This allows us to improve Parent/Children relationships immediately, in a way that is reasonably uncontroversial. Switching our hierarchy to fragmenting relationships would have significant performance implications. ~~Flecs is heavily considering a switch to non-fragmenting relations after careful considerations of the performance tradeoffs.~~ _(Correction from @SanderMertens: Flecs is implementing non-fragmenting storage specialized for asset hierarchies, where asset hierarchies are many instances of small trees that have a well defined structure)_ 2. Adding generalized one-to-many relationships is currently a priority for the [Next Generation Scene / UI effort](https://github.com/bevyengine/bevy/discussions/14437). Specifically, we're interested in building reactions and observers on top. ## The changes This PR does the following: 1. Adds a generic one-to-many Relationship system 3. Ports the existing Parent/Children system to Relationships, which now lives in `bevy_ecs::hierarchy`. The old `bevy_hierarchy` crate has been removed. 4. Adds on_despawn component hooks 5. Relationships can opt-in to "despawn descendants" behavior, meaning that the entire relationship hierarchy is despawned when `entity.despawn()` is called. The built in Parent/Children hierarchies enable this behavior, and `entity.despawn_recursive()` has been removed. 6. `world.spawn` now applies commands after spawning. This ensures that relationship bookkeeping happens immediately and removes the need to manually flush. This is in line with the equivalent behaviors recently added to the other APIs (ex: insert). 7. Removes the ValidParentCheckPlugin (system-driven / poll based) in favor of a `validate_parent_has_component` hook. ## Using Relationships The `Relationship` trait looks like this: ```rust pub trait Relationship: Component + Sized { type RelationshipSources: RelationshipSources<Relationship = Self>; fn get(&self) -> Entity; fn from(entity: Entity) -> Self; } ``` A relationship is a component that: 1. Is a simple wrapper over a "target" Entity. 2. Has a corresponding `RelationshipSources` component, which is a simple wrapper over a collection of entities. Every "target entity" targeted by a "source entity" with a `Relationship` has a `RelationshipSources` component, which contains every "source entity" that targets it. For example, the `Parent` component (as it currently exists in Bevy) is the `Relationship` component and the entity containing the Parent is the "source entity". The entity _inside_ the `Parent(Entity)` component is the "target entity". And that target entity has a `Children` component (which implements `RelationshipSources`). In practice, the Parent/Children relationship looks like this: ```rust #[derive(Relationship)] #[relationship(relationship_sources = Children)] pub struct Parent(pub Entity); #[derive(RelationshipSources)] #[relationship_sources(relationship = Parent)] pub struct Children(Vec<Entity>); ``` The Relationship and RelationshipSources derives automatically implement Component with the relevant configuration (namely, the hooks necessary to keep everything in sync). The most direct way to add relationships is to spawn entities with relationship components: ```rust let a = world.spawn_empty().id(); let b = world.spawn(Parent(a)).id(); assert_eq!(world.entity(a).get::<Children>().unwrap(), &[b]); ``` There are also convenience APIs for spawning more than one entity with the same relationship: ```rust world.spawn_empty().with_related::<Children>(|s| { s.spawn_empty(); s.spawn_empty(); }) ``` The existing `with_children` API is now a simpler wrapper over `with_related`. This makes this change largely non-breaking for existing spawn patterns. ```rust world.spawn_empty().with_children(|s| { s.spawn_empty(); s.spawn_empty(); }) ``` There are also other relationship APIs, such as `add_related` and `despawn_related`. ## Automatic recursive despawn via the new on_despawn hook `RelationshipSources` can opt-in to "despawn descendants" behavior, which will despawn all related entities in the relationship hierarchy: ```rust #[derive(RelationshipSources)] #[relationship_sources(relationship = Parent, despawn_descendants)] pub struct Children(Vec<Entity>); ``` This means that `entity.despawn_recursive()` is no longer required. Instead, just use `entity.despawn()` and the relevant related entities will also be despawned. To despawn an entity _without_ despawning its parent/child descendants, you should remove the `Children` component first, which will also remove the related `Parent` components: ```rust entity .remove::<Children>() .despawn() ``` This builds on the on_despawn hook introduced in this PR, which is fired when an entity is despawned (before other hooks). ## Relationships are the source of truth `Relationship` is the _single_ source of truth component. `RelationshipSources` is merely a reflection of what all the `Relationship` components say. By embracing this, we are able to significantly improve the performance of the system as a whole. We can rely on component lifecycles to protect us against duplicates, rather than needing to scan at runtime to ensure entities don't already exist (which results in quadratic runtime). A single source of truth gives us constant-time inserts. This does mean that we cannot directly spawn populated `Children` components (or directly add or remove entities from those components). I personally think this is a worthwhile tradeoff, both because it makes the performance much better _and_ because it means theres exactly one way to do things (which is a philosophy we try to employ for Bevy APIs). As an aside: treating both sides of the relationship as "equivalent source of truth relations" does enable building simple and flexible many-to-many relationships. But this introduces an _inherent_ need to scan (or hash) to protect against duplicates. [`evergreen_relations`](https://github.com/EvergreenNest/evergreen_relations) has a very nice implementation of the "symmetrical many-to-many" approach. Unfortunately I think the performance issues inherent to that approach make it a poor choice for Bevy's default relationship system. ## Followup Work * Discuss renaming `Parent` to `ChildOf`. I refrained from doing that in this PR to keep the diff reasonable, but I'm personally biased toward this change (and using that naming pattern generally for relationships). * [Improved spawning ergonomics](https://github.com/bevyengine/bevy/discussions/16920) * Consider adding relationship observers/triggers for "relationship targets" whenever a source is added or removed. This would replace the current "hierarchy events" system, which is unused upstream but may have existing users downstream. I think triggers are the better fit for this than a buffered event queue, and would prefer not to add that back. * Fragmenting relations: My current idea hinges on the introduction of "value components" (aka: components whose type _and_ value determines their ComponentId, via something like Hashing / PartialEq). By labeling a Relationship component such as `ChildOf(Entity)` as a "value component", `ChildOf(e1)` and `ChildOf(e2)` would be considered "different components". This makes the transition between fragmenting and non-fragmenting a single flag, and everything else continues to work as expected. * Many-to-many support * Non-fragmenting: We can expand Relationship to be a list of entities instead of a single entity. I have largely already written the code for this. * Fragmenting: With the "value component" impl mentioned above, we get many-to-many support "for free", as it would allow inserting multiple copies of a Relationship component with different target entities. Fixes #3742 (If this PR is merged, I think we should open more targeted followup issues for the work above, with a fresh tracking issue free of the large amount of less-directed historical context) Fixes #17301 Fixes #12235 Fixes #15299 Fixes #15308 ## Migration Guide * Replace `ChildBuilder` with `ChildSpawnerCommands`. * Replace calls to `.set_parent(parent_id)` with `.insert(Parent(parent_id))`. * Replace calls to `.replace_children()` with `.remove::<Children>()` followed by `.add_children()`. Note that you'll need to manually despawn any children that are not carried over. * Replace calls to `.despawn_recursive()` with `.despawn()`. * Replace calls to `.despawn_descendants()` with `.despawn_related::<Children>()`. * If you have any calls to `.despawn()` which depend on the children being preserved, you'll need to remove the `Children` component first. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
766 lines
30 KiB
Rust
766 lines
30 KiB
Rust
//! This example will display a simple menu using Bevy UI where you can start a new game,
|
|
//! change some settings or quit. There is no actual game, it will just display the current
|
|
//! settings for 5 seconds before going back to the menu.
|
|
|
|
use bevy::prelude::*;
|
|
|
|
const TEXT_COLOR: Color = Color::srgb(0.9, 0.9, 0.9);
|
|
|
|
// Enum that will be used as a global state for the game
|
|
#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash, States)]
|
|
enum GameState {
|
|
#[default]
|
|
Splash,
|
|
Menu,
|
|
Game,
|
|
}
|
|
|
|
// One of the two settings that can be set through the menu. It will be a resource in the app
|
|
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone, Copy)]
|
|
enum DisplayQuality {
|
|
Low,
|
|
Medium,
|
|
High,
|
|
}
|
|
|
|
// One of the two settings that can be set through the menu. It will be a resource in the app
|
|
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone, Copy)]
|
|
struct Volume(u32);
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
// Insert as resource the initial value for the settings resources
|
|
.insert_resource(DisplayQuality::Medium)
|
|
.insert_resource(Volume(7))
|
|
// Declare the game state, whose starting value is determined by the `Default` trait
|
|
.init_state::<GameState>()
|
|
.add_systems(Startup, setup)
|
|
// Adds the plugins for each state
|
|
.add_plugins((splash::splash_plugin, menu::menu_plugin, game::game_plugin))
|
|
.run();
|
|
}
|
|
|
|
fn setup(mut commands: Commands) {
|
|
commands.spawn(Camera2d);
|
|
}
|
|
|
|
mod splash {
|
|
use bevy::prelude::*;
|
|
|
|
use super::{despawn_screen, GameState};
|
|
|
|
// This plugin will display a splash screen with Bevy logo for 1 second before switching to the menu
|
|
pub fn splash_plugin(app: &mut App) {
|
|
// As this plugin is managing the splash screen, it will focus on the state `GameState::Splash`
|
|
app
|
|
// When entering the state, spawn everything needed for this screen
|
|
.add_systems(OnEnter(GameState::Splash), splash_setup)
|
|
// While in this state, run the `countdown` system
|
|
.add_systems(Update, countdown.run_if(in_state(GameState::Splash)))
|
|
// When exiting the state, despawn everything that was spawned for this screen
|
|
.add_systems(OnExit(GameState::Splash), despawn_screen::<OnSplashScreen>);
|
|
}
|
|
|
|
// Tag component used to tag entities added on the splash screen
|
|
#[derive(Component)]
|
|
struct OnSplashScreen;
|
|
|
|
// Newtype to use a `Timer` for this screen as a resource
|
|
#[derive(Resource, Deref, DerefMut)]
|
|
struct SplashTimer(Timer);
|
|
|
|
fn splash_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|
let icon = asset_server.load("branding/icon.png");
|
|
// Display the logo
|
|
commands
|
|
.spawn((
|
|
Node {
|
|
align_items: AlignItems::Center,
|
|
justify_content: JustifyContent::Center,
|
|
width: Val::Percent(100.0),
|
|
height: Val::Percent(100.0),
|
|
..default()
|
|
},
|
|
OnSplashScreen,
|
|
))
|
|
.with_children(|parent| {
|
|
parent.spawn((
|
|
ImageNode::new(icon),
|
|
Node {
|
|
// This will set the logo to be 200px wide, and auto adjust its height
|
|
width: Val::Px(200.0),
|
|
..default()
|
|
},
|
|
));
|
|
});
|
|
// Insert the timer as a resource
|
|
commands.insert_resource(SplashTimer(Timer::from_seconds(1.0, TimerMode::Once)));
|
|
}
|
|
|
|
// Tick the timer, and change state when finished
|
|
fn countdown(
|
|
mut game_state: ResMut<NextState<GameState>>,
|
|
time: Res<Time>,
|
|
mut timer: ResMut<SplashTimer>,
|
|
) {
|
|
if timer.tick(time.delta()).finished() {
|
|
game_state.set(GameState::Menu);
|
|
}
|
|
}
|
|
}
|
|
|
|
mod game {
|
|
use bevy::{
|
|
color::palettes::basic::{BLUE, LIME},
|
|
prelude::*,
|
|
};
|
|
|
|
use super::{despawn_screen, DisplayQuality, GameState, Volume, TEXT_COLOR};
|
|
|
|
// This plugin will contain the game. In this case, it's just be a screen that will
|
|
// display the current settings for 5 seconds before returning to the menu
|
|
pub fn game_plugin(app: &mut App) {
|
|
app.add_systems(OnEnter(GameState::Game), game_setup)
|
|
.add_systems(Update, game.run_if(in_state(GameState::Game)))
|
|
.add_systems(OnExit(GameState::Game), despawn_screen::<OnGameScreen>);
|
|
}
|
|
|
|
// Tag component used to tag entities added on the game screen
|
|
#[derive(Component)]
|
|
struct OnGameScreen;
|
|
|
|
#[derive(Resource, Deref, DerefMut)]
|
|
struct GameTimer(Timer);
|
|
|
|
fn game_setup(
|
|
mut commands: Commands,
|
|
display_quality: Res<DisplayQuality>,
|
|
volume: Res<Volume>,
|
|
) {
|
|
commands
|
|
.spawn((
|
|
Node {
|
|
width: Val::Percent(100.0),
|
|
height: Val::Percent(100.0),
|
|
// center children
|
|
align_items: AlignItems::Center,
|
|
justify_content: JustifyContent::Center,
|
|
..default()
|
|
},
|
|
OnGameScreen,
|
|
))
|
|
.with_children(|parent| {
|
|
// First create a `Node` for centering what we want to display
|
|
parent
|
|
.spawn((
|
|
Node {
|
|
// This will display its children in a column, from top to bottom
|
|
flex_direction: FlexDirection::Column,
|
|
// `align_items` will align children on the cross axis. Here the main axis is
|
|
// vertical (column), so the cross axis is horizontal. This will center the
|
|
// children
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
},
|
|
BackgroundColor(Color::BLACK),
|
|
))
|
|
.with_children(|p| {
|
|
p.spawn((
|
|
Text::new("Will be back to the menu shortly..."),
|
|
TextFont {
|
|
font_size: 67.0,
|
|
..default()
|
|
},
|
|
TextColor(TEXT_COLOR),
|
|
Node {
|
|
margin: UiRect::all(Val::Px(50.0)),
|
|
..default()
|
|
},
|
|
));
|
|
p.spawn((
|
|
Text::default(),
|
|
Node {
|
|
margin: UiRect::all(Val::Px(50.0)),
|
|
..default()
|
|
},
|
|
))
|
|
.with_children(|p| {
|
|
p.spawn((
|
|
TextSpan(format!("quality: {:?}", *display_quality)),
|
|
TextFont {
|
|
font_size: 50.0,
|
|
..default()
|
|
},
|
|
TextColor(BLUE.into()),
|
|
));
|
|
p.spawn((
|
|
TextSpan::new(" - "),
|
|
TextFont {
|
|
font_size: 50.0,
|
|
..default()
|
|
},
|
|
TextColor(TEXT_COLOR),
|
|
));
|
|
p.spawn((
|
|
TextSpan(format!("volume: {:?}", *volume)),
|
|
TextFont {
|
|
font_size: 50.0,
|
|
..default()
|
|
},
|
|
TextColor(LIME.into()),
|
|
));
|
|
});
|
|
});
|
|
});
|
|
// Spawn a 5 seconds timer to trigger going back to the menu
|
|
commands.insert_resource(GameTimer(Timer::from_seconds(5.0, TimerMode::Once)));
|
|
}
|
|
|
|
// Tick the timer, and change state when finished
|
|
fn game(
|
|
time: Res<Time>,
|
|
mut game_state: ResMut<NextState<GameState>>,
|
|
mut timer: ResMut<GameTimer>,
|
|
) {
|
|
if timer.tick(time.delta()).finished() {
|
|
game_state.set(GameState::Menu);
|
|
}
|
|
}
|
|
}
|
|
|
|
mod menu {
|
|
use bevy::{app::AppExit, color::palettes::css::CRIMSON, prelude::*};
|
|
|
|
use super::{despawn_screen, DisplayQuality, GameState, Volume, TEXT_COLOR};
|
|
|
|
// This plugin manages the menu, with 5 different screens:
|
|
// - a main menu with "New Game", "Settings", "Quit"
|
|
// - a settings menu with two submenus and a back button
|
|
// - two settings screen with a setting that can be set and a back button
|
|
pub fn menu_plugin(app: &mut App) {
|
|
app
|
|
// 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`
|
|
.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)
|
|
.add_systems(OnExit(MenuState::Main), despawn_screen::<OnMainMenuScreen>)
|
|
// Systems to handle the settings menu screen
|
|
.add_systems(OnEnter(MenuState::Settings), settings_menu_setup)
|
|
.add_systems(
|
|
OnExit(MenuState::Settings),
|
|
despawn_screen::<OnSettingsMenuScreen>,
|
|
)
|
|
// Systems to handle the display settings screen
|
|
.add_systems(
|
|
OnEnter(MenuState::SettingsDisplay),
|
|
display_settings_menu_setup,
|
|
)
|
|
.add_systems(
|
|
Update,
|
|
(setting_button::<DisplayQuality>.run_if(in_state(MenuState::SettingsDisplay)),),
|
|
)
|
|
.add_systems(
|
|
OnExit(MenuState::SettingsDisplay),
|
|
despawn_screen::<OnDisplaySettingsMenuScreen>,
|
|
)
|
|
// Systems to handle the sound settings screen
|
|
.add_systems(OnEnter(MenuState::SettingsSound), sound_settings_menu_setup)
|
|
.add_systems(
|
|
Update,
|
|
setting_button::<Volume>.run_if(in_state(MenuState::SettingsSound)),
|
|
)
|
|
.add_systems(
|
|
OnExit(MenuState::SettingsSound),
|
|
despawn_screen::<OnSoundSettingsMenuScreen>,
|
|
)
|
|
// Common systems to all screens that handles buttons behavior
|
|
.add_systems(
|
|
Update,
|
|
(menu_action, button_system).run_if(in_state(GameState::Menu)),
|
|
);
|
|
}
|
|
|
|
// State used for the current menu screen
|
|
#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash, States)]
|
|
enum MenuState {
|
|
Main,
|
|
Settings,
|
|
SettingsDisplay,
|
|
SettingsSound,
|
|
#[default]
|
|
Disabled,
|
|
}
|
|
|
|
// Tag component used to tag entities added on the main menu screen
|
|
#[derive(Component)]
|
|
struct OnMainMenuScreen;
|
|
|
|
// Tag component used to tag entities added on the settings menu screen
|
|
#[derive(Component)]
|
|
struct OnSettingsMenuScreen;
|
|
|
|
// Tag component used to tag entities added on the display settings menu screen
|
|
#[derive(Component)]
|
|
struct OnDisplaySettingsMenuScreen;
|
|
|
|
// Tag component used to tag entities added on the sound settings menu screen
|
|
#[derive(Component)]
|
|
struct OnSoundSettingsMenuScreen;
|
|
|
|
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
|
|
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
|
|
const HOVERED_PRESSED_BUTTON: Color = Color::srgb(0.25, 0.65, 0.25);
|
|
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
|
|
|
|
// Tag component used to mark which setting is currently selected
|
|
#[derive(Component)]
|
|
struct SelectedOption;
|
|
|
|
// All actions that can be triggered from a button click
|
|
#[derive(Component)]
|
|
enum MenuButtonAction {
|
|
Play,
|
|
Settings,
|
|
SettingsDisplay,
|
|
SettingsSound,
|
|
BackToMainMenu,
|
|
BackToSettings,
|
|
Quit,
|
|
}
|
|
|
|
// This system handles changing all buttons color based on mouse interaction
|
|
fn button_system(
|
|
mut interaction_query: Query<
|
|
(&Interaction, &mut BackgroundColor, Option<&SelectedOption>),
|
|
(Changed<Interaction>, With<Button>),
|
|
>,
|
|
) {
|
|
for (interaction, mut background_color, selected) in &mut interaction_query {
|
|
*background_color = match (*interaction, selected) {
|
|
(Interaction::Pressed, _) | (Interaction::None, Some(_)) => PRESSED_BUTTON.into(),
|
|
(Interaction::Hovered, Some(_)) => HOVERED_PRESSED_BUTTON.into(),
|
|
(Interaction::Hovered, None) => HOVERED_BUTTON.into(),
|
|
(Interaction::None, None) => NORMAL_BUTTON.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
// This system updates the settings when a new value for a setting is selected, and marks
|
|
// the button as the one currently selected
|
|
fn setting_button<T: Resource + Component + PartialEq + Copy>(
|
|
interaction_query: Query<(&Interaction, &T, Entity), (Changed<Interaction>, With<Button>)>,
|
|
selected_query: Single<(Entity, &mut BackgroundColor), With<SelectedOption>>,
|
|
mut commands: Commands,
|
|
mut setting: ResMut<T>,
|
|
) {
|
|
let (previous_button, mut previous_button_color) = selected_query.into_inner();
|
|
for (interaction, button_setting, entity) in &interaction_query {
|
|
if *interaction == Interaction::Pressed && *setting != *button_setting {
|
|
*previous_button_color = NORMAL_BUTTON.into();
|
|
commands.entity(previous_button).remove::<SelectedOption>();
|
|
commands.entity(entity).insert(SelectedOption);
|
|
*setting = *button_setting;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn menu_setup(mut menu_state: ResMut<NextState<MenuState>>) {
|
|
menu_state.set(MenuState::Main);
|
|
}
|
|
|
|
fn main_menu_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|
// Common style for all buttons on the screen
|
|
let button_node = Node {
|
|
width: Val::Px(300.0),
|
|
height: Val::Px(65.0),
|
|
margin: UiRect::all(Val::Px(20.0)),
|
|
justify_content: JustifyContent::Center,
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
};
|
|
let button_icon_node = Node {
|
|
width: Val::Px(30.0),
|
|
// This takes the icons out of the flexbox flow, to be positioned exactly
|
|
position_type: PositionType::Absolute,
|
|
// The icon will be close to the left border of the button
|
|
left: Val::Px(10.0),
|
|
..default()
|
|
};
|
|
let button_text_font = TextFont {
|
|
font_size: 33.0,
|
|
..default()
|
|
};
|
|
|
|
commands
|
|
.spawn((
|
|
Node {
|
|
width: Val::Percent(100.0),
|
|
height: Val::Percent(100.0),
|
|
align_items: AlignItems::Center,
|
|
justify_content: JustifyContent::Center,
|
|
..default()
|
|
},
|
|
OnMainMenuScreen,
|
|
))
|
|
.with_children(|parent| {
|
|
parent
|
|
.spawn((
|
|
Node {
|
|
flex_direction: FlexDirection::Column,
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
},
|
|
BackgroundColor(CRIMSON.into()),
|
|
))
|
|
.with_children(|parent| {
|
|
// Display the game name
|
|
parent.spawn((
|
|
Text::new("Bevy Game Menu UI"),
|
|
TextFont {
|
|
font_size: 67.0,
|
|
..default()
|
|
},
|
|
TextColor(TEXT_COLOR),
|
|
Node {
|
|
margin: UiRect::all(Val::Px(50.0)),
|
|
..default()
|
|
},
|
|
));
|
|
|
|
// Display three buttons for each action available from the main menu:
|
|
// - new game
|
|
// - settings
|
|
// - quit
|
|
parent
|
|
.spawn((
|
|
Button,
|
|
button_node.clone(),
|
|
BackgroundColor(NORMAL_BUTTON),
|
|
MenuButtonAction::Play,
|
|
))
|
|
.with_children(|parent| {
|
|
let icon = asset_server.load("textures/Game Icons/right.png");
|
|
parent.spawn((ImageNode::new(icon), button_icon_node.clone()));
|
|
parent.spawn((
|
|
Text::new("New Game"),
|
|
button_text_font.clone(),
|
|
TextColor(TEXT_COLOR),
|
|
));
|
|
});
|
|
parent
|
|
.spawn((
|
|
Button,
|
|
button_node.clone(),
|
|
BackgroundColor(NORMAL_BUTTON),
|
|
MenuButtonAction::Settings,
|
|
))
|
|
.with_children(|parent| {
|
|
let icon = asset_server.load("textures/Game Icons/wrench.png");
|
|
parent.spawn((ImageNode::new(icon), button_icon_node.clone()));
|
|
parent.spawn((
|
|
Text::new("Settings"),
|
|
button_text_font.clone(),
|
|
TextColor(TEXT_COLOR),
|
|
));
|
|
});
|
|
parent
|
|
.spawn((
|
|
Button,
|
|
button_node,
|
|
BackgroundColor(NORMAL_BUTTON),
|
|
MenuButtonAction::Quit,
|
|
))
|
|
.with_children(|parent| {
|
|
let icon = asset_server.load("textures/Game Icons/exitRight.png");
|
|
parent.spawn((ImageNode::new(icon), button_icon_node));
|
|
parent.spawn((
|
|
Text::new("Quit"),
|
|
button_text_font,
|
|
TextColor(TEXT_COLOR),
|
|
));
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
fn settings_menu_setup(mut commands: Commands) {
|
|
let button_node = Node {
|
|
width: Val::Px(200.0),
|
|
height: Val::Px(65.0),
|
|
margin: UiRect::all(Val::Px(20.0)),
|
|
justify_content: JustifyContent::Center,
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
};
|
|
|
|
let button_text_style = (
|
|
TextFont {
|
|
font_size: 33.0,
|
|
..default()
|
|
},
|
|
TextColor(TEXT_COLOR),
|
|
);
|
|
|
|
commands
|
|
.spawn((
|
|
Node {
|
|
width: Val::Percent(100.0),
|
|
height: Val::Percent(100.0),
|
|
align_items: AlignItems::Center,
|
|
justify_content: JustifyContent::Center,
|
|
..default()
|
|
},
|
|
OnSettingsMenuScreen,
|
|
))
|
|
.with_children(|parent| {
|
|
parent
|
|
.spawn((
|
|
Node {
|
|
flex_direction: FlexDirection::Column,
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
},
|
|
BackgroundColor(CRIMSON.into()),
|
|
))
|
|
.with_children(|parent| {
|
|
for (action, text) in [
|
|
(MenuButtonAction::SettingsDisplay, "Display"),
|
|
(MenuButtonAction::SettingsSound, "Sound"),
|
|
(MenuButtonAction::BackToMainMenu, "Back"),
|
|
] {
|
|
parent
|
|
.spawn((
|
|
Button,
|
|
button_node.clone(),
|
|
BackgroundColor(NORMAL_BUTTON),
|
|
action,
|
|
))
|
|
.with_children(|parent| {
|
|
parent.spawn((Text::new(text), button_text_style.clone()));
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
fn display_settings_menu_setup(mut commands: Commands, display_quality: Res<DisplayQuality>) {
|
|
let button_node = Node {
|
|
width: Val::Px(200.0),
|
|
height: Val::Px(65.0),
|
|
margin: UiRect::all(Val::Px(20.0)),
|
|
justify_content: JustifyContent::Center,
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
};
|
|
let button_text_style = (
|
|
TextFont {
|
|
font_size: 33.0,
|
|
..default()
|
|
},
|
|
TextColor(TEXT_COLOR),
|
|
);
|
|
|
|
commands
|
|
.spawn((
|
|
Node {
|
|
width: Val::Percent(100.0),
|
|
height: Val::Percent(100.0),
|
|
align_items: AlignItems::Center,
|
|
justify_content: JustifyContent::Center,
|
|
..default()
|
|
},
|
|
OnDisplaySettingsMenuScreen,
|
|
))
|
|
.with_children(|parent| {
|
|
parent
|
|
.spawn((
|
|
Node {
|
|
flex_direction: FlexDirection::Column,
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
},
|
|
BackgroundColor(CRIMSON.into()),
|
|
))
|
|
.with_children(|parent| {
|
|
// Create a new `Node`, this time not setting its `flex_direction`. It will
|
|
// use the default value, `FlexDirection::Row`, from left to right.
|
|
parent
|
|
.spawn((
|
|
Node {
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
},
|
|
BackgroundColor(CRIMSON.into()),
|
|
))
|
|
.with_children(|parent| {
|
|
// Display a label for the current setting
|
|
parent.spawn((
|
|
Text::new("Display Quality"),
|
|
button_text_style.clone(),
|
|
));
|
|
// Display a button for each possible value
|
|
for quality_setting in [
|
|
DisplayQuality::Low,
|
|
DisplayQuality::Medium,
|
|
DisplayQuality::High,
|
|
] {
|
|
let mut entity = parent.spawn((
|
|
Button,
|
|
Node {
|
|
width: Val::Px(150.0),
|
|
height: Val::Px(65.0),
|
|
..button_node.clone()
|
|
},
|
|
BackgroundColor(NORMAL_BUTTON),
|
|
quality_setting,
|
|
));
|
|
entity.with_children(|parent| {
|
|
parent.spawn((
|
|
Text::new(format!("{quality_setting:?}")),
|
|
button_text_style.clone(),
|
|
));
|
|
});
|
|
if *display_quality == quality_setting {
|
|
entity.insert(SelectedOption);
|
|
}
|
|
}
|
|
});
|
|
// Display the back button to return to the settings screen
|
|
parent
|
|
.spawn((
|
|
Button,
|
|
button_node,
|
|
BackgroundColor(NORMAL_BUTTON),
|
|
MenuButtonAction::BackToSettings,
|
|
))
|
|
.with_children(|parent| {
|
|
parent.spawn((Text::new("Back"), button_text_style));
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
fn sound_settings_menu_setup(mut commands: Commands, volume: Res<Volume>) {
|
|
let button_node = Node {
|
|
width: Val::Px(200.0),
|
|
height: Val::Px(65.0),
|
|
margin: UiRect::all(Val::Px(20.0)),
|
|
justify_content: JustifyContent::Center,
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
};
|
|
let button_text_style = (
|
|
TextFont {
|
|
font_size: 33.0,
|
|
..default()
|
|
},
|
|
TextColor(TEXT_COLOR),
|
|
);
|
|
|
|
commands
|
|
.spawn((
|
|
Node {
|
|
width: Val::Percent(100.0),
|
|
height: Val::Percent(100.0),
|
|
align_items: AlignItems::Center,
|
|
justify_content: JustifyContent::Center,
|
|
..default()
|
|
},
|
|
OnSoundSettingsMenuScreen,
|
|
))
|
|
.with_children(|parent| {
|
|
parent
|
|
.spawn((
|
|
Node {
|
|
flex_direction: FlexDirection::Column,
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
},
|
|
BackgroundColor(CRIMSON.into()),
|
|
))
|
|
.with_children(|parent| {
|
|
parent
|
|
.spawn((
|
|
Node {
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
},
|
|
BackgroundColor(CRIMSON.into()),
|
|
))
|
|
.with_children(|parent| {
|
|
parent.spawn((Text::new("Volume"), button_text_style.clone()));
|
|
for volume_setting in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] {
|
|
let mut entity = parent.spawn((
|
|
Button,
|
|
Node {
|
|
width: Val::Px(30.0),
|
|
height: Val::Px(65.0),
|
|
..button_node.clone()
|
|
},
|
|
BackgroundColor(NORMAL_BUTTON),
|
|
Volume(volume_setting),
|
|
));
|
|
if *volume == Volume(volume_setting) {
|
|
entity.insert(SelectedOption);
|
|
}
|
|
}
|
|
});
|
|
parent
|
|
.spawn((
|
|
Button,
|
|
button_node,
|
|
BackgroundColor(NORMAL_BUTTON),
|
|
MenuButtonAction::BackToSettings,
|
|
))
|
|
.with_child((Text::new("Back"), button_text_style));
|
|
});
|
|
});
|
|
}
|
|
|
|
fn menu_action(
|
|
interaction_query: Query<
|
|
(&Interaction, &MenuButtonAction),
|
|
(Changed<Interaction>, With<Button>),
|
|
>,
|
|
mut app_exit_events: EventWriter<AppExit>,
|
|
mut menu_state: ResMut<NextState<MenuState>>,
|
|
mut game_state: ResMut<NextState<GameState>>,
|
|
) {
|
|
for (interaction, menu_button_action) in &interaction_query {
|
|
if *interaction == Interaction::Pressed {
|
|
match menu_button_action {
|
|
MenuButtonAction::Quit => {
|
|
app_exit_events.send(AppExit::Success);
|
|
}
|
|
MenuButtonAction::Play => {
|
|
game_state.set(GameState::Game);
|
|
menu_state.set(MenuState::Disabled);
|
|
}
|
|
MenuButtonAction::Settings => menu_state.set(MenuState::Settings),
|
|
MenuButtonAction::SettingsDisplay => {
|
|
menu_state.set(MenuState::SettingsDisplay);
|
|
}
|
|
MenuButtonAction::SettingsSound => {
|
|
menu_state.set(MenuState::SettingsSound);
|
|
}
|
|
MenuButtonAction::BackToMainMenu => menu_state.set(MenuState::Main),
|
|
MenuButtonAction::BackToSettings => {
|
|
menu_state.set(MenuState::Settings);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generic system that takes a component as a parameter, and will despawn all entities with that component
|
|
fn despawn_screen<T: Component>(to_despawn: Query<Entity, With<T>>, mut commands: Commands) {
|
|
for entity in &to_despawn {
|
|
commands.entity(entity).despawn();
|
|
}
|
|
}
|