You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
learn-wgpu/docs/showcase/pong/README.md

266 lines
9.9 KiB
Markdown

# Pong
<div class="warning">
This example is currently broken. I'm planning on rewriting it to use `wgsl` as well as being more sound design wise.
</div>
![](./pong.png)
Practically the "Hello World!" of games. Pong has been remade thousands of times. I know Pong. You know Pong. We all know Pong. That being said, this time I wanted to put a little more effort than most people do. This showcase has a basic menu system, sounds, and different game states.
The architecture is not the best as I prescribed to the "get things done" mentality. If I were to redo this project, I'd change a lot of things. Regardless, let's get into the postmortem.
## The Architecture
I was messing around with separating state from the render code. It ended up similar to an entity component system.
I had a `State` class with all of the objects in the scene. This included the ball and the paddles, as well as the text for the scores and even the menu. `State` also included a `game_state` field of type `GameState`.
```rust
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum GameState {
MainMenu,
Serving,
Playing,
GameOver,
Quiting,
}
```
The `State` class didn't have any methods on it as I was taking a more data oriented approach. Instead I created a `System` trait, and created multiple structs that implemented it.
```rust
pub trait System {
#[allow(unused_variables)]
fn start(&mut self, state: &mut state::State) {}
fn update_state(
&self,
input: &input::Input,
state: &mut state::State,
events: &mut Vec<state::Event>,
);
}
```
The systems would be in charge of controlling updating the different objects state (position, visibility, etc), as well as updating the `game_state` field. I created all the systems on startup, and used a `match` on `game_state` to determine which ones should be allow to run (the `visiblity_system` always runs as it is always needed).
```rust
visiblity_system.update_state(&input, &mut state, &mut events);
match state.game_state {
state::GameState::MainMenu => {
menu_system.update_state(&input, &mut state, &mut events);
if state.game_state == state::GameState::Serving {
serving_system.start(&mut state);
}
},
state::GameState::Serving => {
serving_system.update_state(&input, &mut state, &mut events);
play_system.update_state(&input, &mut state, &mut events);
if state.game_state == state::GameState::Playing {
play_system.start(&mut state);
}
},
state::GameState::Playing => {
ball_system.update_state(&input, &mut state, &mut events);
play_system.update_state(&input, &mut state, &mut events);
if state.game_state == state::GameState::Serving {
serving_system.start(&mut state);
} else if state.game_state == state::GameState::GameOver {
game_over_system.start(&mut state);
}
},
state::GameState::GameOver => {
game_over_system.update_state(&input, &mut state, &mut events);
if state.game_state == state::GameState::MainMenu {
menu_system.start(&mut state);
}
},
state::GameState::Quiting => {},
}
```
It's definitely not the cleanest code, but it works.
I ended up having 6 systems in total.
1. I added the `VisibilitySystem` near the end of development. Up to that point, all the systems had to set the `visible` field of the objects. That was a pain, and cluttered the logic. Instead I decided to create the `VisiblitySystem` to handle that.
2. The `MenuSystem` handled controlling what text was focused, and what would happen when the user pressed the enter key. If the `Play` button was focused, pressing enter would change `game_state` to `GameState::Serving` which would start the game. The `Quit` button would shift to `GameState::Quiting`.
3. The `ServingSystem` sets the balls position to `(0.0, 0.0)`, updates the score texts, and shifts into `GameState::Playing` after a timer.
4. The `PlaySystem` controls the players. It allows them to move, and keeps them from leaving the play space. This system runs on both `GameState::Playing` as well as `GameState::Serving`. I did this to allow the players to reposition themselves before the serve. The `PlaySystem` also will shift into `GameState::GameOver` when on of the players scores is greater than 2.
5. The `BallSystem` system controls the balls movement as well as its bouncing of walls/players. It also updates the score and shifts to `GameState::Serving` when the ball goes off the side of the screen.
6. The `GameOver` system updates the `win_text` and shifts to `GameState::MainMenu` after a delay.
I found the system approach to quite nice to work with. My implementation wasn't the best, but I would like working with it again. I might even implement my own ECS.
## Input
The `System` trait, originally had a `process_input` method. This became a problem when I was implementing allowing players to move between serves. The players would get stuck when the `game_state` switched from `Serving` to `Playing` as the inputs were getting stuck. I only called `process_input` on systems that were currently in use. Changing that would be finicky, so I decided to move all the input code into its own struct.
```rust
use winit::event::{VirtualKeyCode, ElementState};
#[derive(Debug, Default)]
pub struct Input {
pub p1_up_pressed: bool,
pub p1_down_pressed: bool,
pub p2_up_pressed: bool,
pub p2_down_pressed: bool,
pub enter_pressed: bool,
}
impl Input {
pub fn new() -> Self {
Default::default()
}
pub fn update(&mut self, key: VirtualKeyCode, state: ElementState) -> bool {
let pressed = state == ElementState::Pressed;
match key {
VirtualKeyCode::Up => {
self.p2_up_pressed = pressed;
true
}
VirtualKeyCode::Down => {
self.p2_down_pressed = pressed;
true
}
VirtualKeyCode::W => {
self.p1_up_pressed = pressed;
true
}
VirtualKeyCode::S => {
self.p1_down_pressed = pressed;
true
}
VirtualKeyCode::Return => {
self.enter_pressed = pressed;
true
}
_ => false
}
}
pub fn ui_up_pressed(&self) -> bool {
self.p1_up_pressed || self.p2_up_pressed
}
pub fn ui_down_pressed(&self) -> bool {
self.p1_down_pressed || self.p2_down_pressed
}
}
```
This works really well. I simply pass this struct into the `update_state` method.
## Render
I used [wgpu_glyph](https://docs.rs/wgpu_glyph) for the text, and white quads for the ball and paddles. There's not much to say here, it's Pong after all.
I did mess around with batching however. It was totally overkill for this project, but it was a good learning experience. Here's the code if you're interested.
```rust
pub struct QuadBufferBuilder {
vertex_data: Vec<Vertex>,
index_data: Vec<u32>,
current_quad: u32,
}
impl QuadBufferBuilder {
pub fn new() -> Self {
Self {
vertex_data: Vec::new(),
index_data: Vec::new(),
current_quad: 0,
}
}
pub fn push_ball(self, ball: &state::Ball) -> Self {
if ball.visible {
let min_x = ball.position.x - ball.radius;
let min_y = ball.position.y - ball.radius;
let max_x = ball.position.x + ball.radius;
let max_y = ball.position.y + ball.radius;
self.push_quad(min_x, min_y, max_x, max_y)
} else {
self
}
}
pub fn push_player(self, player: &state::Player) -> Self {
if player.visible {
self.push_quad(
player.position.x - player.size.x * 0.5,
player.position.y - player.size.y * 0.5,
player.position.x + player.size.x * 0.5,
player.position.y + player.size.y * 0.5,
)
} else {
self
}
}
pub fn push_quad(mut self, min_x: f32, min_y: f32, max_x: f32, max_y: f32) -> Self {
self.vertex_data.extend(&[
Vertex {
position: (min_x, min_y).into(),
},
Vertex {
position: (max_x, min_y).into(),
},
Vertex {
position: (max_x, max_y).into(),
},
Vertex {
position: (min_x, max_y).into(),
},
]);
self.index_data.extend(&[
self.current_quad * 4 + 0,
self.current_quad * 4 + 1,
self.current_quad * 4 + 2,
self.current_quad * 4 + 0,
self.current_quad * 4 + 2,
self.current_quad * 4 + 3,
]);
self.current_quad += 1;
self
}
pub fn build(self, device: &wgpu::Device) -> (StagingBuffer, StagingBuffer, u32) {
(
StagingBuffer::new(device, &self.vertex_data),
StagingBuffer::new(device, &self.index_data),
self.index_data.len() as u32,
)
}
}
```
## Sound
I used [rodio](https://docs.rs/rodio) for sound. I created a `SoundPack` class to store the sounds. Deciding how to get the sounds to play took some thinking. I chose to pass in a `Vec<state::Event>` into the `update_state` method. The system would then push an event to the `Vec`. The `Event` enum is listed below.
```rust
#[derive(Debug, Copy, Clone)]
pub enum Event {
ButtonPressed,
FocusChanged,
BallBounce(cgmath::Vector2<f32>),
Score(u32),
}
```
I was going to have `BallBounce` play a positioned sound using a `SpatialSink`, but I was getting clipping issues, and I wanted to be done with the project. Aside from that, the events system worked nicely.
## Summary
A fun project to work on. It was overly architected, and kinda hard to make changes, but a good experience none the less.