Control settings menu

Pong Update: Customizable Controls, Pause Menu, and Code Structure

With the Christmas holiday providing extra time, I’ve been focusing on improving my Rust and Bevy-based Pong game. After upgrading the repository to Bevy 0.15, I implemented several major enhancements to improve the game’s functionality and codebase organization.

Implementing Customizable Controls with Leafwing Input Manager

One of the key improvements was implementing customizable controls using the Leafwing Input Manager. This powerful feature allows players to remap their keyboard controls according to their preferences.

Core Game Actions

I defined three essential game actions:

  • Moving the paddle up
  • Moving the paddle down
  • Accessing the menu

These actions default to Arrow Up, Arrow Down, and ESC respectively, but players can now customize them to their liking.

Implementation Details

I started by defining these actions in code and add the default controls:

#[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Debug, Reflect)]
pub enum GameAction {
    Up,
    Down,
    Menu,
}

impl GameAction {
    fn default_input_map() -> InputMap<GameAction> {
        let mut input_map = InputMap::default();
        input_map.insert(Self::Up, KeyCode::ArrowUp);
        input_map.insert(Self::Down, KeyCode::ArrowDown);
        input_map.insert(Self::Menu, KeyCode::Escape);

        input_map
    }
}

To handle control remapping, I created a resource to track which action is being remapped:

#[derive(Resource, Default, Clone, Copy)]
pub struct ControlRemapping {
    current_action: Option<GameAction>,
    is_listening: bool,
}

impl ControlRemapping {
    pub fn start_remapping(control: GameAction) -> Self {
        Self {
            current_action: Some(control),
            is_listening: true,
        }
    }

    pub fn stop_remapping(&mut self) {
        self.current_action = None;
        self.is_listening = false;
    }
}

Then I added a system that listens for key presses and updates the mappings accordingly:

pub fn listen_for_keys(
    mut mapping: ResMut<ControlRemapping>,
    mut key_map: ResMut<InputMap<GameAction>>,
    keys: Res<ButtonInput<KeyCode>>,
) {
    if mapping.is_listening && mapping.current_action.is_some() {
        if let Some(control) = mapping.current_action {
            for key in keys.get_pressed() {
                key_map.clear_action(&control);
                key_map.insert(control, *key);

                mapping.stop_remapping();
            }
        }
    }
}

Adding a Robust Pause System

Implementing a pause menu proved more complex than I initially expected. The pause system needed to:

  1. Stop processing user input
  2. Stop the computer player’s movements
  3. Halt the physics simulation

State Management

I implemented this using a dual-state system:

  • GameState: Controls menu navigation (settings, controls, gameplay)
  • PausedState: Manages the pause status during gameplay

This approach allows the game to maintain its state while paused and show the pause menu without losing any gameplay information.

app
  .init_state::<GameState>()
  .init_state::<PausedState>()
  .configure_sets(Update, (
     PlayingSet
        .run_if(in_state(GameState::Playing))
        .run_if(in_state(PausedState::Playing)),
     PausedSet
        .run_if(in_state(GameState::Playing))
        .run_if(in_state(PausedState::Paused)),
  ))

Halting physics simulation

Rapier defines three sets of systems it runs:

  1. PhysicsSet::SyncBackend –> Runs systems that initialise and synchronise backend data structures
  2. PhysicsSet::StepSimulation –> Runs the physics simulation
  3. PhysicsSet::Writeback –> Writes the updates of the physics simulation into the Rapier Bevy components added to the game

To properly pause the physics simulation, I configured Rapier’s StepSimulation set to respect the pause state:

app.configure_sets(FixedUpdate, (
    PhysicsSet::StepSimulation
        .run_if(in_state(GameState::Playing))
        .run_if(in_state(PausedState::Playing)),
))

Codebase Restructuring

As the project grows, the code became more unwielding and harder to understand and maintain. I asked Claude.ai how to restructure the code and it came up with the following new structure:

src/
├── main.rs
├── game/
│   ├── mod.rs
│   ├── states.rs         # GameState and PausedState
│   ├── controls.rs       # Controls enum and input mapping
│   └── settings.rs       # Difficulty and other game settings
├── pong/
│   ├── mod.rs
│   ├── components.rs     # Ball, Paddle, ScoreField components
│   ├── resources.rs      # Score resource
│   ├── systems.rs        # Core game systems
│   └── constants.rs      # Move constants to separate file
└── ui/
    ├── mod.rs
    ├── menu/
    │   ├── mod.rs
    │   ├── components.rs # MenuButton, MenuOptions etc
    │   ├── actions.rs    # MenuAction trait and implementations
    │   └── builder.rs    # MenuBuilder implementation
    └── systems.rs        # Menu rendering systems

I liked the proposed structure as it follows a logical separation of concerns. So I moved the code around to adhere to this structure.

I made one change compared to Claud’s suggestion. I didn’t use the mod.rs structure as apparently this is the old way of doing things in Rust. The new recommended way to work with modules, would be to add a rust file with the same name as the directory. For example: ui.rs in the root folder would represent the ui/mod.rs module.

Conclusion

These improvements have significantly enhanced both the player experience and code maintainability. The source code is available on GitHub, and I welcome feedback from the community to help improve the implementation further as I continue learning Rust and Bevy.

Feel free to check out the full source code on GitHub and share your suggestions for improvement!

Home » Pong Update: Customizable Controls, Pause Menu, and Code Structure

Comments

Leave a Reply

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