Player selection screen allowing player to select human or AI opponents

Adding Player Selection to My Bevy Pong Game

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:

  1. Created a new StartGame state for the player selection screen
  2. Added Player 2 controls to the controls screen
  3. Moved the controls settings to the main menu
  4. Implemented a GameSettings Bevy resource to use in game generation
  5. 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.

Note: Full source code available on Github

Home » Adding Player Selection to My Bevy Pong Game

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *