Thursday, March 10, 2022

Bevy Game Engine Tutorial: Introduction and First Sprite

Introduction

Bevy is a game engine written in the Rust programming language with a strong ergonomic entity component system model.  Bevy focuses on fast iteration times and being a massively parallel, easy to use engine.  Currently there is not a graphical editor for Bevy but that is in their future plans. This tutorial will target version 0.6 of the engine which is the current version as of March 2022.

This text tutorial is made to complement a video tutorial at LogicProject's Youtube Channel


Project Creation

A Bevy project is started like any other Rust project by running:

cargo new bevy_tutorial

Next we will set up the Cargo.toml file to optimize the engine code as much as possible even when debugging.  This will give us fast debug builds to test but will cause longer compile times when building the engine.  Thankfully Bevy is designed so that we rarely will have to rebuild the entire engine so this is acceptable.  Also we will link dynamically to the engine which will help even more with fast compile times.

[package]
name = "bevy_tutorial"
version = "0.1.0"
edition = "2021"

[profile.dev]
opt-level = 1

[profile.dev.package."*"]
opt-level = 3

[dependencies]
bevy = { version = "0.6", features = ["dynamic"] }

First Window

Now we are ready to create a simple window using Bevy.  Bevy is centered around an App data structure which we create and customize with the builder pattern.  In main we will construct an App and add the DefaultPlugins to it.  The DefaultPlugins set up asset management, rendering, input, diagnostics, and many other things that almost every game will need.  Finally we call run() which will start the game loop and give control to Bevy until the game is closed.

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .run();
}

Now we can customize this window by adding resources to the App.  A resource in bevy is any Rust data structure that we only need one copy of for our game.  The simplest included resource is ClearColor which is used by the DefaultPlugins to set the background of our new blank window.  Adding a resource is as simple as calling insert_resource with an instance of the data structure we want to add. 

use bevy::prelude::*;

pub const CLEAR: Color = Color::rgb(0.1, 0.1, 0.1);

fn main() {
    App::new()
        .insert_resource(ClearColor(CLEAR))
        .add_plugins(DefaultPlugins)
        .run();
}

We can also add a WindowDescriptor resource to the App which will setup things such as the width/height, title, and vsync settings.

use bevy::prelude::*;

pub const CLEAR: Color = Color::rgb(0.1, 0.1, 0.1);
pub const RESOLUTION: f32 = 16.0 / 9.0;

fn main() {
    let height = 900.0;
    App::new()
        .insert_resource(ClearColor(CLEAR))
        .insert_resource(WindowDescriptor {
            width: height * RESOLUTION,
            height: height,
            title: "Bevy Tutorial".to_string(),
            vsync: true,
            resizable: false,
            ..Default::default()
        })
        .add_plugins(DefaultPlugins)
        .run();
}

Our First System

Now we are ready to start writing some real code for our game.  The first thing we need is a system to spawn a camera.  Systems are a major part of the ECS design mode and thankfully Bevy has made it incredibly easy to create a system.  A system is just a standard Rust function with some limited options for parameters.  Here we need Commands which will allow us to spawn an entity with all the components we need for it to behave like a camera.  Specifically the command we want is spawn_bundle because in Bevy, bundles are groups of components for easy use and we will be using the OrthographicCameraBundle. Cameras are used by rendering systems in the DefaultPlugins so we need one for anything to appear on screen. After we create the function we add it as a startup system to the app so it runs once right when the game starts.

fn main() {
    let height = 900.0;
    App::new()
        // Cut
        .add_startup_system(spawn_camera)
        .run();
}

fn spawn_camera(mut commands: Commands) {
    let mut camera = OrthographicCameraBundle::new_2d();

    camera.orthographic_projection.top = 1.0;
    camera.orthographic_projection.bottom = -1.0;

    camera.orthographic_projection.right = 1.0 * RESOLUTION;
    camera.orthographic_projection.left = -1.0 * RESOLUTION;

    camera.orthographic_projection.scaling_mode = ScalingMode::None;

    commands.spawn_bundle(camera);
}

One thing I did was setup the camera top, bottom, left and right coordinates by hand and set the scaling mode to none.  This is a personal preference that makes it easier to rationalize about where things are on the screen for me.  The default uses x and y values that depend on window size which is more difficult for me to manage.

Asset Management

Bevy handles all game assets for us with their AssetServer and Assets structures. The AssetServer handles loading files and gives us a Handle to use the data when the file is loaded.  It is important to note files are loaded in the background and may not be available for the first few frames after loading.  You can query the AssetServer to see when the data is truly loaded.  The Assets structures are generic maps over the different types of assets and let you go between a handle to an asset and the actual asset data.  

For our game we want to use a sprite-sheet (Specifically an ASCII Code page available from the assets on the github link for this project) which in Bevy would be called a TextureAtlas.  First we need the asset in a folder named "assets" for the AssetServer to find our file.  Then we just need to load the image giving us a handle to an image then we create a TextureAtlas which will reference that image.  Finally we will keep that handle in a custom resource so we can easily access it in other systems to create sprites.

The sprite-sheet I use for test graphics


struct AsciiSheet(Handle<TextureAtlas>);

fn load_ascii(
    mut commands: Commands,
    assets: Res<AssetServer>,
    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
) {
    let image = assets.load("Ascii.png");
    let atlas = TextureAtlas::from_grid_with_padding(
        image, 
        Vec2::splat(9.0), 
        16, 
        16, 
        Vec2::splat(2.0));

    let atlas_handle = texture_atlases.add(atlas);

    commands.insert_resource(AsciiSheet(atlas_handle));
}

One thing to note is I chose to us an image with padding to prevent bleeding of pixels from one sprite to the next.  This is a common problem and padding the sprites is the easiest fix I have found.

Example of pixels bleeding between sprites

Finally, we need to add this as a startup system to the app.  A critical thing here is this system needs to run before any other systems that will be expecting the AsciiSheet resource.  Here I achieve this by adding the system to the PreStartupStage in Bevy which will make this run before the rest of startup functions.  There are many different ways to handle system ordering in Bevy.

fn main() {
    let height = 900.0;
    App::new()
        // Cut
        .add_startup_system(spawn_camera)
        .add_startup_system_to_stage(StartupStage::PreStartup, load_ascii)
        .add_plugins(DefaultPlugins)
        .run();
}

First Sprite

Now we are finally ready to draw a sprite on screen.  All we need is another system which will spawn our player.  We are going to make the player a TextureAtlasSprite as we want to use the atlas we just loaded and the index is going to be 1 which is the smiley face on the sheet.  This time the bundle we want to spawn is called SpriteSheetBundle and we will give it the sprite we just created and the handle to the atlas from the resource.  We set the translation z value of our new sprite to 900 to render it on top of most things in the game as visible z values range from 0 to 1000 with higher values on top by default. We also add a Name component for easier debugging in the future.

fn spawn_player(mut commands: Commands, ascii: Res<AsciiSheet>) {
    let mut sprite = TextureAtlasSprite::new(1);
    sprite.color = Color::rgb(0.3, 0.3, 0.9);
    sprite.custom_size = Some(Vec2::splat(1.0));

    commands
        .spawn_bundle(SpriteSheetBundle {
            sprite: sprite,
            texture_atlas: ascii.0.clone(),
            transform: Transform {
                translation: Vec3::new(0.0, 0.0, 900.0),
                ..Default::default()
            },
            ..Default::default()
        })
        .insert(Name::new("Player"));


We now have a sprite drawn on the screen and we could call it a day.  But as one extra push I want to add a background to the face as it is transparent and will look weird wandering around with the grass showing through.

To do this we create another sprite in the same way.  We then use id() at the end of commands to force commands to return the entity we just created.  Next we need to add the background as a child of the player using Bevy's hierarchy functionality.  Because the hierarchy will handle the transforms we set the z of the background to be -1 so it appears behind the player. 

fn spawn_player(mut commands: Commands, ascii: Res<AsciiSheet>) {
    let mut sprite = TextureAtlasSprite::new(1);
    sprite.color = Color::rgb(0.3, 0.3, 0.9);
    sprite.custom_size = Some(Vec2::splat(1.0));

    let player = commands
        .spawn_bundle(SpriteSheetBundle {
            sprite: sprite,
            texture_atlas: ascii.0.clone(),
            transform: Transform {
                translation: Vec3::new(0.0, 0.0, 900.0),
                ..Default::default()
            },
            ..Default::default()
        })
        .insert(Name::new("Player"))
        .id();

    let mut background_sprite = TextureAtlasSprite::new(0);
    background_sprite.color = Color::rgb(0.5, 0.5, 0.5);
    background_sprite.custom_size = Some(Vec2::splat(1.0));

    let background = commands
        .spawn_bundle(SpriteSheetBundle {
            sprite: background_sprite,
            texture_atlas: ascii.0.clone(),
            transform: Transform {
                translation: Vec3::new(0.0, 0.0, -1.0),
                ..Default::default()
            },
            ..Default::default()
        })
        .insert(Name::new("Background"))
        .id(); //id() gives back the entity after creation

    commands.entity(player).push_children(&[background]);
}

Don't forget to add spawn_player as a startup system or cargo will give you a dead_code warning!



Conclusion

In this tutorial, we covered the basics of creating a window and loading/displaying a sprite in Bevy.  I hope this series will be helpful to people trying to learn the engine and feel free to leave any comments or suggestions. Thank you for reading!



4 comments:

  1. Thanks for putting this together! I hope you make more blogs since the text version is a bit easier for me to follow. Cheers :)

    ReplyDelete
  2. Casino Vegas Slots for Little Baby Dragons in the Island of 토토사이트 Sky, who tried on the function of captain of the star cruiser. India’s online gambling market consistent increase in the past years is mainly outcome of} growth of the middle class and the unfold of Internet entry across the country. At this moment, cricket is probably the most guess on sport in India and most punters choose regional websites that present straightforward cost options. Based on such expertise, the gaming apply offers clients with fastidiously planned, practical, solution-oriented authorized services. Fourth, the hung out on gambling in the high severity gambling level and the number of skilled gambling behaviors in the low to reasonable gambling severity level have a significant impression. These results showed that habitual gambling is extremely associated to the adolescent drawback gambling in ecological and organic attributes.

    ReplyDelete
  3. A good instance of that is the Mega Moolah slot, which broke the world document to be the biggest Jackpot paid out on the earth. Other branded slots that introduced a status for Microgaming are Game of Thrones slots and Jurassic Park on-line slot. People like themed Slots end result of|as a end result of} they supply a personal touch to the game with familiar storylines and personalized parts like themed bonus video 토토사이트 games, creative symbols, and intricate design.

    ReplyDelete
  4. Some of our favorites embrace the almighty Cyberpunk City with 98.2% 코인카지노 RTP, and in addition Mystic Elements, 777 Deluxe, and extra. However, it could possibly} become boring half in} the identical video games once more, so we’ve singled out the best online slots and the sites to seek out|to search out} them on. While free slots are nice way|a good way|an effective way} to play just for enjoyable and to hone your abilities earlier than spending actual money, nothing quite compares to the joys and excitement of actual cash gambling. As have the ability to|you possibly can} see from the desk under, each actual cash and free video games come with their very own benefits and disadvantages. This is the place you'll discover free spins and jackpots and knowing your paylines means knowing your chances of winning.

    ReplyDelete

Bevy Game Engine Tutorial: Introduction and First Sprite

Introduction Bevy is a game engine written in the Rust programming language with a strong ergonomic entity component system model.  Bevy foc...