The next evolution of my Pong! game introduces a player selection screen that replaces the previous settings screen. Players can now choose between human and computer opponents, with different AI difficulty levels for an enhanced gaming experience.
Key Changes
I made several core updates to implement this feature:
- Created a new StartGame state for the player selection screen
- Added Player 2 controls to the controls screen
- Moved the controls settings to the main menu
- Implemented a GameSettings Bevy resource to use in game generation
- Updated the game code to support both computer and human-controlled paddles
Building the Player Selection System
The most exciting part was designing the player selection screen. I created a new Bevy GameSettings resource to manage this functionality:
use bevy::prelude::*;
#[derive(Resource)]
pub struct GameSettings {
player1: PlayerType,
player2: PlayerType,
}
impl GameSettings {
pub fn get_player1(&self) -> &PlayerType {
&self.player1
}
pub fn get_player2(&self) -> &PlayerType {
&self.player2
}
pub fn update_players(&mut self, player_num: usize, player_type: PlayerType) {
match player_num {
1 => self.player1 = player_type,
2 => self.player2 = player_type,
_ => panic!("Invalid player num {}", player_num),
}
}
}
impl Default for GameSettings {
fn default() -> Self {
Self {
player1: PlayerType::Human,
player2: PlayerType::Computer(Difficulty::Easy),
}
}
}
#[derive(Component, PartialEq, Copy, Clone)]
pub enum PlayerType {
Human,
Computer(Difficulty),
}
#[derive(Default, PartialEq, Copy, Clone)]
pub enum Difficulty {
#[default]
Easy,
Difficult,
Impossible,
}
impl Difficulty {
pub fn speed(&self) -> f32 {
match self {
Difficulty::Easy => 2.,
Difficulty::Difficult => 4.,
Difficulty::Impossible => 6.,
}
}
}
The GameSettings struct tracks both players through the PlayerType enum, which handles both human players and computer opponents with varying difficulty levels. By default, the game starts with a human player versus an easy-level computer opponent.
Creating the Menu Interface
I leveraged my existing EGUI-based menu framework to build the player selection screen. The builder pattern I developed in November makes creating new menus quick and intuitive:
fn start_game_menu(mut commands: Commands, contexts: EguiContexts, settings: ResMut<GameSettings>) {
MenuBuilder::new("New Game")
.with_top_spacing(100.)
.add_component(MenuLabel::new("Player 1"))
.add_component(MenuLayoutHorizontal::new()
.add_component(MenuSelectableLabel::new(
"Human",
matches!(settings.get_player1(), PlayerType::Human),
CommandMenuAction::new(UpdatePlayerCommand::new(1, PlayerType::Human))
))
.add_component(MenuSelectableLabel::new(
"Easy",
matches!(settings.get_player1(), PlayerType::Computer(difficulty) if difficulty == &Difficulty::Easy),
CommandMenuAction::new(UpdatePlayerCommand::new(1, PlayerType::Computer(Difficulty::Easy)))
))
.add_component(MenuSelectableLabel::new(
"Difficult",
matches!(settings.get_player1(), PlayerType::Computer(difficulty) if difficulty == &Difficulty::Difficult),
CommandMenuAction::new(UpdatePlayerCommand::new(1, PlayerType::Computer(Difficulty::Difficult)))
))
.add_component(MenuSelectableLabel::new(
"Impossible",
matches!(settings.get_player1(), PlayerType::Computer(difficulty) if difficulty == &Difficulty::Impossible),
CommandMenuAction::new(UpdatePlayerCommand::new(1, PlayerType::Computer(Difficulty::Impossible)))
))
)
.add_component(MenuLabel::new("Player 2"))
.add_component(MenuLayoutHorizontal::new()
.add_component(MenuSelectableLabel::new(
"Human",
matches!(settings.get_player2(), PlayerType::Human),
CommandMenuAction::new(UpdatePlayerCommand::new(2, PlayerType::Human))
))
.add_component(MenuSelectableLabel::new(
"Easy",
matches!(settings.get_player2(), PlayerType::Computer(difficulty) if difficulty == &Difficulty::Easy),
CommandMenuAction::new(UpdatePlayerCommand::new(2, PlayerType::Computer(Difficulty::Easy)))
))
.add_component(MenuSelectableLabel::new(
"Difficult",
matches!(settings.get_player2(), PlayerType::Computer(difficulty) if difficulty == &Difficulty::Difficult),
CommandMenuAction::new(UpdatePlayerCommand::new(2, PlayerType::Computer(Difficulty::Difficult)))
))
.add_component(MenuSelectableLabel::new(
"Impossible",
matches!(settings.get_player2(), PlayerType::Computer(difficulty) if difficulty == &Difficulty::Impossible),
CommandMenuAction::new(UpdatePlayerCommand::new(2, PlayerType::Computer(Difficulty::Impossible)))
))
)
.add_component(MenuButton::new("Start Game", ChangeStateMenuAction::new(GameState::Playing)))
.add_component(MenuButton::new("Back", ChangeStateMenuAction::new(GameState::Main)))
.build(contexts, &mut commands);
}
Extending the Menu System
I needed to add new functionality to update the GameSettings resource. Thanks to Bevy’s flexible command system, I created a new MenuAction with just a few lines of code:
pub struct CommandMenuAction<C> where C: Command+Clone {
command: C,
}
impl<C> CommandMenuAction<C> where C: Command+Clone {
pub fn new(command: C) -> Self {
Self { command }
}
}
impl<C> MenuAction for CommandMenuAction<C> where C: Command+Clone {
fn execute(&self, commands: &mut Commands) {
commands.queue(self.command.clone());
}
}
The Power of Custom Commands
The UpdatePlayerCommand provides a clean way to modify game settings:
#[derive(Clone)]
pub struct UpdatePlayerCommand {
player_num: usize,
player_type: PlayerType,
}
impl UpdatePlayerCommand {
pub fn new(player_num: usize, player_type: PlayerType) -> Self {
Self {
player_num,
player_type,
}
}
}
impl Command for UpdatePlayerCommand {
fn apply(self, world: &mut World) {
if let Some(mut settings) = world.get_resource_mut::<GameSettings>() {
settings.update_players(self.player_num, self.player_type);
}
}
}
Conclusion
This update showcases the power of ECS systems and Bevy’s capabilities. The resulting code is clean, maintainable, and extensible – exactly what you want in game development with Rust.
Leave a Reply