Posted on

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)

img


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.


1

https://en.wikipedia.org/wiki/Entity_component_system

2

https://en.wikipedia.org/wiki/Pong