# Pilko Game

Pilko is a Pong clone, written with Rust. The source code is on GitLab (opens new window).

Bevy engine (opens new window) is a young 2D/3D Engine written in Rust. The last time I wrote a game was with pygame around 2008. Pygame is a bit different, by design.

But, let's create a 2D Game. The idea comes from the book Code the Classic (opens new window). Here is the code source of all the games (opens new window). These games are developed with pyzero, a pygame-based even easier lib for creating games.

# Notes

  • I work on a Mac and it doesn't have been tested on other OS.
  • I assume you know how to code, in Rust.
  • This article is my notes while playing with Bevy.
  • If you have question regarding Bevy, RTFM.

# Versions

  • Version of Bevy: bevy = "0.1.3"
  • Verison of

# Let's get started

This is probably the first thing we want to have: a window.

  • Configure the Window with WindowDescriptor (not mandatory)
  • Give a Background Color with ClearColor (not mandatory)
  • And loads the default plugins (very recommanded)
use bevy::{prelude::*, render::pass::ClearColor};

fn main() {
    App::build()
        .add_resource(WindowDescriptor {
            title: "I am a window!".to_string(),
            // width: 300,
            // height: 300,
            resizable: false,
            // mode: WindowMode::Fullscreen { use_size: false },
            ..Default::default()
        })
        .add_resource(ClearColor(Color::rgb(0.2, 0.2, 0.8)))
        .add_default_plugins()
        .run();
}

# Exit the app with Escape Key

Probably after that, we want to kill the app by pressing Escape. I don't know why I do that first all the time. Maybe because a game without keyboard would be weird.

We can also use events to distribute the keyboard (and mouse) events to all systems, but let's keep it simple for now.

use bevy::{app::AppExit, prelude::*};

fn keyboard_input_system(
    keyboard_input: Res<Input<KeyCode>>,
    mut app_exit_events: ResMut<Events<AppExit>>,
) {
    if keyboard_input.pressed(KeyCode::Escape) {
        app_exit_events.send(AppExit)
    }
}

fn main() {
    App::build()
        .add_default_plugins()
        .add_system(keyboard_input_system.system())
        .run();
}

Yeah, we wrote our first system, bound in fn main() to the application.

But this is also where the systems feels magic to me. How do I know what to put as parameters to the systems. The .system() is the magic piece I still don't fully understand.

Whatever.

# Let's draw Gabe

# As a simple sprite

Hi, Gabe: idle Gabe

use bevy::{app::AppExit, prelude::*};

fn setup_sprite_system(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    let texture_handle = asset_server.load("assets/gabe_idle.png").unwrap();

    commands
        .spawn(Camera2dComponents::default())
        .spawn(SpriteComponents {
            material: materials.add(texture_handle.into()),
            scale: Scale(6.0),
            ..Default::default()
        });
}

// .. fn keyboard_input_system()

fn main() {
    App::build()
        .add_default_plugins()
        .add_startup_system(setup_sprite_system.system())
        .add_system(keyboard_input_system.system())
        .run();
}

# As a sprite sheet

Hi, running Gabe: running Gabe

Replace setup_sprite_system by the 2 new systems bellow:

use bevy::{app::AppExit, prelude::*};

fn setup_sprite_sheet_system(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut textures: ResMut<Assets<Texture>>,
    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
) {
    let texture_handle = asset_server
        .load_sync(&mut textures, "assets/gabe_idle_run.png")
        .unwrap();
    let texture = textures.get(&texture_handle).unwrap();
    let texture_atlas = TextureAtlas::from_grid(texture_handle, texture.size, 6, 1);
    let texture_atlas_handle = texture_atlases.add(texture_atlas);

    commands
        .spawn(Camera2dComponents::default())
        .spawn(SpriteSheetComponents {
            texture_atlas: texture_atlas_handle,
            scale: Scale(6.0),
            translation: Translation(Vec3::new(0.0, -215.0, 0.0)),
            ..Default::default()
        })
        .with(Timer::from_seconds(0.1, true));
}

fn animate_sprite_sheet_system(
    texture_atlases: Res<Assets<TextureAtlas>>,
    mut query: Query<(&mut Timer, &mut TextureAtlasSprite, &Handle<TextureAtlas>)>,
) {
    for (timer, mut sprite, texture_atlas_handle) in &mut query.iter() {
        if timer.finished {
            let texture_atlas = texture_atlases.get(&texture_atlas_handle).unwrap();
            sprite.index = ((sprite.index as usize + 1) % texture_atlas.textures.len()) as u32;
        }
    }
}

// .. fn keyboard_input_system()

fn main() {
    App::build()
        .add_default_plugins()
        .add_startup_system(setup_sprite_sheet_system.system())
        .add_system(animate_sprite_sheet.system())
        .add_system(keyboard_input_system.system())
        .run();
}

# Result

running gabe video

Nice already, isn't it?

# I Like to Move It

To move it, we need to create a Component, called Gabe. We give it some properties like speed.

struct Gabe {
    speed: f32,
}

and we need to connect it in setup_sprite_sheet_system

commands
    .spawn(Camera2dComponents::default())
    .spawn(SpriteSheetComponents {
        texture_atlas: texture_atlas_handle,
        scale: Scale(6.0),
        ..Default::default()
    })
    .with(Gabe { speed: 350. }) // Hello, Gabe!
    .with(Timer::from_seconds(0.1, true));

And finally, we are going to Query for Gabe and Translation in order to modify the position, based on Time. Here, again, they are magically connected.

fn keyboard_input_system(
    time: Res<Time>,
    window: Res<Windows>,
    keyboard_input: Res<Input<KeyCode>>,
    mut app_exit_events: ResMut<Events<AppExit>>,
    mut query: Query<(&Gabe, &mut Translation)>,
) {
    let window = window.get_primary().unwrap();
    let width = ((window.width - 24 * 6) / 2) as f32;

    if keyboard_input.pressed(KeyCode::Escape) {
        app_exit_events.send(AppExit)
    }

    for (gabe, mut translation) in &mut query.iter() {
        let mut direction = 0.0;
        if keyboard_input.pressed(KeyCode::Left) {
            direction -= 1.0;
        }

        if keyboard_input.pressed(KeyCode::Right) {
            direction += 1.0;
        }

        *translation.0.x_mut() += time.delta_seconds * direction * gabe.speed;

        // bound Gabe
        *translation.0.x_mut() = f32::max(-width, f32::min(width, translation.0.x()));
    }
}

What's cool here, is also to see how we can have access to the Windows size.

Ok, so now we can move Gabe. Looks like we have the basic for a game. 🚀

The full code of that is available on GitLab (opens new window).

# It's time to talk about plugins

They are, as far as I understand, just a group of systems, resources... and are easy to do:

struct MyPlugin;

impl Plugin for MyPlugin {
    fn build(&self, app: &mut AppBuilder) {
        // add whatever to the app
    }
}

Then, bound it to the App in main()

fn main() {
    App::build()
        .add_default_plugins()
        .add_plugin(MyPlugin)
        .run();
}

# FPS plugin

Because I am sure you want to have it, here is a plugin to show FPS.

use bevy::{
    diagnostic::{Diagnostics, FrameTimeDiagnosticsPlugin},
    prelude::*,
};

pub struct FpsPlugin;

impl Plugin for FpsPlugin {
    fn build(&self, app: &mut AppBuilder) {
        app.add_plugin(FrameTimeDiagnosticsPlugin)
            .add_startup_system(setup_fps_system.system())
            .add_system(fps_update_system.system());
    }
}

fn fps_update_system(diagnostics: Res<Diagnostics>, mut text: Mut<Text>) {
    if let Some((Some(fps), Some(average))) = diagnostics
        .get(FrameTimeDiagnosticsPlugin::FPS)
        .map(|x| (x.value(), x.average()))
    {
        text.value = format!("{:<3.3} ({:<3.3})", fps, average);
    }
}

fn setup_fps_system(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands
        .spawn(UiCameraComponents::default())
        .spawn(TextComponents {
            text: Text {
                value: "FPS".to_string(),
                font: asset_server.load("assets/fonts/bit5x3.ttf").unwrap(),
                style: TextStyle {
                    font_size: 10.0,
                    color: Color::WHITE,
                },
            },
            transform: Transform::new(Mat4::from_translation(Vec3::new(0.0, 0.0, 2.0))),
            ..Default::default()
        });
}

# Background and Paddles

Let's setup now our first draft for Boing! with what we've learn previously. So, we have, for the moment:

  • Our game ground (or room in fact)
  • 2 paddles that move
  • A ball in a middle.
use bevy::{app::AppExit, prelude::*, render::pass::ClearColor, window::WindowMode};

// --- Paddles ---
#[derive(PartialEq)]
enum Side {
   Left,
   Right,
}

struct Paddle {
   speed: f32,
   side: Side,
}

fn setup_paddles_system(
   mut commands: Commands,
   asset_server: Res<AssetServer>,
   mut materials: ResMut<Assets<ColorMaterial>>,
) {
   let paddle_left_handle = asset_server.load("assets/images/bat00.png").unwrap();
   let paddle_right_handle = asset_server.load("assets/images/bat10.png").unwrap();

   commands
       .spawn(Camera2dComponents::default())
       .spawn(SpriteComponents {
           material: materials.add(paddle_left_handle.into()),
           translation: Translation(Vec3::new(-360., 0., 1.0)),
           ..Default::default()
       })
       .with(Paddle {
           speed: 350.,
           side: Side::Left,
       })
       .spawn(SpriteComponents {
           material: materials.add(paddle_right_handle.into()),
           translation: Translation(Vec3::new(360., 0., 1.0)),
           ..Default::default()
       })
       .with(Paddle {
           speed: 350.,
           side: Side::Right,
       });
}

// --- Ball ---
struct Ball {
   speed: f32,
}

fn setup_ball_system(
   mut commands: Commands,
   asset_server: Res<AssetServer>,
   mut materials: ResMut<Assets<ColorMaterial>>,
) {
   let ball_handle = asset_server.load("assets/images/ball.png").unwrap();

   commands
       .spawn(Camera2dComponents::default())
       .spawn(SpriteComponents {
           material: materials.add(ball_handle.into()),
           translation: Translation(Vec3::new(0., 0., 1.0)),
           ..Default::default()
       })
       .with(Ball { speed: 500. });
}

// --- Background ---
fn setup_background_system(
   mut commands: Commands,
   asset_server: Res<AssetServer>,
   mut materials: ResMut<Assets<ColorMaterial>>,
) {
   let texture_handle = asset_server.load("assets/images/table.png").unwrap();

   commands
       .spawn(Camera2dComponents::default())
       .spawn(SpriteComponents {
           material: materials.add(texture_handle.into()),
           ..Default::default()
       });
}

// --- Keyboard ---
fn keyboard_input_system(
   time: Res<Time>,
   keyboard_input: Res<Input<KeyCode>>,
   mut app_exit_events: ResMut<Events<AppExit>>,
   mut query: Query<(&Paddle, &mut Translation)>,
) {
   if keyboard_input.pressed(KeyCode::Escape) {
       app_exit_events.send(AppExit)
   }

   for (paddle, mut translation) in &mut query.iter() {
       let mut direction = 0.0;

       if paddle.side == Side::Right {
           if keyboard_input.pressed(KeyCode::J) {
               direction += 1.0;
           }

           if keyboard_input.pressed(KeyCode::N) {
               direction -= 1.0;
           }
       } else {
           if keyboard_input.pressed(KeyCode::F) {
               direction += 1.0;
           }

           if keyboard_input.pressed(KeyCode::V) {
               direction -= 1.0;
           }
       }

       *translation.0.y_mut() += time.delta_seconds * direction * paddle.speed;
       *translation.0.y_mut() = f32::max(-160., f32::min(160., translation.0.y()));
   }
}

// --- Main Application ---
fn main() {
   App::build()
       .add_resource(WindowDescriptor {
           title: String::from("Bevy Boing!"),
           width: 800,
           height: 480,
           resizable: false,
           mode: WindowMode::Fullscreen { use_size: true },
           ..Default::default()
       })
       .add_resource(ClearColor(Color::rgb(0., 0., 0.)))
       .add_default_plugins()
       .add_startup_system(setup_paddles_system.system())
       .add_startup_system(setup_ball_system.system())
       .add_startup_system(setup_background_system.system())
       .add_system(keyboard_input_system.system())
       .run();
}