Table of Contents
I got bored.
Entity Component Systems1 seemed cool.
Bevy seemed super cool.
I didn't want to come up with some crazy game to learn how to use ECS, so Pong2 felt like a great classic game to clone.
Let's go!
Entity Component Systems
Entities: A logical grouping of Components. Generally, these are just IDs.
Component: Data that represents an aspect.
System: A process that acts upon desired components
Pong
The way I see it, there are only 4 "types" components:
- Paddles
- Ball
- Score
- Bouncy wall
Initial window setup
const WINDOW_WIDTH: f32 = 800.;
const WINDOW_HEIGHT: f32 = 600.;
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(
WindowPlugin{
primary_window: Some(Window{
title: "ponger".into(),
resolution: (WINDOW_WIDTH,WINDOW_HEIGHT).into(),
resizable: false,
enabled_buttons: EnabledButtons{
minimize: true,
maximize: false,
close: true,
},
..default()
}),
..default()
}
))
.add_systems(Startup,setup_camera)
.run();
}
fn setup_camera(mut commands: Commands){
commands.spawn(Camera2dBundle::default());
}
Components
// Marker component. Indicates an entity is the player.
#[derive(Component)]
struct Player;
//Marker component. Indicates an entity is the ball.
#[derive(Component)]
struct Ball;
//Velocity component. Gives an entity velocity.
#[derive(Component)]
struct Velocity{
x: f32,
y: f32,
}
Adding a ball
const BALL_RADIUS: f32 = 10.;
fn setup_ball(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>
)
{
let mut rng = rand::thread_rng();
let vel: f32 = rng.gen();
commands.spawn((MaterialMesh2dBundle {
mesh: meshes.add(shape::Circle::new(BALL_RADIUS).into()).into(),
material: materials.add(ColorMaterial::from(Color::WHITE)),
transform: Transform::from_translation(Vec3::new(0., 0., 0.)),
..default()
},(Velocity{x: -1.5, y:-0.3},Ball)));
}
If you're new to ECS, notice that I'm not spawning a Ball
that has Velocity
, as you probably would in OOP.
Instead, I'm simply spawning a MaterialMesh2dBundle
, and I'm tagging it with the individual components. MaterialMesh2dBundle
is simply another entity that the developers of Bevy created. I'm using this here because it provides me an easy way to access commonly-used attributes, like Transform
.
Really soak in the fact that Ball
has no concept of Velocity
, and think about how you could now give anything the attribute of Velocity
just by tagging the entity.
Adding a player paddle
const PADDLE_WIDTH: f32 = 20.;
const PADDLE_HEIGHT: f32 = 100.;
fn setup_rect(mut commands: Commands) {
commands.spawn((SpriteBundle {
sprite: Sprite {
color: Color::WHITE,
custom_size: Some(Vec2::new(PADDLE_WIDTH, PADDLE_HEIGHT)),
..default()
},
transform: Transform::from_translation(Vec3::new((-WINDOW_WIDTH/2.)+30., 0., 0.)),
..default()
},(Player,Velocity{x: 0., y:0.})));
}
Here, we have a different pair of components: Player
and Velocity
.
Full bouncy wall
For now, let's just make all 4 walls bouncy. This way the ball won't ever travel out of the camera view.
fn wall_collisions(mut ball_query: Query<(&mut Velocity, &Transform),With<Ball>>){
let (mut ball_velocity, ball_transform) = ball_query.single_mut();
if ball_transform.translation.y >= WINDOW_HEIGHT/2.{
ball_velocity.y = -ball_velocity.y;
}else if ball_transform.translation.y <= -WINDOW_HEIGHT/2.{
ball_velocity.y = -ball_velocity.y;
}
if ball_transform.translation.x >= WINDOW_WIDTH/2.{
ball_velocity.x = -ball_velocity.x;
}else if ball_transform.translation.x <= -WINDOW_WIDTH/2.{
ball_velocity.x = -ball_velocity.x;
}
}
We've got a query that looks for anything that has components Velocity
and Transform
, but only if it comes With
marker component Ball
.
We first get a mutable Velocity
, and a non-mutable Transform
. We're using .single_mut()
as we're sure there's ever only ever 1 ball in the world at any time. If for whatever reason there are more than one, this would panic.
What follows is a really naive solution to the problem. We simply check if the ball is outside of any of the walls. If it is, we simply reverse either the x
or y
Velocity
component.
Paddle Collision
fn ball_collision(
mut player_query: Query<&Transform,With<Player>>,
mut ball_query: Query<(&mut Velocity, &Transform),With<Ball>>
){
let player_transform = player_query.single_mut(); //ensure only 1
let (mut ball_velocity, ball_transform) = ball_query.single_mut(); //ensure only 1
let upper_y = player_transform.translation.y + PADDLE_HEIGHT/2.;
let lower_y = player_transform.translation.y - PADDLE_HEIGHT/2.;
let upper_x = player_transform.translation.x + PADDLE_WIDTH/2.;
if (ball_transform.translation.x - BALL_RADIUS..ball_transform.translation.x + BALL_RADIUS).contains(&upper_x)
&&
(
(lower_y..upper_y).contains(&ball_transform.translation.y)
)
{
ball_velocity.x = -ball_velocity.x;
ball_velocity.y = -ball_velocity.y;
}
}
Player Movement & Position
fn player_movement_control(mut query: Query<(&mut Transform, &mut Velocity), With<Player>>, keyboard_input: Res<Input<KeyCode>>){
let (transform, mut velocity) = query.single_mut();
if keyboard_input.pressed(KeyCode::Up) && transform.translation.y + PADDLE_HEIGHT/2. <= WINDOW_HEIGHT/2.{
velocity.y = PLAYER_MOVEMENT_SPEED;
}
else if keyboard_input.pressed(KeyCode::Down) && transform.translation.y - PADDLE_HEIGHT/2. >= -WINDOW_HEIGHT/2.{
velocity.y = -PLAYER_MOVEMENT_SPEED;
}
else{
velocity.y = 0.;
}
}
fn update_position(mut query: Query<(&Velocity, &mut Transform)>){
for (velocity, mut transform) in query.iter_mut(){
transform.translation.x += velocity.x;
transform.translation.y += velocity.y;
}
}
Really simple way to restrict movement to the Y axis, and will refuse to move out of the window
Putting it all together
Remember to add all the systems!
.add_systems(Startup,setup_camera)
.add_systems(Startup, setup_ball)
.add_systems(Startup, setup_rect)
.add_systems(Update, update_position)
.add_systems(Update, player_movement_control)
.add_systems(Update, ball_collision)
.add_systems(Update, wall_collisions)
If at any point you thought to yourself "wow, why are so many things hardcoded. doesn't this noob know how to make it more responsive?"
Honestly, I don't really care, because that's not the point. I want to learn ECS, not make a perfect implementation of Pong.
What's next?
Refactoring, better controls, another player, multiplayer through web? scores!
A lot more to go.
https://en.wikipedia.org/wiki/Entity_component_system
https://en.wikipedia.org/wiki/Pong