use bevy::{prelude::*, utils::HashMap};
use bevy_quickmenu::{
style::Stylesheet, ActionTrait, Menu, MenuIcon, MenuItem, MenuState, QuickMenuPlugin,
ScreenTrait,
};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, States, Default)]
enum GameState {
#[default]
Settings,
Game,
}
impl GameState {
fn is_game(state: Res<State<GameState>>) -> bool {
state.get() == &GameState::Game
}
fn is_settings(state: Res<State<GameState>>) -> bool {
state.get() == &GameState::Settings
}
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_state::<GameState>()
.add_plugins(settings::SettingsPlugin)
.add_plugins(game::Game)
.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera3dBundle::default());
}
mod settings {
use super::*;
#[derive(Debug, Event)]
enum MyEvent {
CloseSettings,
}
#[derive(Debug, Clone)]
struct CustomState {
sound_on: bool,
gamepads: Vec<(Gamepad, String)>,
controls: HashMap<usize, ControlDevice>,
logo: Handle<Image>,
}
pub struct SettingsPlugin;
impl Plugin for SettingsPlugin {
fn build(&self, app: &mut App) {
app
.add_event::<MyEvent>()
.add_plugins(QuickMenuPlugin::<Screens>::new())
.add_systems(OnEnter(GameState::Settings), setup_system)
.add_systems(
Update,
(event_reader, update_gamepads_system).run_if(GameState::is_settings),
);
}
}
fn setup_system(mut commands: Commands, assets: Res<AssetServer>) {
let sheet = Stylesheet::default().with_background(BackgroundColor(Color::BLACK));
commands.insert_resource(MenuState::new(
CustomState {
sound_on: true,
gamepads: Vec::new(),
controls: [
(0, ControlDevice::keyboard1()),
(1, ControlDevice::keyboard2()),
(2, ControlDevice::keyboard3()),
(3, ControlDevice::keyboard4()),
]
.into(),
logo: assets.load("logo.png"),
},
Screens::Root,
Some(sheet),
))
}
fn update_gamepads_system(
gamepads: Res<Gamepads>,
menu_state: Option<ResMut<MenuState<Screens>>>,
) {
let Some(mut menu_state) = menu_state else {
return
};
let gamepads = gamepads
.iter()
.map(|p| {
(
p,
gamepads.name(p).map(|s| s.to_owned()).unwrap_or_default(),
)
})
.collect();
if menu_state.state().gamepads != gamepads {
menu_state.state_mut().gamepads = gamepads;
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
enum Actions {
Close,
SoundOn,
SoundOff,
Control(usize, ControlDevice),
}
impl ActionTrait for Actions {
type State = CustomState;
type Event = MyEvent;
fn handle(&self, state: &mut CustomState, event_writer: &mut EventWriter<MyEvent>) {
match self {
Actions::Close => event_writer.send(MyEvent::CloseSettings),
Actions::SoundOn => state.sound_on = true,
Actions::SoundOff => state.sound_on = false,
Actions::Control(p, d) => {
state.controls.insert(*p, *d);
}
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
enum Screens {
Root,
Controls,
Sound,
Player(usize),
}
impl ScreenTrait for Screens {
type Action = Actions;
type State = CustomState;
fn resolve(&self, state: &CustomState) -> Menu<Screens> {
match self {
Screens::Root => root_menu(state),
Screens::Controls => controls_menu(state),
Screens::Sound => sound_menu(state),
Screens::Player(p) => player_controls_menu(state, *p),
}
}
}
fn root_menu(state: &CustomState) -> Menu<Screens> {
Menu::new(
"root",
vec![
MenuItem::image(state.logo.clone()),
MenuItem::headline("Menu"),
MenuItem::action("Start", Actions::Close),
MenuItem::screen("Sound", Screens::Sound).with_icon(MenuIcon::Sound),
MenuItem::screen("Controls", Screens::Controls).with_icon(MenuIcon::Controls),
],
)
}
fn sound_menu(state: &CustomState) -> Menu<Screens> {
Menu::new(
"sound",
vec![
MenuItem::label("Toggles sound and music"),
MenuItem::action("On", Actions::SoundOn).checked(state.sound_on),
MenuItem::action("Off", Actions::SoundOff).checked(!state.sound_on),
],
)
}
fn controls_menu(state: &CustomState) -> Menu<Screens> {
let mut players: Vec<usize> = state.controls.keys().copied().collect();
players.sort();
Menu::new(
"controls",
players
.into_iter()
.map(|player| MenuItem::screen(format!("Player {player}"), Screens::Player(player)))
.collect(),
)
}
fn player_controls_menu(state: &CustomState, player: usize) -> Menu<Screens> {
let selected_control = state.controls[&player];
let mut entries: Vec<_> = vec![
ControlDevice::keyboard1(),
ControlDevice::keyboard2(),
ControlDevice::keyboard3(),
ControlDevice::keyboard4(),
]
.iter()
.map(|kb| {
MenuItem::action(kb.to_string(), Actions::Control(player, *kb))
.checked(kb.id() == selected_control.id())
})
.collect();
for (pad, title) in &state.gamepads {
let device = ControlDevice::Gamepad { gamepad_id: pad.id };
entries.push(
MenuItem::action(title, Actions::Control(player, device))
.checked(device.id() == selected_control.id()),
)
}
Menu::new("players", entries)
}
fn event_reader(
mut commands: Commands,
mut event_reader: EventReader<MyEvent>,
mut next_state: ResMut<NextState<GameState>>,
) {
for event in event_reader.iter() {
match event {
MyEvent::CloseSettings => {
bevy_quickmenu::cleanup(&mut commands);
next_state.set(GameState::Game);
}
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum ControlDevice {
Gamepad {
gamepad_id: usize,
},
Keyboard {
title: &'static str,
description: &'static str,
keyboard_id: usize,
left: KeyCode,
right: KeyCode,
action: KeyCode,
},
}
impl ControlDevice {
pub fn id(&self) -> usize {
match self {
ControlDevice::Gamepad { gamepad_id, .. } => *gamepad_id,
ControlDevice::Keyboard { keyboard_id, .. } => *keyboard_id,
}
}
}
impl std::fmt::Display for ControlDevice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ControlDevice::Gamepad { gamepad_id } => {
f.write_fmt(format_args!("Gamepad {gamepad_id}",))
}
ControlDevice::Keyboard { title, .. } => f.write_fmt(format_args!("{title}")),
}
}
}
impl ControlDevice {
pub fn keyboard1() -> ControlDevice {
ControlDevice::Keyboard {
title: "Keyboard 1",
description: "Left / Right + M",
keyboard_id: 42001,
left: KeyCode::Left,
right: KeyCode::Right,
action: KeyCode::M,
}
}
pub fn keyboard2() -> ControlDevice {
ControlDevice::Keyboard {
title: "Keyboard 2",
description: "A / D + B",
keyboard_id: 42002,
left: KeyCode::A,
right: KeyCode::D,
action: KeyCode::B,
}
}
pub fn keyboard3() -> ControlDevice {
ControlDevice::Keyboard {
title: "Keyboard 3",
description: "I / O + K",
keyboard_id: 42003,
left: KeyCode::I,
right: KeyCode::O,
action: KeyCode::K,
}
}
pub fn keyboard4() -> ControlDevice {
ControlDevice::Keyboard {
title: "Keyboard 4",
description: "T / Y + H",
keyboard_id: 42004,
left: KeyCode::T,
right: KeyCode::Y,
action: KeyCode::H,
}
}
}
}
mod game {
use super::*;
pub struct Game;
impl Plugin for Game {
fn build(&self, app: &mut App) {
app.add_systems(OnEnter(GameState::Game), setup_system)
.add_systems(Update, detect_close_system.run_if(GameState::is_game));
}
}
#[derive(Component)]
struct GameComponent;
fn setup_system(mut commands: Commands, asset_server: Res<AssetServer>) {
commands
.spawn((TextBundle::from_section(
"Return Key to go back to menu",
TextStyle {
font: asset_server.load("font.otf"),
font_size: 30.0,
color: Color::WHITE,
},
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(60.0),
left: Val::Px(50.0),
..default()
}),))
.insert(GameComponent);
}
fn detect_close_system(
mut commands: Commands,
keyboard_input: Res<Input<KeyCode>>,
mut next_state: ResMut<NextState<GameState>>,
game_items: Query<Entity, With<GameComponent>>,
) {
if keyboard_input.just_pressed(KeyCode::Return) {
for entity in game_items.iter() {
commands.entity(entity).despawn_recursive();
}
next_state.set(GameState::Settings);
}
}
}