Amethyst

From its website, Amethyst is described as a “data-driven game engine written in Rust”. The engine uses an ECS (entity, component, system) for game logic and architecture. I am still relatively new to Amethyst, but I understand from my experience that ECS is used in Amethyst as a way to architect a program by separating code into components, entities, and systems so that the program can be abstractly parallelized. In other words, through using ECS and Amethyst, your game can automatically take advantage of multiple CPU cores. There are other game engine options for Rust, but Amethyst appears to be the most popular and ambitious of the options. To see other options for game development in Rust I recommend checking out arewegameyet.com.

ECS

ECS is core to Amethyst. There is no getting around using ECS when developing a game with Amethyst. At first. this was difficult for me. I previously had been more used to game engines like Pygame and LÖVE. However, after a couple weeks of using the engine, I found that ECS makes the process of game programming not only more efficient, but also in many ways easier. For example, ECS makes it much easier to quickly understand other people’s code. It is very easy to determine at any given point whether the code you are looking at is a system, component or entity and subsequently figure out exactly what that the code is doing.

Components and Entities

Components are the building blocks of entities and entities are the building blocks of the game. An example of an entity is the player-controlled spaceship in my space shooter game. When defining an entity you often use .with() to indicate that you are creating the entity “with” that component. Below is the example of me doing this in my space shooter game. In the code, you can see that I’m defining an entity representing my spaceship player to be put in the game. The entity contains a SpriteRender component, a custom Spaceship component that I defined, a Transform component, and a Transparent component.

let mut local_transform = Transform::default();
local_transform.set_xyz(GAME_WIDTH / 2.0, GAME_HEIGHT / 6.0, 0.9);

let sprite_render = SpriteRender {
    sprite_sheet: sprite_sheet_handle.clone(),
    sprite_number: 0,
};

world
    .create_entity()
    .with(sprite_render)
    .with(Spaceship {
        width: SPACESHIP_WIDTH,
        height: SPACESHIP_HEIGHT,
        hitbox_width: SPACESHIP_HITBOX_WIDTH,
        hitbox_height: SPACESHIP_HITBOX_HEIGHT,
        max_speed: SPACESHIP_MAX_SPEED,
        current_velocity_x: 0.0,
        current_velocity_y: 0.0,
        acceleration_x: SPACESHIP_ACCELERATION_X,
        deceleration_x: SPACESHIP_DECELERATION_X,
        acceleration_y: SPACESHIP_ACCELERATION_Y,
        deceleration_y: SPACESHIP_DECELERATION_Y,
        fire_speed: SPACESHIP_STARTING_FIRE_SPEED,
        fire_reset_timer: 0.0,
        damage: SPACESHIP_STARTING_DAMAGE,
        barrel_cooldown: SPACESHIP_BARREL_COOLDOWN,
        barrel_reset_timer: 0.0,
        barrel_speed: SPACESHIP_BARREL_SPEED,
        barrel_action_right: false,
        barrel_action_left: false,
        barrel_duration: SPACESHIP_BARREL_DURATION,
        barrel_action_timer: SPACESHIP_BARREL_DURATION,
        barrel_damage: 0.0,
        pos_x: local_transform.translation().x,
        pos_y: local_transform.translation().y,
    })
    .with(local_transform)
    .with(Transparent)
    .build();

The above example includes a component that is custom, the Spaceship, and components that are included with Amethyst such as SpriteRender, Transform, and Transparent. Below is the code where I define my custom Spaceship component. It contains a lot of different data that is used in different systems to control the game and it implements Component. It is also implemented with storage which I’ll get into in the Systems section.

pub struct Spaceship {
    pub width: f32,
    pub height: f32,
    pub hitbox_width: f32,
    pub hitbox_height: f32,
    pub current_velocity_x: f32,
    pub current_velocity_y: f32,
    pub max_speed: f32,
    pub acceleration_x: f32,
    pub deceleration_x: f32,
    pub acceleration_y: f32,
    pub deceleration_y: f32,
    pub fire_speed: f32,
    pub fire_reset_timer: f32,
    pub damage: f32,
    pub barrel_cooldown: f32,
    pub barrel_reset_timer: f32,
    pub barrel_speed: f32,
    pub barrel_action_left: bool,
    pub barrel_action_right: bool,
    pub barrel_duration: f32,
    pub barrel_action_timer: f32,
    pub barrel_damage: f32,
    pub pos_x: f32,
    pub pos_y: f32,
}

impl Component for Spaceship {
    type Storage = DenseVecStorage<Self>;
}

Systems

Systems are where the logic of the game happens. Above, where the Spaceship component is defined, it is implemented with type Storage = DenseVecStorage<Self>;. Creating a system consists mainly of writing and reading to storages associated with entities and components. This is done by creating a type out of all of the SystemData needed for the system. Then, as needed, data can be iterated through, read, and modified to create game logic. In the example below, you can see that I also get user input in this system.

pub struct SpaceshipSystem;
impl<'s> System<'s> for SpaceshipSystem {

    type SystemData = (
        Entities<'s>,
        WriteStorage<'s, Transform>,
        WriteStorage<'s, Spaceship>,
        WriteStorage<'s, Enemy>,
        Read<'s, InputHandler<String, String>>,
        Read<'s, Time>,
        ReadExpect<'s, SpriteResource>,
        ReadExpect<'s, LazyUpdate>,
    );

    fn run(&mut self, (entities, mut transforms, mut spaceships, mut enemies, input, time, sprite_resource, lazy_update): Self::SystemData) {

        let mut shoot = input.action_is_down("shoot").unwrap();
        let mut barrel_left = input.action_is_down("barrel_left").unwrap();
        let mut barrel_right= input.action_is_down("barrel_right").unwrap();


        for (spaceship, transform) in (&mut spaceships, &mut transforms).join() {

            spaceship.pos_x = transform.translation().x;
            spaceship.pos_y= transform.translation().y;

            //firing cooldown
            if spaceship.fire_reset_timer > 0.0 {
                spaceship.fire_reset_timer -= time.delta_seconds();
                shoot = false;
            }

            //barrel roll input cooldown
            if spaceship.barrel_reset_timer > 0.0 && !spaceship.barrel_action_left && !spaceship.barrel_action_right {
                spaceship.barrel_reset_timer -= time.delta_seconds();
                barrel_left = false;
                barrel_right = false;
            }

            //barrel roll action cooldown
            if spaceship.barrel_action_left || spaceship.barrel_action_right {

                //if currently barrel rolling can't initiate another barrel roll
                barrel_left = false;
                barrel_right = false;

                //countdown to end of barrel roll if time left else set velocity to the appropriate max speed, stop the action, and reset cooldown
                if spaceship.barrel_action_timer > 0.0 {
                    spaceship.barrel_action_timer -= time.delta_seconds();
                }else {


                    if spaceship.barrel_action_left {
                        spaceship.current_velocity_x = -1.0 * spaceship.max_speed;
                    }

                    if spaceship.barrel_action_right {
                        spaceship.current_velocity_x = spaceship.max_speed;
                    }

                    spaceship.barrel_action_left = false;
                    spaceship.barrel_action_right = false;
                    spaceship.barrel_reset_timer = spaceship.barrel_cooldown;
                    for enemy in (&mut enemies).join() {
                        enemy.barrel_damaged = false;
                    }

                }

            }

            if shoot && !spaceship.barrel_action_left && !spaceship.barrel_action_right {
                let fire_position = Vector3::new(
                    transform.translation()[0], transform.translation()[1] + spaceship.height / 2.0, 0.1,
                );

                fire_blast(&entities, &sprite_resource, 3, fire_position, spaceship.damage, spaceship.current_velocity_x, spaceship.current_velocity_y, &lazy_update);
                spaceship.fire_reset_timer = spaceship.fire_speed;
            }

            if barrel_left {
                spaceship.barrel_action_left = true;
                spaceship.barrel_action_timer = spaceship.barrel_duration;
            }

            if barrel_right {
                spaceship.barrel_action_right = true;
                spaceship.barrel_action_timer = spaceship.barrel_duration;
            }

        }
    }
}

Just like when defining entities, systems are added to a dispatcher by using .with() as well. Below is the code for the dispatcher used in the main game state (if you want to learn about states I encourage you to read the Amethyst book).

impl Default for SpaceShooter {
    fn default() -> Self {
        SpaceShooter {
            dispatcher: DispatcherBuilder::new()
                .with(systems::SpaceshipSystem, "spaceship_system", &[])
                .with(systems::BlastSystem, "blast_system", &[])
                .with(systems::EnemySystem, "enemy_system", &[])
                .with(systems::SpawnerSystem, "spawner_system", &[])
                .with(systems::PlayerHitSystem, "player_hit_system", &[])
                .with(systems::ExplosionSystem, "explosion_system", &[])
                .with(systems::ItemSystem, "item_system", &[])
                .with(systems::BarrelRollSystem, "barrel_roll_system", &[])
                .with(systems::SpaceshipMovementSystem, "spaceship_movement_system", &[])
                .with(systems::ItemSpawnSystem, "item_spawn_system", &[])
                .build(),
        }
    }
}

Space Shooter Game

This game began as a way for me to learn Rust and Amethyst and has turned into a project that I plan on continuing. You can view the readme file on the github page. On it, I have defined mechanics, visual, audio, and gameplay objectives that I plan to implement into the game.

Overall I plan on making this into a game inspired like games such as Raiden and The Binding of Isaac. Raiden is an arcade game where an aircraft is controlled to shoot down enemies and score points. The Binding of Isaac is a game with randomly generated levels, with rooms, enemies, items, and bosses. I have already implemented a pool that items can be added to for upgrading the player’s ship and I have implemented a pool that enemies can be pulled from to be randomly spawned.

Below is gameplay of my space shooter game at its current state.


If you want to check out my code you can go to its github page. If you are interested in contributing you can feel free to send me an email at cdsupina@micronote.tech.