# Bevy Engine

Bevy engine 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. Let's code Pong (or Boing!) from the book Code the Classic. Here is the code source of all the games. 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.

Version of Bevy used: bevy = "0.1.3"

# 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.

# It's time to talk about plugins

This 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()
        });
}