Logo

The Amethyst Engine

Presentation

Howdy! This book will teach you everything you need to know about building video games and interactive simulations with the Amethyst game engine. This engine is written entirely in Rust, a safe and fast systems programming language, and sports a clean and modern design. More correctly, though, Amethyst is actually a collection of separate libraries and tools that collectively make up a game engine.

Amethyst is free and open source software, distributed under a dual license of MIT and Apache. This means that the engine is given to you at no cost and its source code is completely yours to tinker with. The code is available on GitHub. Contributions and feature requests will always be welcomed!

Getting started

This book is split into several sections, with this introduction being the first. The others are:

  • Getting Started – Prepare your computer for Amethyst development.
  • Concepts – An overview of the concepts used in Amethyst. Recommended.
  • Pong Tutorial – Build a basic pong game in Rust.
  • Math – A quick introduction to doing math with Amethyst.
  • Animation – Explains the architecture of the amethyst_animation crate.
  • Controlling System Execution – Shows you how to structure more complex games that need to change the System graph.
  • Glossary – Defines special terms used throughout the book.
  • Appendix A: Config Files – Shows you how to define your data in RON files.

Read the crate-level API documentation for more details.

Motivation

Most of us have worked with quite a few game engines over the years, namely Unity, Unreal Engine, JMonkeyEngine and many more. While they all are pretty solid solutions if you want to build a quality game, each have their own pros and cons that you have to weigh before using them, especially in regards to performance and scalability.

We think that basing the Amethyst engine on good and modern principles will allow us to make an open source game engine that can actually be more performant than those engines. Those principles are:

  1. Modularity.

    Modularity is at the core of the Unix philosophy, which proved itself to be an excellent way of developing software over the years. You will always be free to use the built-in modules, or to write your own and integrate them easily into the engine. Since modules are small and well integrated, it is easier to reason about what they do and how they relate to other modules.

  2. Parallelism.

    Modern computers, even cheap ones, all have multithreading with multicore CPUs. We expect that over the years, there will be more and more opportunities for parallelism to improve performance. With a proper parallel engine, we are convinced that your game will be more and more performant over the years without even needing you to update it.

  3. Data-oriented/Data-driven.

    Building your game around the data makes it really easy to prototype and quickly build a game. Complex behaviours like swapping assets during gameplay become a breeze, making testing and balancing a lot faster.

Why use Amethyst?

While there are a lot of great building blocks in the Rust ecosystem, using the Amethyst engine instead of building your own game engine definitely has a lot of advantages.

First of all, the engine is based on the Specs library, which is a common base on which the engine's concepts are built. For a great introduction to game development with Rust and an Entity Component System, see this great talk by Catherine West. Amethyst's take on ECS is described in the concepts section of the book. A lot of features have been glued together using those:

There are the obvious ones:

  • Transformations
  • Graphics
  • Windowing
  • Inputs
  • Audio
  • Etc...

And also the less known but also essential features:

  • Animations
  • Gltf
  • Locales
  • Networking

If you were not to use Amethyst, not only would you need to create all those features (or use pre-existing crates), but you would also need to glue the layers together.

Amethyst does all of this for you, so that you can focus on making your game instead of worrying about the low-level details.

Futhermore, because of the architecture of Amethyst, almost all the parts are both configurable and replaceable. This means that if you do want to change something to suit your needs, there's always a way to do it.

For example, the rodio crate is currently used for the audio features in the engine, but if you would rather use something more complex or a custom solution, all you have to do is add some glue that moves the data coming from Specs into the library that you are using to play and control the audio, without even having to touch the engine code!

Contributing

We are always happy to welcome new contributors!

To know where to start, we suggest you read our contribution guidelines

If you want to contribute, or have questions, let us know either on GitHub, or on Discord.

Getting started

Setting up Rust

We recommend using rustup to easily install the latest stable version of rust. Instructions should be on screen once rustup is downloaded.

Updating Rust: If you already have Rust installed, make sure you're using the latest version by running rustup update.

We recommend using the stable version of Rust, as Rust nightlies tend to break rather often.

Using the stable toolchain: Rustup can be configured to default to the stable toolchain by running rustup default stable.

Required dependencies

Please check the dependencies section of the README.md for details on what dependencies are required for compiling Amethyst.

Please note that you need to have a functional graphics driver installed. If you get a panic about the renderer unable to create the rendering context when trying to run an example, a faulty driver installation could be the issue.

Setting up Amethyst

You can either use the Amethyst CLI or cargo to set up your project.

Amethyst CLI (Easiest)

If you wish to use the Amethyst cli tool, you can install it like so

cargo install amethyst_tools

and then run

amethyst new <game-name>

you should get Cargo.toml, src/main.rs and config/display.ron.

Starter Project

If you want to get running as quickly as possibly and start playing around with Amethyst, you can also use a starter project. These are specifically made for certain types of games, and will set you up with the groundwork needed to start right away.
The README.md file on these will include everything you need to know to run the starter project.

Note: Right now, the only starter available is for 2D games. This will expand over time, and offer more options for different types of games.

Cargo (Manual)

In case you're doing this with cargo, here's what you need to do:

  • Add amethyst as dependency in your Cargo.toml.
  • Create a config folder and put a display.ron in it.
  • (Optional) Copy the code from one of amethyst's examples.

Important note on versioning

Amethyst is divided in two major versions:

  • The released crates.io version, which is the latest version available on crates.io
  • The git (master) version, which is the current unreleased development snapshot of Amethyst available on Github

Note: You can see which version you're currently looking at by checking the URL in your browser. The book / documentation for master contains "master" in the address, the crates.io version is called "stable".

Depending on the book version that you choose to read, make sure that the amethyst version in your Cargo.toml matches that.

For the released crates.io version, you should have something like this:

[dependencies]
amethyst = "LATEST_CRATES.IO_VERSION"

The latest crates.io version can be found here.

If you want to use the latest unreleased changes, your Cargo.toml file should look like this:

[dependencies]
amethyst = { git = "https://github.com/amethyst/amethyst", rev = "COMMIT_HASH" }

The commit hash part is optional. It indicates which specific commit your project uses, to prevent unexpected breakage when we make changes to the git version.

Concepts behind Amethyst

Amethyst uses quite a few concepts that you might not be familiar with. This section of the book explains what they are, how they work and how they relate to each other.

If you are a practical person and want to quickly get into the code, you can skip to the pong tutorial section of the book, which is focused on practice. That said, reading this section is suggested, as it can be hard to understand the examples without knowing the theory presented here.

If you don't understand how something works in amethyst, knowing the concepts presented here will help you understand how some implementations are made.

Chapters

State

What is a state?

The word "state" can mean a lot of different things in computer science. In the case of amethyst, it is used to represent the "game state".

A game state is a general and global section of the game.

Example

As an example, let's say you are making a pong game.

  • When the user opens up the game, it first loads all the assets and shows a loading screen.
  • Then, the main menu shows up, asking you if you want to start a game in single or multiplayer.
  • Once you select an option, the game displays the paddles and the ball and starts playing.
  • By pressing escape, you can toggle the "pause" menu.
  • Once the score limit is reached, a result screen is shown with a button to go back to the main menu.

The game can be divided into different states:

  • LoadingState
  • MainMenuState
  • GameplayState
  • PauseState
  • ResultState

While you could effectively insert all the game's logic into a single state GameState, dividing it into multiple parts makes it much easier to reason about and maintain.

State Manager

Amethyst has a built-in state manager, which allows easily switching between different States. It is based on the concept of a pushdown-automaton, which is a combination of a Stack and a State Machine.

Stack

The stack concept makes it so you can "push" States on top of each other.

If we take the pong example of earlier, you can push the PauseState over the GameplayState.

When you want to go out of pause, you pop the PauseState out of the stack and you are back into the GameplayState, just as you left it.

State Machine

The concept of State Machine can be pretty complex, but here we will only explain the basics of it. The State Machine is usually composed of two elements: Transitions and Events.

Transitions are simply the "switching" between two states.

For example, from LoadingState, go to state MainMenuState.

Amethyst has multiple types of transitions.

  • You can Push a State over another.
  • You can also Switch a State, which replaces the current State with a new one.

Events are what trigger the transitions. In the case of amethyst, it is the different methods called on the State. Continue reading to learn about them.

Life Cycle

States are only valid for a certain period of time, during which a lot of things can occur. A State contains methods that reflect the most common of those events:

  • on_start: When a State is added to the stack, this method is called on it.
  • on_stop: When a State is removed from the stack, this method is called on it.
  • on_pause: When a State is pushed over the current one, the current one is paused, and this method is called on it.
  • on_resume: When the State that was pushed over the current State is popped, the current one resumes, and this method is called on the now-current State.
  • handle_event: Allows easily handling events, like the window closing or a key being pressed.
  • fixed_update: This method is called on the active State at a fixed time interval (1/60th second by default).
  • update: This method is called on the active State as often as possible by the engine.
  • shadow_update: This method is called as often as possible by the engine on all States which are on the StateMachines stack, including the active State. Unlike update, this does not return a Trans.
  • shadow_fixed_update: This method is called at a fixed time interval (1/60th second by default) on all States which are on the StateMachines stack, including the active State. Unlike fixed_update, this does not return a Trans.

If you aren't using SimpleState or EmptyState, you must implement the update method to call data.data.update(&mut data.world).

Game Data

States can have arbitrary data associated with them. If you need to store data that is tightly coupled to your State, the classic way is to put it in the State's struct.

States also have internal data, which is any type T. In most cases, the two following are the most used: () and GameData.

() means that there is no data associated with this State. This is usually used for tests and not for actual games. GameData is the de-facto standard. It is a struct containing a Dispatcher. This will be discussed later.

When calling your State's methods, the engine will pass a StateData struct which contains both the World (which will also be discussed later) and the Game Data type that you chose.

Code

Yes! It's finally time to get some code in here!

Here will just be a small code snippet that shows the basics of State's usage. For more advanced examples, see the following pong tutorial.

Creating a State

extern crate amethyst;
use amethyst::prelude::*;

struct GameplayState {
    /// The `State`-local data. Usually you will not have anything.
    /// In this case, we have the number of players here.
    player_count: u8,
}

impl SimpleState for GameplayState {
    fn on_start(&mut self, _data: StateData<'_, GameData<'_, '_>>) {
        println!("Number of players: {}", self.player_count);
    }
}

That's a lot of code, indeed!

We first declare the State's struct GameplayState.

In this case, we give it some data: player_count, a byte.

Then, we implement the SimpleState trait for our GameplayState. SimpleState is a shorthand for State<GameData<'static, 'static>, ()> where GameData is the internal shared data between states.

Switching State

Now, if we want to change to a second state, how do we do it?

Well, we'll need to use one of the methods that return the Trans type.

Those are:

  • handle_event
  • fixed_update
  • update

Let's use handle_event to go to the PausedState and come back by pressing the "Escape" key.

extern crate amethyst;
use amethyst::prelude::*;
use amethyst::input::{VirtualKeyCode, is_key_down};

struct GameplayState;
struct PausedState;

// This time around, we are using () instead of GameData, because we don't have any `System`s that need to be updated.
// (They are covered in the dedicated section of the book.)
// Instead of writing `State<(), StateEvent>`, we can instead use `EmptyState`.
impl EmptyState for GameplayState {
    fn handle_event(&mut self, _data: StateData<()>, event: StateEvent) -> EmptyTrans {
        if let StateEvent::Window(event) = &event {
            if is_key_down(&event, VirtualKeyCode::Escape) {
                // Pause the game by going to the `PausedState`.
                return Trans::Push(Box::new(PausedState));
            }
        }

        // Escape isn't pressed, so we stay in this `State`.
        Trans::None
    }
}

impl EmptyState for PausedState {
    fn handle_event(&mut self, _data: StateData<()>, event: StateEvent) -> EmptyTrans {
        if let StateEvent::Window(event) = &event {
            if is_key_down(&event, VirtualKeyCode::Escape) {
                // Go back to the `GameplayState`.
                return Trans::Pop;
            }
        }

        // Escape isn't pressed, so we stay in this `State`.
        Trans::None
    }
}

Event Handling

As you already saw, we can handle events from the handle_event method. But what is this weird StateEvent all about?

Well, it is simply an enum. It regroups multiple types of events that are emitted throughout the engine by default. To change the set of events that the state receives, you create a new event enum and derive EventReader for that type.

# #[macro_use] extern crate amethyst;
# use amethyst::prelude::*;
# use amethyst::ui::UiEvent;
# use amethyst::input::{VirtualKeyCode, is_key_down};
# use amethyst::winit::Event;

// These imports are required for the #[derive(EventReader)] code to build
use amethyst::core::{
    ecs::{Read, SystemData, World},
    shrev::{ReaderId, EventChannel},
    EventReader
};

#[derive(Clone, Debug)]
pub struct AppEvent {
    data: i32,
}

#[derive(Debug, EventReader, Clone)]
#[reader(MyEventReader)]
pub enum MyEvent {
    Window(Event),
    Ui(UiEvent),
    App(AppEvent),
}

struct GameplayState;

impl State<(), MyEvent> for GameplayState {
    fn handle_event(&mut self, _data: StateData<()>, event: MyEvent) -> Trans<(), MyEvent> {
        match event {
            MyEvent::Window(_) => {}, // Events related to the window and inputs.
            MyEvent::Ui(_) => {}, // Ui event. Button presses, mouse hover, etc...
            MyEvent::App(ev) => println!("Got an app event: {:?}", ev),
        };

        Trans::None
    }
}

# fn main() {}

To make Application aware of the change to which events to send to the state, you also need to supply both the event type, and the EventReader type (the name you give in the #[reader(SomeReader)] derive attribute) when the Application is created. This is done by replacing Application::build (or Application::new) with CoreApplication::<_, MyEvent, MyEventReader>::build() (or CoreApplication::<_, MyEvent, MyEventReader>::new()).

Note: Events are gathered from EventChannels. EventChannels are covered in the dedicated book section.

Entity and Component

What are Entity and Component?

An Entity represents a single object in your world. Component represents one aspect of an object. For example, a bottle of water has a shape, a volume, a color and is made of a material (usually plastic). In this example, the bottle is the entity, and the properties are components.

Entity and Component in Amethyst

In an inheritance design, entity usually contains components. All the data and methods related to an entity are stored within. However, in the ECS design, entity is just a general purpose object. In fact, the implementation of Entity in Amethyst is simply:

struct Entity(u32, Generation);

where u32 is the id of the entity and generation is used to check if the entity has been deleted.

Entitys are stored in a special container EntitiesRes. Whereas the data associated with the entities are grouped into components and stored in the designated storages.

Consider an example where you have three objects: two bottles and a person.

objectxyshapecolorname
Bottle A150.0202.1"round""red"
Bottle B570.0122.0"square""white"
Person C100.5300.8"Peter"

We can separate bottle's properties into PositionComponent and BottleComponent, and person's properties into PositionComponent and PersonComponent. Here's an illustration of how the three objects would be stored.

How entity and components are stored

As you could see from the graph, entities do not store data. Nor do they know any information about their components. They serve the purpose of object identification and tracking object existence. The component storage stores all the data and their connection to entities.

If you are familiar with relational databases, this organization looks quite similar to the tables in a database, where entity id serves as the key in each table. In fact, you can even join components or entities like joining tables. For example, to update the position of all the persons, you will need to join the PersonComponent and the PositionComponent.

EntitiesRes

Even though the structure of the entity is pretty simple, entity manipulation is very sophisticated and crucial to game performance. This is why entities are handled exclusively by the struct EntitiesRes. EntitiesRes provides two ways for creating/deleting entities:

  • Immediate creation/deletion, used for game setup or clean up.
  • Lazy creation/deletion, used in the game play state. It updates entities in batch at the end of each game loop. This is also referred to as atomic creation/deletion.

You will see how these methods are used in later chapters.

Declaring a component

To declare a component, you first declare the relevant underlying data:

# extern crate amethyst;
# use amethyst::core::math::{Isometry3, Vector3};

/// This `Component` describes the shape of an `Entity`
enum Shape {
    Sphere { radius: f32 },
    Cuboid { height: f32, width: f32, depth: f32 },
}

/// This `Component` describes the transform of an `Entity`
pub struct Transform {
    /// Translation + rotation value
    iso: Isometry3<f32>,
    /// Scale vector
    scale: Vector3<f32>,
}

and then you implement the Component trait for them:

# extern crate amethyst;
# struct Shape;
# struct Transform;
use amethyst::ecs::{Component, DenseVecStorage, FlaggedStorage};

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

impl Component for Transform {
    type Storage = FlaggedStorage<Self, DenseVecStorage<Self>>;
}

The storage type will determine how you store the component, but it will not initialize the storage. Storage is initialized when you register a component in World or when you use that component in a System.

Storages

There are a few storage strategies for different usage scenarios. The most commonly used types are DenseVecStorage, VecStorage and FlaggedStorage.

  • DenseVecStorage: Elements are stored in a contiguous vector. No empty space is left between Components, allowing a lowered memory usage for big components.
  • VecStorage: Elements are stored into a sparse array. The entity id is the same as the index of component. If your component is small (<= 16 bytes) or is carried by most entities, this is preferable over DenseVecStorage.
  • FlaggedStorage: Used to keep track of changes of a component. Useful for caching purposes.

DenseVecStorage ( entity_id maps to data_id )

data
data_id
entity_id
data data data data ...
0 2 3 1 ...
0 1 5 9 ...

VecStorage ( entity_id = data index, can be empty )

data
data data empty data ...

For more information, see the specs storage reference and the "Storages" section of the specs book.

There are a bunch more storages, and deciding which one is the best isn't trivial and should be done based on careful benchmarking. A general rule is: if your component is used in over 30% of entities, use VecStorage. If you don't know which one you should use, DenseVecStorage is a good default. It will need more memory than VecStorage for pointer-sized components, but it will perform well for most scenarios.

Tags

Components can also be used to "tag" entities. The usual way to do it is to create an empty struct, and implement Component using NullStorage as the Storage type for it. Null storage means that it is not going to take memory space to store those components.

You will learn how to use those tag components in the System chapter.

Resource

What is a resource?

A resource is any type that stores data that you might need for your game AND that is not specific to an entity. For example, the score of a pong game is global to the whole game and isn't owned by any of the entities (paddle, ball and even the ui score text).

Creating a resource

Resources are stored in the World container.

Adding a resource to a World instance is done like this:

# extern crate amethyst;
use amethyst::ecs::World;

struct MyResource {
    pub game_score: i32,
}

fn main() {
    let mut world = World::empty();
    
    let my = MyResource {
        game_score: 0,
    };
    
    world.insert(my);
}

Fetching a resource (from World)

Fetching a resource can be done like this:

# extern crate amethyst;
# use amethyst::ecs::World;
# #[derive(Debug, PartialEq)]
# struct MyResource {
#   pub game_score: i32,
# }
# fn main() {
#   let mut world = World::empty();
#   let my = MyResource{
#     game_score: 0,
#   };
#   world.insert(my);
  // try_fetch returns a Option<Fetch<MyResource>>
  let fetched = world.try_fetch::<MyResource>();
  if let Some(fetched_resource) = fetched {
      //dereference Fetch<MyResource> to access data
      assert_eq!(*fetched_resource, MyResource{ game_score: 0, });
  } else {
      println!("No MyResource present in `World`");
  }
# }

If you want to get a resource and create it if it doesn't exist:

# extern crate amethyst;
# use amethyst::ecs::World;
# struct MyResource;
# fn main() {
#   let mut world = World::empty();
#   let my = MyResource;
  // If the resource isn't inside `World`, 
  // it will insert the instance we created earlier.
let fetched = world.entry::<MyResource>().or_insert_with(|| my);
# }

If you want to change a resource that is already inside of World:

# extern crate amethyst;
# use amethyst::ecs::World;
# struct MyResource {
#   pub game_score: i32,
# }
# fn main() {
#   let mut world = World::empty();
#   let my = MyResource{
#     game_score: 0,
#   };
#   world.insert(my);
  // try_fetch_mut returns a Option<FetchMut<MyResource>>
  let fetched = world.try_fetch_mut::<MyResource>();
  if let Some(mut fetched_resource) = fetched {
    assert_eq!(fetched_resource.game_score, 0);
    fetched_resource.game_score = 10;
    assert_eq!(fetched_resource.game_score, 10);
  } else {
    println!("No MyResource present in `World`");
  }
# }

Other ways of fetching a resource will be covered in the system section of the book.

Deleting a resource

There is no method to properly "delete" a resource added to the world. The usual method to achieve something similar is to add an Option<MyResource> and to set it to None when you want to delete it.

Storages, part 2

A Component's Storage is a resource. The components are "attached" to entities, but as said previously, they are not "owned" by the entities at the implementation level. By storing them into Storages and by having Storage be placed inside World, it allows global access to all of the components at runtime with minimal effort.

Actually accessing the components inside Storages will be covered in the world and system sections of the book.

WARNING: If you try to fetch the component directly, you will not get the storage. You will get a Default::default() instance of that component. To get the Storage resource that HOLDS all the MyComponent instances, you need to fetch ReadStorage<MyComponent>.

World

What is a World?

A World is a container for resources, with some helper functions that make your life easier. This chapter will showcase those functions and their usage.

Adding a resource

# extern crate amethyst;
use amethyst::ecs::{World, WorldExt};

// A simple struct with no data.
struct MyResource;

fn main() {
    // We create a new `World` instance.
    let mut world = World::new();
    
    // We create our resource.
    let my = MyResource;
    
    // We add the resource to the world.
    world.insert(my);
}

Fetching a resource

Here's how to fetch a read-only resource. Be aware that this method panics if the resource isn't inserted into Resources.

# extern crate amethyst;
# use amethyst::ecs::{World, WorldExt};
# struct MyResource;
# fn main() {
#   let mut world = World::new();
    let my = world.read_resource::<MyResource>();
# }

If you are not sure that the resource will be present, use the methods available on Resources, as shown in the resource chapter.

# extern crate amethyst;
# use amethyst::ecs::{World, WorldExt};
# struct MyResource;
# fn main() {
#   let mut world = World::new();
    let my = world.entry::<MyResource>().or_insert_with(|| MyResource);
# }

Modifying a resource

# extern crate amethyst;
# use amethyst::ecs::{World, WorldExt};
# struct MyResource;
# fn main() {
#   let mut world = World::new();
    let mut my = world.write_resource::<MyResource>();
# }

Creating entities

You first start by creating the entity builder. Then, you can add components to your entity. Finally, you call the build() method on the entity builder to get the actual entity. Please note that in order to use this syntax, you need to import the amethyst::prelude::Builder trait.

# extern crate amethyst;
# use amethyst::ecs::{World, WorldExt};
# struct MyComponent;
# impl amethyst::ecs::Component for MyComponent {
#   type Storage = amethyst::ecs::VecStorage<MyComponent>;
# }
# fn main() {
#   let mut world = World::new();
    world.register::<MyComponent>();
    use amethyst::prelude::Builder;

    let mut entity_builder = world.create_entity();
    entity_builder = entity_builder.with(MyComponent);
    let my_entity = entity_builder.build();
# }

Shorter version:

# extern crate amethyst;
# use amethyst::ecs::{World, WorldExt};
# struct MyComponent;
# impl amethyst::ecs::Component for MyComponent {
#   type Storage = amethyst::ecs::VecStorage<MyComponent>;
# }
# fn main() {
#   let mut world = World::new();
    use amethyst::prelude::Builder;

    let my_entity = world
       .create_entity()
       .with(MyComponent)
       .build();
# }

Internally, the World interacts with EntitiesRes, which is a resource holding the entities inside of Resources.

Accessing a Component

# extern crate amethyst;
# use amethyst::ecs::{Builder, World, WorldExt};
# struct MyComponent;
# impl amethyst::ecs::Component for MyComponent {
#   type Storage = amethyst::ecs::VecStorage<MyComponent>;
# }
# fn main() {
#   let mut world = World::new();
    // Create an `Entity` with `MyComponent`.
    // `World` will implicitly write to the component's storage in `Resources`.
    let my_entity = world.create_entity().with(MyComponent).build();
    
    // Get a ReadStorage<MyComponent>
    let storage = world.read_storage::<MyComponent>();
    
    // Get the actual component from the storage.
    let my = storage.get(my_entity).expect("Failed to get component for entity");
# }

Modifying a Component

This is almost the same as accessing a component:

# extern crate amethyst;
# use amethyst::ecs::{Builder, World, WorldExt};
# struct MyComponent;
# impl amethyst::ecs::Component for MyComponent {
#   type Storage = amethyst::ecs::VecStorage<MyComponent>;
# }
# fn main() {
#   let mut world = World::new();
    let my_entity = world.create_entity().with(MyComponent).build();
    let mut storage = world.write_storage::<MyComponent>();
    let mut my = storage.get_mut(my_entity).expect("Failed to get component for entity");
# }

Getting all entities

It is pretty rare to use this, but can be useful in some occasions.

# extern crate amethyst;
# use amethyst::ecs::{World, WorldExt};
# fn main() {
#   let mut world = World::new();
    // Returns `EntitiesRes`
    let entities = world.entities();
# }

Delete an entity

Single:

# extern crate amethyst;
# use amethyst::ecs::{Builder, World, WorldExt};
# fn main() {
#   let mut world = World::new();
#   let my_entity = world.create_entity().build();
    world.delete_entity(my_entity).expect("Failed to delete entity. Was it already removed?");
# }

Multiple:

# extern crate amethyst;
# use amethyst::ecs::{Builder, World, WorldExt};
# fn main() {
#   let mut world = World::new();
#   let entity_vec: Vec<amethyst::ecs::Entity> = vec![world.create_entity().build()];
    world.delete_entities(entity_vec.as_slice()).expect("Failed to delete entities from specified list.");
# }

All:

# extern crate amethyst;
# use amethyst::ecs::{World, WorldExt};
# fn main() {
#   let mut world = World::new();
    world.delete_all();
# }

Note: Entities are lazily deleted, which means that deletion only happens at the end of the frame and not immediately when calling the delete method.

Check if the entity was deleted

# extern crate amethyst;
# use amethyst::ecs::{Builder, World, WorldExt};
# fn main() {
#   let mut world = World::new();
#   let my_entity = world.create_entity().build();
    // Returns true if the entity was **not** deleted.
    let is_alive = world.is_alive(my_entity);
# }

Exec

This is just to show that this feature exists. It is normal to not understand what it does until you read the system chapter

Sometimes, you will want to create an entity where you need to fetch resources to create the correct components for it. There is a function that acts as a shorthand for this:

# extern crate amethyst;
# use amethyst::ecs::{ReadExpect, World, WorldExt};
# struct Dummy;
# type SomeSystemData<'a> = ReadExpect<'a, Dummy>;
# trait DoSomething {
#   fn do_something(&mut self);
# }
# impl<'a> DoSomething for SomeSystemData<'a> {
#   fn do_something(&mut self) { }
# }
# fn main() {
#   let mut world = World::new();
    world.exec(|mut data: SomeSystemData| {
        data.do_something();
    });
# }

We will talk about what SystemData is in the system chapter.

System

What is a System?

A system is where the logic of the game is executed. In practice, it consists of a struct implementing a function executed on every iteration of the game loop, and taking as an argument data about the game.

Systems can be seen as a small unit of logic. All systems are run by the engine together (even in parallel when possible), and do a specialized operation on one or a group of entities.

Structure

A system struct is a structure implementing the trait amethyst::ecs::System.

Here is a very simple example implementation:

# extern crate amethyst;
# use amethyst::ecs::System;
struct MyFirstSystem;

impl<'a> System<'a> for MyFirstSystem {
    type SystemData = ();

    fn run(&mut self, data: Self::SystemData) {
        println!("Hello!");
    }
}

This system will, on every iteration of the game loop, print "Hello!" in the console. This is a pretty boring system as it does not interact at all with the game. Let us spice it up a bit.

Accessing the context of the game

In the definition of a system, the trait requires you to define a type SystemData. This type defines what data the system will be provided with on each call of its run method. SystemData is only meant to carry information accessible to multiple systems. Data local to a system is usually stored in the system's struct itself instead.

The Amethyst engine provides useful system data types to use in order to access the context of a game. Here are some of the most important ones:

  • Read<'a, Resource> (respectively Write<'a, Resource>) allows you to obtain an immutable (respectively mutable) reference to a resource of the type you specify. This is guaranteed to not fail as if the resource is not available, it will give you the Default::default() of your resource.
  • ReadExpect<'a, Resource> (respectively WriteExpect<'a, Resource>) is a failable alternative to the previous system data, so that you can use resources that do not implement the Default trait.
  • ReadStorage<'a, Component> (respectively WriteStorage<'a, Component>) allows you to obtain an immutable (respectively mutable) reference to the entire storage of a certain Component type.
  • Entities<'a> allows you to create or destroy entities in the context of a system.

You can then use one, or multiple of them via a tuple.

# extern crate amethyst;
# use amethyst::ecs::{System, Read};
# use amethyst::core::timing::Time;
struct MyFirstSystem;

impl<'a> System<'a> for MyFirstSystem {
    type SystemData = Read<'a, Time>;

    fn run(&mut self, data: Self::SystemData) {
        println!("{}", data.delta_seconds());
    }
}

Here, we get the amethyst::core::timing::Time resource to print in the console the time elapsed between two frames. Nice! But that's still a bit boring.

Manipulating storages

Once you have access to a storage, you can use them in different ways.

Getting a component of a specific entity

Sometimes, it can be useful to get a component in the storage for a specific entity. This can easily be done using the get or, for mutable storages, get_mut methods.

# extern crate amethyst;
# use amethyst::ecs::{Entity, System, WriteStorage};
# use amethyst::core::Transform;
struct WalkPlayerUp {
    player: Entity,
}

impl<'a> System<'a> for WalkPlayerUp {
    type SystemData = WriteStorage<'a, Transform>;

    fn run(&mut self, mut transforms: Self::SystemData) {
        transforms.get_mut(self.player).unwrap().prepend_translation_y(0.1);
    }
}

This system makes the player go up by 0.1 unit every iteration of the game loop! To identify what entity the player is, we stored it beforehand in the system's struct. Then, we get its Transform from the transform storage, and move it along the Y axis by 0.1.

A transform is a very common structure in game development. It represents the position, rotation and scale of an object in the game world. You will use them a lot, as they are what you need to change when you want to move something around in your game.

However, this approach is pretty rare because most of the time you don't know what entity you want to manipulate, and in fact you may want to apply your changes to multiple entities.

Getting all entities with specific components

Most of the time, you will want to perform logic on all entities with a specific component, or even all entities with a selection of components.

This is possible using the join method. You may be familiar with joining operations if you have ever worked with databases. The join method takes multiple storages, and iterates over all entities that have a component in each of those storages. It works like an "AND" gate. It will return an iterator containing a tuple of all the requested components if they are ALL on the same entity.

If you join with components A, B and C, only the entities that have ALL those components will be considered.

Needless to say that you can use it with only one storage to iterate over all entities with a specific component.

Keep in mind that the join method is only available by importing amethyst::ecs::Join.

# extern crate amethyst;
# use amethyst::ecs::{System, ReadStorage, WriteStorage};
# use amethyst::core::Transform;
# struct FallingObject;
# impl amethyst::ecs::Component for FallingObject {
#   type Storage = amethyst::ecs::DenseVecStorage<FallingObject>;
# }
use amethyst::ecs::Join;

struct MakeObjectsFall;

impl<'a> System<'a> for MakeObjectsFall {
    type SystemData = (
        WriteStorage<'a, Transform>,
        ReadStorage<'a, FallingObject>,
    );

    fn run(&mut self, (mut transforms, falling): Self::SystemData) {
        for (mut transform, _) in (&mut transforms, &falling).join() {
            if transform.translation().y > 0.0 {
                transform.prepend_translation_y(-0.1);
            }
        }
    }
}

This system will make all entities with both a Transform with a positive y coordinate and a FallingObject tag component fall by 0.1 unit per game loop iteration. Note that as the FallingObject is only here as a tag to restrict the joining operation, we immediately discard it using the _ syntax.

Cool! Now that looks like something we'll actually do in our games!

Getting entities that have some components, but not others

There is a special type of Storage in specs called AntiStorage. The not operator (!) turns a Storage into its AntiStorage counterpart, allowing you to iterate over entities that do NOT have this Component. It is used like this:

# extern crate amethyst;
# use amethyst::ecs::{System, ReadStorage, WriteStorage};
# use amethyst::core::Transform;
# struct FallingObject;
# impl amethyst::ecs::Component for FallingObject {
#   type Storage = amethyst::ecs::DenseVecStorage<FallingObject>;
# }
use amethyst::ecs::Join;

struct NotFallingObjects;

impl<'a> System<'a> for NotFallingObjects {
    type SystemData = (
        WriteStorage<'a, Transform>,
        ReadStorage<'a, FallingObject>,
    );

    fn run(&mut self, (mut transforms, falling): Self::SystemData) {
        for (mut transform, _) in (&mut transforms, !&falling).join() {
            // If they don't fall, why not make them go up!
            transform.prepend_translation_y(0.1);
        }
    }
}

Manipulating the structure of entities

It may sometimes be interesting to manipulate the structure of entities in a system, such as creating new ones or modifying the component layout of existing ones. This kind of process is done using the Entities<'a> system data.

Requesting Entities<'a> does not impact performance, as it contains only immutable resources and therefore does not block the dispatching.

Creating new entities in a system

Creating an entity while in the context of a system is very similar to the way one would create an entity using the World struct. The only difference is that one needs to provide mutable storages of all the components they plan to add to the entity.

# extern crate amethyst;
# use amethyst::ecs::{System, WriteStorage, Entities};
# use amethyst::core::Transform;
# struct Enemy;
# impl amethyst::ecs::Component for Enemy {
#   type Storage = amethyst::ecs::VecStorage<Enemy>;
# }
struct SpawnEnemies {
    counter: u32,
}

impl<'a> System<'a> for SpawnEnemies {
    type SystemData = (
        WriteStorage<'a, Transform>,
        WriteStorage<'a, Enemy>,
        Entities<'a>,
    );

    fn run(&mut self, (mut transforms, mut enemies, entities): Self::SystemData) {
        self.counter += 1;
        if self.counter > 200 {
            entities.build_entity()
                .with(Transform::default(), &mut transforms)
                .with(Enemy, &mut enemies)
                .build();
            self.counter = 0;
        }
    }
}

This system will spawn a new enemy every 200 game loop iterations.

Removing an entity

Deleting an entity is very easy using Entities<'a>.

# extern crate amethyst;
# use amethyst::ecs::{System, Entities, Entity};
# struct MySystem { entity: Entity }
# impl<'a> System<'a> for MySystem {
#   type SystemData = Entities<'a>;
#   fn run(&mut self, entities: Self::SystemData) {
#       let entity = self.entity;
entities.delete(entity);
#   }
# }

Iterating over components with associated entity

Sometimes, when you iterate over components, you may want to also know what entity you are working with. To do that, you can use the joining operation with Entities<'a>.

# extern crate amethyst;
# use amethyst::ecs::{Join, System, Entities, WriteStorage, ReadStorage};
# use amethyst::core::Transform;
# struct FallingObject;
# impl amethyst::ecs::Component for FallingObject {
#   type Storage = amethyst::ecs::VecStorage<FallingObject>;
# }
struct MakeObjectsFall;

impl<'a> System<'a> for MakeObjectsFall {
    type SystemData = (
        Entities<'a>,
        WriteStorage<'a, Transform>,
        ReadStorage<'a, FallingObject>,
    );

    fn run(&mut self, (entities, mut transforms, falling): Self::SystemData) {
        for (e, mut transform, _) in (&*entities, &mut transforms, &falling).join() {
            if transform.translation().y > 0.0 {
                transform.prepend_translation_y(-0.1);
            } else {
                entities.delete(e);
            }
        }
    }
}

This system does the same thing as the previous MakeObjectsFall, but also cleans up falling objects that reached the ground.

Adding or removing components

You can also insert or remove components from a specific entity. To do that, you need to get a mutable storage of the component you want to modify, and simply do:

# extern crate amethyst;
# use amethyst::ecs::{System, Entities, Entity, WriteStorage};
# struct MyComponent;
# impl amethyst::ecs::Component for MyComponent {
#   type Storage = amethyst::ecs::VecStorage<MyComponent>;
# }
# struct MySystem { entity: Entity }
# impl<'a> System<'a> for MySystem {
#   type SystemData = WriteStorage<'a, MyComponent>;
#   fn run(&mut self, mut write_storage: Self::SystemData) {
#       let entity = self.entity;
// Add the component
write_storage.insert(entity, MyComponent);

// Remove the component
write_storage.remove(entity);
#   }
# }

Keep in mind that inserting a component on an entity that already has a component of the same type will overwrite the previous one.

Changing states through resources

In a previous section we talked about States, and how they are used to organize your game into different logical sections. Sometimes we want to trigger a state transition from a system. For example, if a player dies we might want to remove their entity and signal to the state machine to push a state that shows a "You Died" screen.

So how can we affect states from systems? There are a couple of ways, but this section will detail the easiest one: using a Resource.

Before that, let's just quickly remind ourselves what a resource is:

A Resource is any type that stores data that you might need for your game AND that is not specific to an entity.

The data in a resource is available both to systems and states. We can use this to our advantage!

Let's say you have the following two states:

  • GameplayState: State in which the game is running.
  • GameMenuState: State where the game is paused and we interact with a game menu.

The following example shows how to keep track of which state we are currently in. This allows us to do a bit of conditional logic in our systems to determine what to do depending on which state is currently active, and manipulating the states by tracking user actions:

# extern crate amethyst;
use amethyst::prelude::*;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CurrentState {
    MainMenu,
    Gameplay,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum UserAction {
    OpenMenu,
    ResumeGame,
    Quit,
}

impl Default for CurrentState {
    fn default() -> Self {
        CurrentState::Gameplay
    }
}

struct Game {
    user_action: Option<UserAction>,
    current_state: CurrentState,
}

impl Default for Game {
    fn default() -> Self {
        Game {
            user_action: None,
            current_state: CurrentState::default(),
        }
    }
}

struct GameplayState;

impl SimpleState for GameplayState {
    fn update(&mut self, data: &mut StateData<'_, GameData<'_, '_>>) -> SimpleTrans {
        // If the `Game` resource has been set up to go back to the menu, pop
        // the state so that we go back.

        let mut game = data.world.write_resource::<Game>();

        if let Some(UserAction::OpenMenu) = game.user_action.take() {
            return Trans::Push(Box::new(GameMenuState));
        }

        Trans::None
    }

    fn on_resume(&mut self, mut data: StateData<'_, GameData<'_, '_>>) {
        // mark that the current state is a gameplay state.
        data.world.write_resource::<Game>().current_state = CurrentState::Gameplay;
    }
}

struct GameMenuState;

impl SimpleState for GameMenuState {
    fn update(&mut self, data: &mut StateData<'_, GameData<'_, '_>>) -> SimpleTrans {
        let mut game = data.world.write_resource::<Game>();

        match game.user_action.take() {
            Some(UserAction::ResumeGame) => Trans::Pop,
            Some(UserAction::Quit) => {
                // Note: no need to clean up :)
                Trans::Quit
            },
            _ => Trans::None,
        }
    }

    fn on_resume(&mut self, mut data: StateData<'_, GameData<'_, '_>>) {
        // mark that the current state is a main menu state.
        data.world.write_resource::<Game>().current_state = CurrentState::MainMenu;
    }
}

Let's say we want the player to be able to press escape to enter the menu. We modify our input handler to map the open_menu action to Esc, and we write the following system:

# extern crate amethyst;
#
# #[derive(Clone, Copy, Debug, PartialEq, Eq)]
# enum CurrentState {
#     MainMenu,
#     Gameplay,
# }
#
# impl Default for CurrentState { fn default() -> Self { CurrentState::Gameplay } }
#
# #[derive(Clone, Copy, Debug, PartialEq, Eq)]
# enum UserAction {
#     OpenMenu,
#     ResumeGame,
#     Quit,
# }
#
# struct Game {
#     user_action: Option<UserAction>,
#     current_state: CurrentState,
# }
#
# impl Default for Game {
#     fn default() -> Self {
#         Game {
#             user_action: None,
#             current_state: CurrentState::default(),
#         }
#     }
# }
#
use amethyst::{
    prelude::*,
    ecs::{System, prelude::*},
    input::{InputHandler, StringBindings},
};

struct MyGameplaySystem;

impl<'s> System<'s> for MyGameplaySystem {
    type SystemData = (
        Read<'s, InputHandler<StringBindings>>,
        Write<'s, Game>,
    );

    fn run(&mut self, (input, mut game): Self::SystemData) {
        match game.current_state {
            CurrentState::Gameplay => {
                let open_menu = input
                    .action_is_down("open_menu")
                    .unwrap_or(false);

                // Toggle the `open_menu` variable to signal the state to
                // transition.
                if open_menu {
                    game.user_action = Some(UserAction::OpenMenu);
                }
            }
            // do nothing for other states.
            _ => {}
        }
    }
}

Now whenever you are playing the game and you press the button associated with the open_menu action, the GameMenuState will resume and the GameplayState will pause.

The SystemData trait

While this is rarely useful, it is possible to create custom SystemData types.

The Dispatcher populates the SystemData on every call of the run method. To do that, your SystemData type must implement the trait amethyst::ecs::SystemData in order to have it be valid.

This is rather complicated trait to implement, fortunately Amethyst provides a derive macro for it, that can implement the trait to any struct as long as all its fields are SystemData. Most of the time however, you will not even need to implement it at all as you will be using SystemData structs provided by the engine.

Please note that tuples of structs implementing SystemData are themselves SystemData. This is very useful when you need to request multiple SystemData at once quickly.

# extern crate amethyst;
# extern crate shred;
# #[macro_use] extern crate shred_derive;
#
# use amethyst::{
#     ecs::{Component, Join, ReadStorage, System, SystemData, VecStorage, World, WriteStorage},
#     shred::ResourceId,
# };
#
# struct FooComponent {
#   stuff: f32,
# }
# impl Component for FooComponent {
#   type Storage = VecStorage<FooComponent>;
# }
#
# struct BarComponent {
#   stuff: f32,
# }
# impl Component for BarComponent {
#   type Storage = VecStorage<BarComponent>;
# }
#
# #[derive(SystemData)]
# struct BazSystemData<'a> {
#  field: ReadStorage<'a, FooComponent>,
# }
#
# impl<'a> BazSystemData<'a> {
#   fn should_process(&self) -> bool {
#       true
#   }
# }
#
#[derive(SystemData)]
struct MySystemData<'a> {
    foo: ReadStorage<'a, FooComponent>,
    bar: WriteStorage<'a, BarComponent>,
    baz: BazSystemData<'a>,
}

struct MyFirstSystem;

impl<'a> System<'a> for MyFirstSystem {
    type SystemData = MySystemData<'a>;

    fn run(&mut self, mut data: Self::SystemData) {
        if data.baz.should_process() {
            for (foo, mut bar) in (&data.foo, &mut data.bar).join() {
                bar.stuff += foo.stuff;
            }
        }
    }
}

System Initialization

Systems may need to access resources from the World in order to be instantiated. For example, obtaining a ReaderId to an EventChannel that exists in the World. When there is an existing event channel in the World, a System should register itself as a reader of that channel instead of replacing it, as that invalidates all other readers.

In Amethyst, the World that the application begins with is populated with a number of default resources -- event channels, a thread pool, a frame limiter, and so on.

Given the default resources begin with special limits, we need a way to pass the System initialization logic through to the application, including parameters to the System's constructor. This is information the SystemDesc trait captures.

For each System, an implementation of the SystemDesc trait specifies the logic to instantiate the System. For Systems that do not require special initialization logic, the SystemDesc derive automatically implements the SystemDesc trait on the system type itself:

# extern crate amethyst;
use amethyst::{
    core::SystemDesc,
    derive::SystemDesc,
    ecs::{System, SystemData, World},
};

#[derive(SystemDesc)]
struct SystemName;

impl<'a> System<'a> for SystemName {
    type SystemData = ();

    fn run(&mut self, data: Self::SystemData) {
        println!("Hello!");
    }
}

The SystemDesc derive page demonstrates the use cases supported by the SystemDesc derive. For more complex cases, the Implementing the SystemDesc Trait page explains how to implement the SystemDesc trait.

SystemDesc Derive

The SystemDesc derive supports the following cases when generating a SystemDesc trait implementation:

  • Parameters to pass to the system constructor.
  • Fields to skip -- defaulted by the system constructor.
  • Registering a ReaderId for an EventChannel<_> in the World.
  • Registering a ReaderId to a component's FlaggedStorage.
  • Inserting a resource into the World.

If your system initialization use case is not covered, please see the Implementing the SystemDesc Trait page.

In each of the following examples, make sure you have the following imports:

# extern crate amethyst;
#
use amethyst::{
    core::SystemDesc,
    derive::SystemDesc,
    ecs::{System, SystemData, World},
};

Passing parameters to system constructor

# extern crate amethyst;
#
# use amethyst::{
#     core::SystemDesc,
#     derive::SystemDesc,
#     ecs::{System, SystemData, World},
# };
#
#[derive(SystemDesc)]
#[system_desc(name(SystemNameDesc))]
pub struct SystemName {
    field_0: u32,
    field_1: String,
}

impl SystemName {
    fn new(field_0: u32, field_1: String) -> Self {
        SystemName { field_0, field_1 }
    }
}
#
# impl<'a> System<'a> for SystemName {
#     type SystemData = ();
#     fn run(&mut self, data: Self::SystemData) {}
# }
Generated code
# extern crate amethyst;
#
# use amethyst::{
#     core::SystemDesc,
#     derive::SystemDesc,
#     ecs::{System, SystemData, World},
# };
#
# pub struct SystemName {
#     field_0: u32,
#     field_1: String,
# }
#
# impl SystemName {
#     fn new(field_0: u32, field_1: String) -> Self {
#         SystemName { field_0, field_1 }
#     }
# }
#
# impl<'a> System<'a> for SystemName {
#     type SystemData = ();
#     fn run(&mut self, data: Self::SystemData) {}
# }
#
/// Builds a `SystemName`.
#[derive(Default, Debug)]
pub struct SystemNameDesc {
    field_0: u32,
    field_1: String,
}

impl SystemNameDesc {
    fn new(field_0: u32, field_1: String) -> Self {
        SystemNameDesc { field_0, field_1 }
    }
}

impl<'a, 'b> SystemDesc<'a, 'b, SystemName> for SystemNameDesc {
    fn build(self, world: &mut World) -> SystemName {
        <SystemName as System<'_>>::SystemData::setup(world);

        SystemName::new(self.field_0, self.field_1)
    }
}

Fields to skip -- defaulted by the system constructor

# extern crate amethyst;
#
# use amethyst::{
#     core::SystemDesc,
#     derive::SystemDesc,
#     ecs::{System, SystemData, World},
# };
#
#[derive(SystemDesc)]
#[system_desc(name(SystemNameDesc))]
pub struct SystemName {
    #[system_desc(skip)]
    field_0: u32,
    field_1: String,
}

impl SystemName {
    fn new(field_1: String) -> Self {
        SystemName { field_0: 123, field_1 }
    }
}
#
# impl<'a> System<'a> for SystemName {
#     type SystemData = ();
#     fn run(&mut self, data: Self::SystemData) {}
# }
Generated code
# extern crate amethyst;
#
# use amethyst::{
#     core::SystemDesc,
#     derive::SystemDesc,
#     ecs::{System, SystemData, World},
# };
#
# pub struct SystemName {
#     field_0: u32,
#     field_1: String,
# }
#
# impl SystemName {
#     fn new(field_1: String) -> Self {
#         SystemName { field_0: 123, field_1 }
#     }
# }
#
# impl<'a> System<'a> for SystemName {
#     type SystemData = ();
#     fn run(&mut self, data: Self::SystemData) {}
# }
#
/// Builds a `SystemName`.
#[derive(Default, Debug)]
pub struct SystemNameDesc {
    field_1: String,
}

impl SystemNameDesc {
    fn new(field_1: String) -> Self {
        SystemNameDesc { field_1 }
    }
}

impl<'a, 'b> SystemDesc<'a, 'b, SystemName> for SystemNameDesc {
    fn build(self, world: &mut World) -> SystemName {
        <SystemName as System<'_>>::SystemData::setup(world);

        SystemName::new(self.field_1)
    }
}

Note: If there are no field parameters, the SystemDesc implementation will call SystemName::default():

# extern crate amethyst;
#
# use amethyst::{
#     core::SystemDesc,
#     derive::SystemDesc,
#     ecs::{System, SystemData, World},
# };
#
#[derive(Default, SystemDesc)]
#[system_desc(name(SystemNameDesc))]
pub struct SystemName {
    #[system_desc(skip)]
    field_0: u32,
}
#
# impl<'a> System<'a> for SystemName {
#     type SystemData = ();
#     fn run(&mut self, data: Self::SystemData) {}
# }
Generated code
# extern crate amethyst;
#
# use amethyst::{
#     core::SystemDesc,
#     derive::SystemDesc,
#     ecs::{System, SystemData, World},
# };
#
# #[derive(Default)]
# pub struct SystemName {
#     field_0: u32,
# }
#
# impl<'a> System<'a> for SystemName {
#     type SystemData = ();
#     fn run(&mut self, data: Self::SystemData) {}
# }
#
/// Builds a `SystemName`.
#[derive(Debug)]
pub struct SystemNameDesc {}

impl Default for SystemNameDesc {
    fn default() -> Self {
        SystemNameDesc {}
    }
}

impl<'a, 'b> SystemDesc<'a, 'b, SystemName> for SystemNameDesc {
    fn build(self, world: &mut World) -> SystemName {
        <SystemName as System<'_>>::SystemData::setup(world);

        SystemName::default()
    }
}

Registering a ReaderId for an EventChannel<_> in the World

# extern crate amethyst;
#
# use amethyst::{
#     core::SystemDesc,
#     derive::SystemDesc,
#     ecs::{System, SystemData, World},
#     shrev::{EventChannel, ReaderId},
#     ui::UiEvent,
# };
#
#[derive(SystemDesc)]
#[system_desc(name(SystemNameDesc))]
pub struct SystemName {
    #[system_desc(event_channel_reader)]
    reader_id: ReaderId<UiEvent>,
}

impl SystemName {
    fn new(reader_id: ReaderId<UiEvent>) -> Self {
        SystemName { reader_id }
    }
}
#
# impl<'a> System<'a> for SystemName {
#     type SystemData = ();
#     fn run(&mut self, data: Self::SystemData) {}
# }
Generated code
# extern crate amethyst;
#
# use amethyst::{
#     core::SystemDesc,
#     derive::SystemDesc,
#     ecs::{System, SystemData, World},
#     shrev::{EventChannel, ReaderId},
#     ui::UiEvent,
# };
#
# pub struct SystemName {
#     reader_id: ReaderId<UiEvent>,
# }
#
# impl SystemName {
#     fn new(reader_id: ReaderId<UiEvent>) -> Self {
#         SystemName { reader_id }
#     }
# }
#
# impl<'a> System<'a> for SystemName {
#     type SystemData = ();
#     fn run(&mut self, data: Self::SystemData) {}
# }
#
/// Builds a `SystemName`.
#[derive(Debug)]
pub struct SystemNameDesc;

impl Default for SystemNameDesc {
    fn default() -> Self {
        SystemNameDesc {}
    }
}

impl<'a, 'b> SystemDesc<'a, 'b, SystemName> for SystemNameDesc {
    fn build(self, world: &mut World) -> SystemName {
        <SystemName as System<'_>>::SystemData::setup(world);

        let reader_id = world
            .fetch_mut::<EventChannel<UiEvent>>()
            .register_reader();

        SystemName::new(reader_id)
    }
}

Registering a ReaderId to a component's FlaggedStorage

# extern crate amethyst;
#
# use amethyst::{
#     core::SystemDesc,
#     derive::SystemDesc,
#     ecs::{storage::ComponentEvent, System, SystemData, World, WriteStorage},
#     shrev::{EventChannel, ReaderId},
#     ui::UiResize,
# };
#
#[derive(SystemDesc)]
#[system_desc(name(SystemNameDesc))]
pub struct SystemName {
    #[system_desc(flagged_storage_reader(UiResize))]
    resize_events_id: ReaderId<ComponentEvent>,
}

impl SystemName {
    fn new(resize_events_id: ReaderId<ComponentEvent>) -> Self {
        SystemName { resize_events_id }
    }
}
#
# impl<'a> System<'a> for SystemName {
#     type SystemData = ();
#     fn run(&mut self, data: Self::SystemData) {}
# }
Generated code
# extern crate amethyst;
#
# use amethyst::{
#     core::SystemDesc,
#     derive::SystemDesc,
#     ecs::{storage::ComponentEvent, System, SystemData, World, WriteStorage},
#     shrev::{EventChannel, ReaderId},
#     ui::UiResize,
# };
#
# pub struct SystemName {
#     resize_events_id: ReaderId<ComponentEvent>,
# }
#
# impl SystemName {
#     fn new(resize_events_id: ReaderId<ComponentEvent>) -> Self {
#         SystemName { resize_events_id }
#     }
# }
#
# impl<'a> System<'a> for SystemName {
#     type SystemData = ();
#     fn run(&mut self, data: Self::SystemData) {}
# }
#
/// Builds a `SystemName`.
#[derive(Debug)]
pub struct SystemNameDesc;

impl Default for SystemNameDesc {
    fn default() -> Self {
        SystemNameDesc {}
    }
}

impl<'a, 'b> SystemDesc<'a, 'b, SystemName> for SystemNameDesc {
    fn build(self, world: &mut World) -> SystemName {
        <SystemName as System<'_>>::SystemData::setup(world);

        let resize_events_id = WriteStorage::<UiResize>::fetch(&world)
                            .register_reader();

        SystemName::new(resize_events_id)
    }
}

Inserting a resource into the World

# extern crate amethyst;
#
# use amethyst::{
#     core::SystemDesc,
#     derive::SystemDesc,
#     ecs::{ReadExpect, System, SystemData, World},
# };
#
pub struct NonDefault;

#[derive(Default, SystemDesc)]
#[system_desc(insert(NonDefault))]
pub struct SystemName;

impl<'a> System<'a> for SystemName {
    type SystemData = ReadExpect<'a, NonDefault>;
    fn run(&mut self, data: Self::SystemData) {}
}
Generated code
# extern crate amethyst;
#
# use amethyst::{
#     core::SystemDesc,
#     derive::SystemDesc,
#     ecs::{ReadExpect, System, SystemData, World},
# };
#
# pub struct NonDefault;
#
# #[derive(Default)]
# pub struct SystemName;
#
# impl<'a> System<'a> for SystemName {
#     type SystemData = ReadExpect<'a, NonDefault>;
#     fn run(&mut self, data: Self::SystemData) {}
# }
#
/// Builds a `SystemName`.
#[derive(Debug)]
pub struct SystemNameDesc;

impl Default for SystemNameDesc {
    fn default() -> Self {
        SystemNameDesc {}
    }
}

impl<'a, 'b> SystemDesc<'a, 'b, SystemName> for SystemNameDesc {
    fn build(self, world: &mut World) -> SystemName {
        <SystemName as System<'_>>::SystemData::setup(world);

        world.insert(NonDefault);

        SystemName::default()
    }
}

Implementing the SystemDesc Trait

If the SystemDesc derive is unable to generate a SystemDesc trait implementation for system initialization, the SystemDesc trait can be implemented manually:

# extern crate amethyst;
#
use amethyst::{
    audio::output::Output,
    core::SystemDesc,
    ecs::{System, SystemData, World},
};

# /// Syncs 3D transform data with the audio engine to provide 3D audio.
# #[derive(Debug, Default)]
# pub struct AudioSystem(Output);
# impl<'a> System<'a> for AudioSystem {
#     type SystemData = ();
#     fn run(&mut self, _: Self::SystemData) {}
# }
#
/// Builds an `AudioSystem`.
#[derive(Default, Debug)]
pub struct AudioSystemDesc {
    /// Audio `Output`.
    pub output: Output,
}

impl<'a, 'b> SystemDesc<'a, 'b, AudioSystem> for AudioSystemDesc {
    fn build(self, world: &mut World) -> AudioSystem {
        <AudioSystem as System<'_>>::SystemData::setup(world);

        world.insert(self.output.clone());

        AudioSystem(self.output)
    }
}

// in `main.rs`:
// let game_data = GameDataBuilder::default()
//     .with_system_desc(AudioSystemDesc::default(), "", &[]);

Templates

use amethyst_core::SystemDesc;

/// Builds a `SystemName`.
#[derive(Default, Debug)]
pub struct SystemNameDesc;

impl<'a, 'b> SystemDesc<'a, 'b, SystemName> for SystemNameDesc {
    fn build(self, world: &mut World) -> SystemName {
        <SystemName as System<'_>>::SystemData::setup(world);

        let arg = unimplemented!("Replace code here");

        SystemName::new(arg)
    }
}

With type parameters:

use std::marker::PhantomData;

use derivative::Derivative;

use amethyst_core::ecs::SystemData;
use amethyst_core::SystemDesc;

/// Builds a `SystemName`.
#[derive(Derivative, Debug)]
#[derivative(Default(bound = ""))]
pub struct SystemNameDesc<T> {
    marker: PhantomData<T>,
}

impl<'a, 'b, T> SystemDesc<'a, 'b, SystemName<T>>
    for SystemNameDesc<T>
where
    T: unimplemented!("Replace me."),
{
    fn build(self, world: &mut World) -> SystemName<T> {
        <SystemName<T> as System<'_>>::SystemData::setup(world);

        let arg = unimplemented!("Replace code here");

        SystemName::new(arg)
    }
}

Dispatcher

What is a Dispatcher?

Dispatchers are the heart of the ECS infrastructure. They are the executors that decide when the Systems will be executed so that they don't walk over each other.

When a dispatcher is created, it is associated with the systems that it will execute. It then generates an execution plan that respects mutability rules while maximizing parallelism.

Respecting mutability rules

When a system wants to access a Storage or a resource, they can do so either mutably or immutably. This works just like in Rust: either only one system can request something mutably and no other system can access it, or multiple systems can request something but only immutably.

The dispatcher looks at all the SystemData in the systems and builds execution stages.

If you want to have the best performance possible, you should prefer immutable over mutable whenever it is possible. (Read instead of Write, ReadStorage instead of WriteStorage).

Note: Please however keep in mind that Write is still preferable to locks in most cases, such as Mutex or RwLock for example.

Event Channel

This chapter will be easier than the previous ones.

While it is not essential to understand it to use amethyst, it can make your life much much easier in a lot of situations where using only data would make your code too complex.

What is an event channel?

An EventChannel acts like a queue for any type that is Send + Sync + 'static.

It is a single producer/multiple receiver queue. This means that it works best when used with only a single "thing" (usually a system) producing events. In most cases, the EventChannel should be stored in a global resource for ease of access. More on this later.

Creating an event channel

Super simple!

# extern crate amethyst;
# use amethyst::shrev::EventChannel;
    // In the following examples, we are going to use `MyEvent` values as events.
    #[derive(Debug)]
    pub enum MyEvent {
        A,
        B,
    }
    
    let mut channel = EventChannel::<MyEvent>::new();

Writing events to the event channel

Single:

# extern crate amethyst;
# #[derive(Debug)]
# pub enum MyEvent {
#   A,
#   B,
# }
# fn main() {
#   let mut channel = amethyst::shrev::EventChannel::<MyEvent>::new();
    channel.single_write(MyEvent::A);
# }

Multiple:

# extern crate amethyst;
# #[derive(Debug)]
# pub enum MyEvent {
#   A,
#   B,
# }
# fn main() {
#   let mut channel = amethyst::shrev::EventChannel::<MyEvent>::new();
    channel.iter_write(vec![MyEvent::A, MyEvent::A, MyEvent::B].into_iter());
# }

Reading events

This is the part where it becomes tricky. To be able to track where each of the receiver's reading is at, the EventChannel needs to be aware of their presence. This is done by registering a ReaderId.

# extern crate amethyst;
# #[derive(Debug)]
# pub enum MyEvent {
#   A,
#   B,
# }
# fn main() {
#   let mut channel = amethyst::shrev::EventChannel::<MyEvent>::new();
    let mut reader = channel.register_reader();
# }

Then, when you want to read the events:

# extern crate amethyst;
# #[derive(Debug)]
# pub enum MyEvent {
#   A,
#   B,
# }
# fn main() {
#   let mut channel = amethyst::shrev::EventChannel::<MyEvent>::new();
#   let mut reader = channel.register_reader();
    for event in channel.read(&mut reader) {
        // The type of the event is inferred from the generic type
        // we assigned to the `EventChannel<MyEvent>` earlier when creating it.
        println!("Received event value of: {:?}", event);
    }
# }

Note that you only need to have a read access to the channel when reading events. It is the ReaderId that needs to be mutable to keep track of where your last read was.

IMPORTANT: The event channel automatically grows as events are added to it and only decreases in size once all readers have read through the older events. This mean that if you create a ReaderId but don't read from it on each frame, the event channel will start to consume more and more memory.

Patterns

When using the event channel, we usually re-use the same pattern over and over again to maximize parallelism. It goes as follow:

Create the event channel and add it to to the world during State creation:

# extern crate amethyst;
# use amethyst::{ecs::{World, WorldExt}, shrev::EventChannel};
# #[derive(Debug)]
# pub enum MyEvent {
#   A,
#   B,
# }
# fn main() {
#   let mut world = World::new();
world.insert(
    EventChannel::<MyEvent>::new(),
);
# }

Note: You can also derive Default, this way you don't have to manually create your resource and add it. Resources implementing Default are automatically added to Resources when a System uses them (Read or Write in SystemData).

In the producer System, get a mutable reference to your resource:

# extern crate amethyst;
# use amethyst::ecs::Write;
# use amethyst::shrev::EventChannel;
# #[derive(Debug)]
# pub enum MyEvent {
#   A,
#   B,
# }
# struct MySystem;
# impl<'a> amethyst::ecs::System<'a> for MySystem {
type SystemData = Write<'a, EventChannel<MyEvent>>;
#   fn run(&mut self, _: Self::SystemData) { }
# }

In the receiver Systems, you need to store the ReaderId somewhere.

# extern crate amethyst;
# use amethyst::shrev::ReaderId;
# #[derive(Debug)]
# pub enum MyEvent {
#   A,
#   B,
# }
struct ReceiverSystem {
    // The type inside of ReaderId should be the type of the event you are using.
    reader: Option<ReaderId<MyEvent>>,
}

and you also need to get read access:

# extern crate amethyst;
# use amethyst::ecs::Read;
# use amethyst::shrev::EventChannel;
# #[derive(Debug)]
# pub enum MyEvent {
#   A,
#   B,
# }
# struct MySystem;
# impl<'a> amethyst::ecs::System<'a> for MySystem {
    type SystemData = Read<'a, EventChannel<MyEvent>>;
#   fn run(&mut self, _: Self::SystemData) { }
# }

Then, in the System's new method:

# extern crate amethyst;
# use amethyst::shrev::{EventChannel, ReaderId};
# use amethyst::ecs::{System, SystemData, World};
# #[derive(Debug)]
# pub enum MyEvent {
#   A,
#   B,
# }
# struct MySystem { reader: ReaderId<MyEvent>, }
#
impl MySystem {
    pub fn new(world: &mut World) -> Self {
        <Self as System<'_>>::SystemData::setup(world);
        let reader = world.fetch_mut::<EventChannel<MyEvent>>().register_reader();
        Self { reader }
    }
}
#
# impl<'a> amethyst::ecs::System<'a> for MySystem {
#   type SystemData = ();
#   fn run(&mut self, _: Self::SystemData) { }
# }

Finally, you can read events from your System.

# extern crate amethyst;
# use amethyst::ecs::Read;
# use amethyst::shrev::EventChannel;
# #[derive(Debug)]
# pub enum MyEvent {
#   A,
#   B,
# }
# struct MySystem {
#   reader: amethyst::shrev::ReaderId<MyEvent>,
# }
impl<'a> amethyst::ecs::System<'a> for MySystem {
    type SystemData = Read<'a, EventChannel<MyEvent>>;
    fn run(&mut self, my_event_channel: Self::SystemData) {
        for event in my_event_channel.read(&mut self.reader) {
            println!("Received an event: {:?}", event);
        }
    }
}

Pong Tutorial

To get a better feeling for how Amethyst works, we're going to implement a Pong clone. You can find a full Pong example (our end goal) in Amethyst's examples folder. This tutorial breaks that project up into discrete steps so it's easier to understand what everything is doing.

Prerequisites

Make sure to follow the Getting started chapter before starting with the tutorial / running the examples.

Running the code after a chapter

If you've cloned the Amethyst repo, you can run any of the examples like so:

cargo run --example pong_tutorial_01 --features "vulkan"

The example named pong_tutorial_xy contains the code which you should have after following all tutorials from 1 to xy.

Note: On macOS, you might want to use "metal" instead of "vulkan".

The main difference between real game code and the example code is where the config and assets folders are located.

For instance, in the pong_tutorial_01 example we have:

let display_config_path =
    app_root.join("examples/pong_tutorial_01/config/display.ron");

let assets_dir = app_root.join("examples/assets/");

But for your own project you'll probably want something like this:

let display_config_path = app_root.join("config/display.ron");

let assets_dir = app_root.join("assets/");

Setting up the project

In this chapter, we will go through the basics of setting up the amethyst project, starting the logger, opening a window and preparing a simple rendering setup.

Creating a new project

Let's start a new project:

amethyst new pong

Update the dependencies in the project's Cargo.toml so that it contains:

[package]
name = "pong"
version = "0.1.0"
authors = []
edition = "2018"

[dependencies.amethyst]
version = "0.13"
features = ["vulkan"]

Alternatively, if you are developing on macOS, you might want to use the metal rendering backend instead of vulkan. In this case, you should change the features entry in the amethyst dependency table.

[dependencies.amethyst]
version = "0.13"
features = ["metal"]

We can start with editing the main.rs file inside src directory. You can delete everything in that file, then add these imports:

//! Pong Tutorial 1

use amethyst::{
    prelude::*,
    renderer::{
        plugins::{RenderFlat2D, RenderToWindow},
        types::DefaultBackend,
        RenderingBundle,
    },
    utils::application_root_dir,
};

We'll be learning more about these as we go through this tutorial. The prelude includes the basic (and most important) types like Application, World, and State. We also import all the necessary types to define a basic rendering pipeline.

Now we have all the dependencies installed and imports prepared, we are ready to start working on defining our game code.

Creating the game state

Now we create our core game struct:

pub struct Pong;

We'll be implementing the SimpleState trait on this struct, which is used by Amethyst's state machine to start, stop, and update the game.

impl SimpleState for Pong {}

Implementing the SimpleState teaches our application what to do when a close signal is received from your operating system. This happens when you press the close button in your graphical environment. This allows the application to quit as needed.

Now that our Pong is already a game state, let's add some code to actually get things started! We'll start with our main() function, and we'll have it return a Result so that we can use ?. This will allow us to automatically exit if any errors occur during setup.

# extern crate amethyst;
# use amethyst::prelude::*;
fn main() -> amethyst::Result<()> {

    // We'll put the rest of the code here.

    Ok(())
}

Note: The SimpleState is just a simplified version of State trait. It already implements a bunch of stuff for us, like the State's update and handle_event methods that you would have to implement yourself were you using just a regular State. Its behavior mostly cares about handling the exit signal cleanly, by just quitting the application directly from the current state.

Setting up the logger

Inside main() we first start the amethyst logger with a default LoggerConfig so we can see errors, warnings and debug messages while the program is running.

# extern crate amethyst;
#
# fn main() {
amethyst::start_logger(Default::default());
# }

From now on, every info, warning, and error will be present and clearly formatted inside your terminal window.

Note: There are many ways to configure that logger, for example, to write the log to the filesystem. You can find more information about how to do that in Logger API reference. We will use the most basic setup in this tutorial for simplicity.

Preparing the display config

Next, we need to create a DisplayConfig to store the configuration for our game's window. We can either define the configuration in our code or better yet load it from a file. The latter approach is handier, as it allows us to change configuration (e.g, the window size) without having to recompile our game every time.

Starting the project with amethyst new should have automatically generated DisplayConfig data in config/display.ron. If you created the project manually, go ahead and create it now.

In either case, open display.ron and change its contents to the following:

(
    title: "Pong!",
    dimensions: Some((500, 500)),
)

Note: If you have never run into Rusty Object Notation before (or RON for short), it is a data storage format that mirrors Rust's syntax. Here, the data represents the DisplayConfig struct. If you want to learn more about the RON syntax, you can visit the official repository.

This will set the default window dimensions to 500 x 500, and make the title bar say "Pong!" instead of the sad, lowercase default of "pong".

In main() in main.rs, we will prepare the path to a file containing the display configuration:

# extern crate amethyst;
#
# use amethyst::{
#     utils::application_root_dir,
#     Error,
# };
#
# fn main() -> Result<(), Error>{
let app_root = application_root_dir()?;
let display_config_path = app_root.join("config").join("display.ron");
#     Ok(())
# }

Creating an application

In main() in main.rs we are going to add the basic application setup:

# extern crate amethyst;
# use amethyst::{
#     prelude::*,
#     utils::application_root_dir,
# };
# fn main() -> Result<(), amethyst::Error> {
# struct Pong; impl SimpleState for Pong {}
let game_data = GameDataBuilder::default();

# let app_root = application_root_dir()?;
let assets_dir = app_root.join("assets");
let mut world = World::new();
let mut game = Application::new(assets_dir, Pong, game_data)?;
game.run();
#     Ok(())
# }

Here we're creating a new instance of GameDataBuilder, a central repository of all the game logic that runs periodically during the game runtime. Right now it's empty, but soon we will start adding all sorts of systems and bundles to it - which will run our game code.

That builder is then combined with the game state struct (Pong), creating the overarching Amethyst's root object: Application. It binds the OS event loop, state machines, timers and other core components in a central place.

Then we call .run() on game which starts the game loop. The game will continue to run until our SimpleState returns Trans::Quit, or when all states have been popped off the state machine's stack.

Now, try compiling the code.

Note: Please note that when compiling the game for the first time, it may take upwards an half an hour. Be assured, though, that subsequent builds of the project will be faster.

You should be able to see the application start, but nothing will happen and your terminal will hang until you kill the process. This means that the core game loop is running in circles, and is awaiting tasks. Let's give it something to do by adding a renderer!

Setting up basic rendering

After preparing the display config and application scaffolding, it's time to actually use it. Last time we left our GameDataBuilder instance empty, now we'll add some systems to it.

# extern crate amethyst;
# use amethyst::{
#     prelude::*,
#     renderer::{
#         plugins::{RenderFlat2D, RenderToWindow},
#         types::DefaultBackend,
#         RenderingBundle,
#     },
#     utils::application_root_dir,
# };
# fn main() -> Result<(), amethyst::Error>{
let app_root = application_root_dir()?;

let display_config_path = app_root.join("config").join("display.ron");

let mut world = World::new();
let game_data = GameDataBuilder::default()
    .with_bundle(
        RenderingBundle::<DefaultBackend>::new()
            // The RenderToWindow plugin provides all the scaffolding for opening a window and drawing on it
            .with_plugin(
                RenderToWindow::from_config_path(display_config_path)
                    .with_clear([0.0, 0.0, 0.0, 1.0]),
            )
            // RenderFlat2D plugin is used to render entities with a `SpriteRender` component.
            .with_plugin(RenderFlat2D::default()),
    )?;
# Ok(()) }

Here we are adding a RenderingBundle. Bundles are essentially sets of systems preconfigured to work together, so you don't have to write them all down one by one.

Note: We will cover systems and bundles in more detail later. For now, think of a bundle as a collection of systems that, in combination, will provide a certain feature to the engine. You will surely be writing your own bundles for your own game's features soon.

The RenderingBundle has a difference to most other bundles: It doesn't really do much by itself. Instead, it relies on its own plugin system to define what should be rendered and how. We use the with_plugin method to tell it that we want to add the RenderToWindow and RenderFlat2D plugins. Those plugins will equip our renderer with the ability to open a window and draw sprites to it.

In this configuration, our window will have a black background. If you want to use a different color, you can tweak the RGBA values inside with_clear. Values range from 0.0 to 1.0, try using for instance [0.00196, 0.23726, 0.21765, 1.0] to get a nice cyan-colored window.

Note: This setup code is using Amethyst's RenderPlugin trait based system that uses rendy crate to define the rendering. If you plan to go beyond the rendering building blocks that Amethyst provides out of the box, you can read about render graph in the rendy graph docs.

Success! Now we can compile and run this code with cargo run and get a window. It should look something like this:

Step one

Drawing the paddles

Now let's do some drawing! But to draw something, we need something to draw. In Amethyst, those "somethings" are called entities.

Amethyst uses an Entity-Component-System (ECS) framework called specs, also written in Rust. You can learn more about Specs in the The Specs Book. Here's a basic explanation of ECS from the documentation:

The term ECS is shorthand for Entity-Component-System. These are the three core concepts. Each entity is associated with some components. Those entities and components are processed by systems. This way, you have your data (components) completely separated from the behavior (systems). An entity just logically groups components; so a Velocity component can be applied to the Position component of the same entity.

I recommend at least skimming the rest of The Specs Book to get a good intuition of how Amethyst works, especially if you're new to ECS.

A quick refactor

Before adding more of the Pong logic, we are going to separate the application initialization code from the Pong code.

  1. In the src directory, create a new file called pong.rs and add the following use statements. These are needed to make it through this chapter:

    # extern crate amethyst;
    #
    use amethyst::{
        assets::{AssetStorage, Loader, Handle},
        core::transform::Transform,
        ecs::prelude::{Component, DenseVecStorage},
        prelude::*,
        renderer::{Camera, ImageFormat, SpriteRender, SpriteSheet, SpriteSheetFormat, Texture},
    };
    
  2. Move the Pong struct and the impl SimpleState for Pong block from main.rs into pong.rs.

  3. In main.rs declare pong as a module and import the Pong state:

    mod pong;
    
    use crate::pong::Pong;
    

Get around the World

First, in pong.rs, let's add a new method to our State implementation: on_start. This method is called when the State starts. We will leave it empty for now.

# extern crate amethyst;
# use amethyst::prelude::*;
# struct Pong;
impl SimpleState for Pong {
    fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {

    }
}

The StateData<'_, GameData<'_, '_>> is a structure given to all State methods. The important part of its content here is its world field.

The World structure stores all of the game's runtime data -- entities and components.

Rendering the game using the Camera

The first thing we will need in our game is a Camera. This is the component that will determine what is rendered on screen. It behaves just like a real-life camera: it looks at a specific part of the world and can be moved around at will.

  1. Define the size of the playable area at the top of pong.rs.

    pub const ARENA_HEIGHT: f32 = 100.0;
    pub const ARENA_WIDTH: f32 = 100.0;
    

    These are public as they will be used in other modules.

  2. Create the camera entity.

    In pong, we want the camera to cover the entire arena. Let's do it in a new function initialise_camera:

    # extern crate amethyst;
    #
    # const ARENA_HEIGHT: f32 = 100.0;
    # const ARENA_WIDTH: f32 = 100.0;
    # use amethyst::prelude::*;
    # use amethyst::ecs::World;
    # use amethyst::renderer::Camera;
    # use amethyst::core::Transform;
    fn initialise_camera(world: &mut World) {
        // Setup camera in a way that our screen covers whole arena and (0, 0) is in the bottom left. 
        let mut transform = Transform::default();
        transform.set_translation_xyz(ARENA_WIDTH * 0.5, ARENA_HEIGHT * 0.5, 1.0);
    
        world
            .create_entity()
            .with(Camera::standard_2d(ARENA_WIDTH, ARENA_HEIGHT))
            .with(transform)
            .build();
    }
    

    This creates an entity that will carry our camera, with an orthographic projection of the size of our arena. We also attach a Transform component, representing its position in the world.

    The Camera::standard_2d function creates a default 2D camera that is pointed along the world's Z axis. The area in front of the camera has a horizontal X axis, and a vertical Y axis. The X axis increases moving to the right, and the Y axis increases moving up. The camera's position is the center of the viewable area. We position the camera with set_translation_xyz to the middle of our game arena so that (0, 0) is the bottom left of the viewable area, and (ARENA_WIDTH, ARENA_HEIGHT) is the top right.

    Notice that we also shifted the camera 1.0 along the Z axis. This is to make sure that the camera is able to see the sprites that sit on the XY plane where Z is 0.0:

    Camera Z shift

    Note: Orthographic projections are a type of 3D visualization on 2D screens that keeps the size ratio of the 2D images displayed intact. They are very useful in games without actual 3D, like our pong example. Perspective projections are another way of displaying graphics, more useful in 3D scenes.

  3. To finish setting up the camera, we need to call initialise_camera from the Pong state's on_start method:

    # extern crate amethyst;
    # use amethyst::prelude::*;
    # use amethyst::ecs::World;
    # fn initialise_camera(world: &mut World) { }
    # struct MyState;
    # impl SimpleState for MyState {
    fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
        let world = data.world;
    
        initialise_camera(world);
    }
    # }
    

Now that our camera is set up, it's time to add the paddles.

Our first Component

Now, we will create the Paddle component, all in pong.rs.

  1. Define constants for the paddle width and height.

    pub const PADDLE_HEIGHT: f32 = 16.0;
    pub const PADDLE_WIDTH: f32 = 4.0;
    
  2. Define the Side enum and Paddle struct:

    # pub const PADDLE_HEIGHT: f32 = 16.0;
    # pub const PADDLE_WIDTH: f32 = 4.0;
    #
    #[derive(PartialEq, Eq)]
    pub enum Side {
        Left,
        Right,
    }
    
    pub struct Paddle {
        pub side: Side,
        pub width: f32,
        pub height: f32,
    }
    
    impl Paddle {
        fn new(side: Side) -> Paddle {
            Paddle {
                side,
                width: PADDLE_WIDTH,
                height: PADDLE_HEIGHT,
            }
        }
    }
    

    "But that just looks like a regular struct!" you might say.

    And you're right, the special sauce comes next.

  3. Implement the Component trait for Paddle:

    # extern crate amethyst;
    #
    # use amethyst::ecs::{Component, DenseVecStorage};
    #
    # struct Paddle;
    #
    impl Component for Paddle {
        type Storage = DenseVecStorage<Self>;
    }
    

    By implementing Component for the Paddle struct, it can now be attached to entities in the game.

    When implemented the Component trait, we must specify the storage type. Different storage types optimize for faster access, lower memory usage, or a balance between the two. For more information on storage types, check out the Specs documentation.

Initialise some entities

Now that we have a Paddle component, let's define some paddle entities that include that component and add them to our World.

First let's look at our imports:

# extern crate amethyst;
use amethyst::core::transform::Transform;

Transform is an Amethyst ECS component which carries position and orientation information. It is relative to a parent, if one exists.

Okay, let's make some entities! We'll define an initialise_paddles function which will create left and right paddle entities and attach a Transform component to each to position them in our world. As we defined earlier, our canvas is from 0.0 to ARENA_WIDTH in the horizontal dimension and from 0.0 to ARENA_HEIGHT in the vertical dimension. Keep in mind that the anchor point of our entities will be in the middle of the image we will want to render on top of them. This is a good rule to follow in general, as it makes operations like rotation easier.

# extern crate amethyst;
# use amethyst::prelude::*;
# use amethyst::core::Transform;
# use amethyst::ecs::World;
# enum Side {
#   Left,
#   Right,
# }
# struct Paddle;
# impl amethyst::ecs::Component for Paddle {
#   type Storage = amethyst::ecs::VecStorage<Paddle>;
# }
# impl Paddle {
#   fn new(side: Side) -> Paddle { Paddle }
# }
# const PADDLE_HEIGHT: f32 = 16.0;
# const PADDLE_WIDTH: f32 = 4.0;
# const ARENA_HEIGHT: f32 = 100.0;
# const ARENA_WIDTH: f32 = 100.0;
/// Initialises one paddle on the left, and one paddle on the right.
fn initialise_paddles(world: &mut World) {
    let mut left_transform = Transform::default();
    let mut right_transform = Transform::default();

    // Correctly position the paddles.
    let y = ARENA_HEIGHT / 2.0;
    left_transform.set_translation_xyz(PADDLE_WIDTH * 0.5, y, 0.0);
    right_transform.set_translation_xyz(ARENA_WIDTH - PADDLE_WIDTH * 0.5, y, 0.0);

    // Create a left plank entity.
    world
        .create_entity()
        .with(Paddle::new(Side::Left))
        .with(left_transform)
        .build();

    // Create right plank entity.
    world
        .create_entity()
        .with(Paddle::new(Side::Right))
        .with(right_transform)
        .build();
}

This is all the information Amethyst needs to track and move the paddles in our virtual world, but we'll need to do some more work to actually draw them.

As a sanity check, let's make sure the code for initialising the paddles compiles. Update the on_start method to the following:

# extern crate amethyst;
# use amethyst::prelude::*;
# use amethyst::ecs::World;
# fn initialise_paddles(world: &mut World) { }
# fn initialise_camera(world: &mut World) { }
# struct MyState;
# impl SimpleState for MyState {
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
    let world = data.world;

    initialise_paddles(world);
    initialise_camera(world);
}
# }

Let's run our blank screen game!

thread 'main' panicked at 'Tried to fetch a resource, but the resource does not exist.
Try adding the resource by inserting it manually or using the `setup` method.

Uh oh, what's wrong? Sadly the message is pretty difficult to decipher.

If you are using a nightly compiler and enable the nightly feature of Amethyst, you will receive a more informative error message:

thread 'main' panicked at 'Tried to fetch a resource of type "amethyst::ecs::storage::MaskedStorage<pong::Paddle>", but the resource does not exist.
Try adding the resource by inserting it manually or using the `setup` method.'

To turn on the nightly feature, enable the nightly flag for the Amethyst crate in your Cargo.toml file.
Use one of the below methods for declaring dependencies (using both will result in an error):

In Dependencies:

[dependencies]
amethyst = { version = "X.XX", features = ["nightly"] }

In Separate Table:

[dependencies.amethyst]
version = "X.XX"
features = ["nightly"]

Run the project using the nightly channel if you don't have it as your default: cargo +nightly run.

For a Component to be used, there must be a Storage<ComponentType> resource set up in the World. The error message above means we have registered the Paddle component on an entity, but have not set up the Storage. We can fix this by adding the following line before initialise_paddles(world) in the on_start method:

# extern crate amethyst;
# use amethyst::ecs::{World, WorldExt};
# struct Paddle;
# impl amethyst::ecs::Component for Paddle {
#   type Storage = amethyst::ecs::VecStorage<Paddle>;
# }
# fn register() {
#   let mut world = World::new();
world.register::<Paddle>();
# }

This is rather inconvenient — to need to manually register each component before it can be used. There must be a better way. Hint: there is.

When we add systems to our application, any component that a System uses is automatically registered. However, as we haven't got any Systems, we have to live with registering the Paddle component manually.

Let's run the game again.

Amethyst has a lot of internal systems it uses to keep things running we need to bring into the context of the World. For simplicity, these have been grouped into "Bundles" which include related systems and resources. We can add these to our Application's GameData using the with_bundle method, similarly to how you would register a system. We already have RenderBundle in place, registering another one will look similar. You have to first import TransformBundle, then register it as follows:

# extern crate amethyst;
#
use amethyst::core::transform::TransformBundle;
#
# use amethyst::{
#     prelude::*,
#     utils::application_root_dir,
# };
#
# struct Pong;
# impl SimpleState for Pong { }
#
fn main() -> amethyst::Result<()> {
#   amethyst::start_logger(Default::default());
#
#   let app_root = application_root_dir()?;
#   let display_config_path =
#       app_root.join("examples/pong_tutorial_02/config/display.ron");
#
    // ...
    let mut world = World::new();
    let game_data = GameDataBuilder::default()
        // ...

        // Add the transform bundle which handles tracking entity positions
        .with_bundle(TransformBundle::new())?;

#   let assets_dir = "/";
#   let mut game = Application::new(assets_dir, Pong, game_data)?;
#   Ok(())
}

This time, when you run the game you should see the familiar black screen. Hooray!

Drawing

This section will finally allow us to see something.

The first thing we will have to do is load the sprite sheet we will use for all our graphics in the game. Create a texture folder in the assets directory of the project. This will contain the spritesheet texture pong_spritesheet.png, which we need to render the elements of the game. We will perform the loading in a new function in pong.rs called load_sprite_sheet.

First, let's declare the function and load the sprite sheet's image data.

# extern crate amethyst;
#
# use amethyst::{
#     assets::{AssetStorage, Loader, Handle},
#     core::transform::Transform,
#     ecs::prelude::{Component, DenseVecStorage},
#     prelude::*,
#     renderer::{
#         camera::{Camera, Projection},
#         formats::texture::ImageFormat,
#         sprite::{SpriteRender, SpriteSheet, SpriteSheetFormat},
#         Texture,
#     },
# };
#
fn load_sprite_sheet(world: &mut World) -> Handle<SpriteSheet> {
    // Load the sprite sheet necessary to render the graphics.
    // The texture is the pixel data
    // `texture_handle` is a cloneable reference to the texture
    let texture_handle = {
        let loader = world.read_resource::<Loader>();
        let texture_storage = world.read_resource::<AssetStorage<Texture>>();
        loader.load(
            "texture/pong_spritesheet.png",
            ImageFormat::default(),
            (),
            &texture_storage,
        )
    };

    //...
#   unimplemented!()
}

The Loader is an asset loader which is defined as a resource (not an Entity, Component, or System, but still a part of our ECS World). It was created when we built our Application in main.rs, and it can read assets like .obj files, but also it can load a .png as a Texture as in our use case.

Resources in Specs are a type of data which can be shared between systems, while being independent of entities, in contrast to components, which are attached to specific entities.

The AssetStorage<Texture> is also a resource; this is where the loader puts the Texture it will load from our sprite sheet. In order to manage them while remaining fast, Amethyst does not give us direct access to the assets we load. If it did otherwise, we would have to wait for the texture to be fully loaded to do all the other things we have to prepare, which would be a waste of time! Instead, the load function will return a Handle<Texture>. This handle "points" to the place where the asset will be loaded. In Rust terms, it is equivalent to a reference-counted option. It is extremely useful, especially as cloning the handle does not clone the asset in memory, so many things can use the same asset at once.

Alongside our sprite sheet texture, we need a file describing where the sprites are on the sheet. Let's create, right next to it, a file called pong_spritesheet.ron. It will contain the following sprite sheet definition:

(
    texture_width: 8,
    texture_height: 16,
    sprites: [
        (
            x: 0,
            y: 0,
            width: 4,
            height: 16,
        ),
        (
            x: 4,
            y: 0,
            width: 4,
            height: 4,
        ),
    ],
)

Note: Make sure to pay attention to the kind of parentheses in the ron file. Especially, if you are used to writing JSON or similar format files, you might be tempted to use curly braces there; that will however lead to very hard-to-debug errors, especially since amethyst will not warn you about that when compiling.

Finally, we load the file containing the position of each sprite on the sheet.

# extern crate amethyst;
#
# use amethyst::{
#     assets::{AssetStorage, Handle, Loader},
#     core::transform::Transform,
#     ecs::prelude::{Component, DenseVecStorage},
#     prelude::*,
#     renderer::{
#         camera::{Camera, Projection},
#         formats::texture::ImageFormat,
#         sprite::{SpriteRender, SpriteSheet, SpriteSheetFormat},
#         Texture,
#     },
# };
#
fn load_sprite_sheet(world: &mut World) -> Handle<SpriteSheet> {
#
#   let texture_handle = {
#       let loader = world.read_resource::<Loader>();
#       let texture_storage = world.read_resource::<AssetStorage<Texture>>();
#       loader.load(
#           "texture/pong_spritesheet.png",
#           ImageFormat::default(),
#           (),
#           &texture_storage,
#       )
#   };
#
    // ...

    let loader = world.read_resource::<Loader>();
    let sprite_sheet_store = world.read_resource::<AssetStorage<SpriteSheet>>();
    loader.load(
        "texture/pong_spritesheet.ron", // Here we load the associated ron file
        SpriteSheetFormat(texture_handle),
        (),
        &sprite_sheet_store,
    )
# }

This is where we have to use the texture handle. The Loader will take the file containing the sprites' positions and the texture handle, and create a nicely packaged SpriteSheet struct. It is this struct that we will be using to actually draw stuff on the screen.

Please note that the order of sprites declared in the sprite sheet file is also significant, as sprites are referenced by the index in the vector. If you're wondering about the ball sprite, it does exist on the image, but we will get to it in a later part of the tutorial.

So far, so good. We have a sprite sheet loaded, now we need to link the sprites to the paddles. We update the initialise_paddles function by changing its signature to:

# extern crate amethyst;
# use amethyst::ecs::World;
# use amethyst::{assets::Handle, renderer::sprite::SpriteSheet};
fn initialise_paddles(world: &mut World, sprite_sheet: Handle<SpriteSheet>)
# { }

Inside initialise_paddles, we construct a SpriteRender for a paddle. We only need one here, since the only difference between the two paddles is that the right one is flipped horizontally.

# extern crate amethyst;
# use amethyst::ecs::World;
# use amethyst::{assets::Handle, renderer::{SpriteRender, SpriteSheet}};
# fn initialise_paddles(world: &mut World, sprite_sheet: Handle<SpriteSheet>) {
// Assign the sprites for the paddles
let sprite_render = SpriteRender {
    sprite_sheet: sprite_sheet.clone(),
    sprite_number: 0, // paddle is the first sprite in the sprite_sheet
};
# }

SpriteRender is the Component that indicates which sprite of which sprite sheet should be drawn for a particular entity. Since the paddle is the first sprite in the sprite sheet, we use 0 for the sprite_number.

Next we simply add the components to the paddle entities:

# extern crate amethyst;
# use amethyst::ecs::World;
# use amethyst::assets::Handle;
# use amethyst::renderer::sprite::{SpriteSheet, SpriteRender};
# use amethyst::prelude::*;
# fn initialise_paddles(world: &mut World, sprite_sheet: Handle<SpriteSheet>) {
# let sprite_render = SpriteRender {
#   sprite_sheet: sprite_sheet.clone(),
#   sprite_number: 0, // paddle is the first sprite in the sprite_sheet
# };
// Create a left plank entity.
world
    .create_entity()
    .with(sprite_render.clone())
    // ... other components
    .build();

// Create right plank entity.
world
    .create_entity()
    .with(sprite_render.clone())
    // ... other components
    .build();
# }

We're nearly there, we just have to wire up the sprite to the paddles. We put it all together in the on_start() method:

# extern crate amethyst;
# use amethyst::assets::Handle;
# use amethyst::prelude::*;
# use amethyst::renderer::{sprite::SpriteSheet, Texture};
# use amethyst::ecs::World;
# struct Paddle;
# impl amethyst::ecs::Component for Paddle {
#   type Storage = amethyst::ecs::VecStorage<Paddle>;
# }
# fn initialise_paddles(world: &mut World, spritesheet: Handle<SpriteSheet>) { }
# fn initialise_camera(world: &mut World) { }
# fn load_sprite_sheet(world: &mut World) -> Handle<SpriteSheet> { unimplemented!() }
# struct MyState;
# impl SimpleState for MyState {
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
    let world = data.world;

    // Load the spritesheet necessary to render the graphics.
    let sprite_sheet_handle = load_sprite_sheet(world);

    world.register::<Paddle>();

    initialise_paddles(world, sprite_sheet_handle);
    initialise_camera(world);
}
# }

And we're done. Let's run our game and have fun!

If all is well, we should get something that looks like this:

Step two

In the next chapter, we'll explore the "S" in ECS and actually get these paddles moving!

Moving the paddles

In the previous chapter, we learned about the relationship between entities and components, and how they represent the "things" in our games. This chapter introduces Systems - the S in "ECS". Systems are objects that represent operations over entities, or more specifically, combinations of components. Let's add a system that moves the paddles based on user input.

A system is nothing more than a function that runs once each frame and potentially makes some changes to components. If you've used other game engines, this probably sounds familiar: Unity engine calls these objects MonoBehaviours and Unreal engine calls them Actors, but these all represent the same basic idea.

Systems in Specs / Amethyst are slightly different. Rather than describing the behavior of a single instance (e.g., a single enemy in your game), they describe the behavior of all components of a specific type (all enemies). This makes your code more modular, easier to test, and makes it run faster.

Let's get started.

Capturing user input

To capture user input, we'll need to introduce a few more files to our game. Let's start by creating a config file under the config directory of our project, called bindings.ron, which will contain a RON representation of the amethyst_input::Bindings struct:

(
  axes: {
    "left_paddle": Emulated(pos: Key(W), neg: Key(S)),
    "right_paddle": Emulated(pos: Key(Up), neg: Key(Down)),
  },
  actions: {},
)

In Amethyst, inputs can either be axes (a range that represents an analog controller stick or relates two buttons as opposite ends of a range), or actions (also known as scalar input - a button that is either pressed or not). In this file, we're creating the inputs to move each paddle up (pos:) or down (neg:) on the vertical axis: W and S for the left paddle, and the Up and Down arrow keys for the right paddle. We name them "left_paddle" and "right_paddle", which will allow us to refer to them by name in the code when we will need to read their respective values to update positions.

Next, we'll add an InputBundle to the game's Application object, that contains an InputHandler system which captures inputs, and maps them to the axes we defined. Let's make the following changes to main.rs.

# extern crate amethyst;
# use amethyst::prelude::*;
# use amethyst::core::transform::TransformBundle;
# use amethyst::utils::application_root_dir;
# use amethyst::window::DisplayConfig;
# macro_rules! env { ($x:expr) => ("") }
# fn main() -> amethyst::Result<()> {
use amethyst::input::{InputBundle, StringBindings};

# let app_root = application_root_dir()?;
let binding_path = app_root.join("config").join("bindings.ron");

let input_bundle = InputBundle::<StringBindings>::new()
    .with_bindings_from_file(binding_path)?;

# let path = "./config/display.ron";
# let config = DisplayConfig::load(&path);
# let assets_dir = "assets";
# struct Pong;
# impl SimpleState for Pong { }
let mut world = World::new();
let game_data = GameDataBuilder::default()
    .with_bundle(TransformBundle::new())?
    .with_bundle(input_bundle)?
    // ..
    ;
let mut game = Application::new(assets_dir, Pong, game_data)?;
game.run();
# Ok(())
# }

For InputBundle<StringBindings>, the parameter type determines how axes and actions are identified in the bindings.ron file (in this example, Strings are used; e.g. "left_paddle").

At this point, we're ready to write a system that reads input from the InputHandler, and moves the paddles accordingly. First, we'll create a directory called systems under src to hold all our systems. We'll use a module to collect and export each of our systems to the rest of the application. Here's our mod.rs for src/systems:

pub use self::paddle::PaddleSystem;

mod paddle;

We're finally ready to implement the PaddleSystem in systems/paddle.rs:

# extern crate amethyst;
#
# mod pong {
#     use amethyst::ecs::prelude::*;
#
#     pub enum Side {
#       Left,
#       Right,
#     }
#     pub struct Paddle {
#       pub side: Side,
#     }
#     impl Component for Paddle {
#       type Storage = VecStorage<Self>;
#     }
#
#     pub const ARENA_HEIGHT: f32 = 100.0;
#     pub const PADDLE_HEIGHT: f32 = 16.0;
# }
#
use amethyst::core::{Transform, SystemDesc};
use amethyst::derive::SystemDesc;
use amethyst::ecs::{Join, Read, ReadStorage, System, SystemData, World, WriteStorage};
use amethyst::input::{InputHandler, StringBindings};

// You'll have to mark PADDLE_HEIGHT as public in pong.rs
use crate::pong::{Paddle, Side, ARENA_HEIGHT, PADDLE_HEIGHT};

#[derive(SystemDesc)]
pub struct PaddleSystem;

impl<'s> System<'s> for PaddleSystem {
    type SystemData = (
        WriteStorage<'s, Transform>,
        ReadStorage<'s, Paddle>,
        Read<'s, InputHandler<StringBindings>>,
    );

    fn run(&mut self, (mut transforms, paddles, input): Self::SystemData) {
        for (paddle, transform) in (&paddles, &mut transforms).join() {
            let movement = match paddle.side {
                Side::Left => input.axis_value("left_paddle"),
                Side::Right => input.axis_value("right_paddle"),
            };
            if let Some(mv_amount) = movement {
                if mv_amount != 0.0 {
                    let side_name = match paddle.side {
                        Side::Left => "left",
                        Side::Right => "right",
                    };
                    println!("Side {:?} moving {}", side_name, mv_amount);
                }
            }
        }
    }
}
#
# fn main() {}

Alright, there's quite a bit going on here!

We create a unit struct PaddleSystem, and with the SystemDesc derive. This is short for System Descriptor. In Amethyst, systems may need to access resources from the World in order to be instantiated. For each System, an implementation of the SystemDesc trait must be provided to specify the logic to instantiate the System. For Systems that do not require special instantiation logic, the SystemDesc derive automatically implements the SystemDesc trait on the system type itself.

Next, we implement the System trait for it with the lifetime of the components on which it operates. Inside the implementation, we define the data the system operates on in the SystemData tuple: WriteStorage, ReadStorage, and Read. More specifically, the generic types we've used here tell us that the PaddleSystem mutates Transform components, WriteStorage<'s, Transform>, it reads Paddle components, ReadStorage<'s, Paddle>, and also accesses the InputHandler<StringBindings> resource we created earlier, using the Read structure.

For InputHandler<StringBindings>, make sure the parameter type is the same as the one used to create the InputBundle earlier.

Now that we have access to the storages of the components we want, we can iterate over them. We perform a join operation over the Transform and Paddle storages. This will iterate over all entities that have both a Paddle and Transform attached to them, and give us access to the actual components, immutable for the Paddle and mutable for the Transform.

There are many other ways to use storages. For example, you can use them to get a reference to the component of a specific type held by an entity, or simply iterate over them without joining. However, in practice, your most common use will be to join over multiple storages as it is rare to have a system affect only one specific component.

Please also note that it is possible to join over storages using multiple threads by using par_join instead of join, but here the overhead introduced is not worth the gain offered by parallelism.

Let's add this system to our GameDataBuilder in main.rs:

mod systems; // Import the module
// --snip--

# extern crate amethyst;
# use amethyst::prelude::*;
# use amethyst::core::transform::TransformBundle;
# use amethyst::input::StringBindings;
# use amethyst::window::DisplayConfig;
fn main() -> amethyst::Result<()> {
// --snip--

# let path = "./config/display.ron";
# let config = DisplayConfig::load(&path);
# mod systems {
#
# use amethyst::core::ecs::{System, SystemData, World};
# use amethyst::core::SystemDesc;
# use amethyst::derive::SystemDesc;
#
# use amethyst;
# #[derive(SystemDesc)]
# pub struct PaddleSystem;
# impl<'a> amethyst::ecs::System<'a> for PaddleSystem {
# type SystemData = ();
# fn run(&mut self, _: Self::SystemData) { }
# }
# }
# let input_bundle = amethyst::input::InputBundle::<StringBindings>::new();
let mut world = World::new();
let game_data = GameDataBuilder::default()
    // ...
    .with_bundle(TransformBundle::new())?
    .with_bundle(input_bundle)?
    .with(systems::PaddleSystem, "paddle_system", &["input_system"]) // Add this line
    // ...
#   ;
# let assets_dir = "/";
# struct Pong;
# impl SimpleState for Pong { }
# let mut game = Application::new(assets_dir, Pong, game_data)?;
# Ok(())
}

Take a look at the with method call. Here, we're not adding a bundle, we're adding a system alone. We provide an instance of the system, a string representing its name and a list of dependencies. The dependencies are the names of the systems that must be run before our newly added system. Here, we require the input_system to be run as we will use the user's input to move the paddles, so we need to have this data be prepared. The input_system key itself is defined in the standard InputBundle.

Modifying the transform

If we run the game now, we'll see the console print our keypresses. Let's make it update the position of the paddle. To do this, we'll modify the y component of the transform's translation.

# extern crate amethyst;
# use amethyst::core::Transform;
# use amethyst::core::SystemDesc;
# use amethyst::derive::SystemDesc;
# use amethyst::ecs::{Join, Read, ReadStorage, System, SystemData, World, WriteStorage};
# use amethyst::input::{InputHandler, StringBindings};
# enum Side {
#   Left,
#   Right,
# }
# pub struct Paddle {
#   side: Side,
# }
# impl amethyst::ecs::Component for Paddle {
#   type Storage = amethyst::ecs::VecStorage<Paddle>;
# }
# #[derive(SystemDesc)]
# pub struct PaddleSystem;
# impl<'s> System<'s> for PaddleSystem {
#  type SystemData = (
#    WriteStorage<'s, Transform>,
#    ReadStorage<'s, Paddle>,
#    Read<'s, InputHandler<StringBindings>>,
#  );
fn run(&mut self, (mut transforms, paddles, input): Self::SystemData) {
    for (paddle, transform) in (&paddles, &mut transforms).join() {
        let movement = match paddle.side {
            Side::Left => input.axis_value("left_paddle"),
            Side::Right => input.axis_value("right_paddle"),
        };
        if let Some(mv_amount) = movement {
            let scaled_amount = 1.2 * mv_amount as f32;
            transform.prepend_translation_y(scaled_amount);
        }
    }
}
# }

This is our first attempt at moving the paddles: we take the movement and scale it by some factor to make the motion seem smooth. In a real game, we would use the time elapsed between frames to determine how far to move the paddle, so that the behavior of the game would not be tied to the game's framerate. Amethyst provides you with amethyst::core::timing::Time for that purpose, but for now current approach should suffice. If you run the game now, you'll notice the paddles are able to "fall" off the edges of the game area.

To fix this, we need to limit the paddle's movement to the arena border with a minimum and maximum value. But as the anchor point of the paddle is in the middle of the sprite, we also need to offset that limit by half the height of the sprite for the paddles not to go halfway out of the screen. Therefore, we will clamp the y value of the transform from ARENA_HEIGHT - PADDLE_HEIGHT * 0.5 (the top of the arena minus the offset) to PADDLE_HEIGHT * 0.5 (the bottom of the arena plus the offset).

Our run function should now look something like this:

# extern crate amethyst;
# use amethyst::core::Transform;
# use amethyst::core::SystemDesc;
# use amethyst::derive::SystemDesc;
# use amethyst::ecs::{Join, Read, ReadStorage, System, SystemData, World, WriteStorage};
# use amethyst::input::{InputHandler, StringBindings};
# const PADDLE_HEIGHT: f32 = 16.0;
# const PADDLE_WIDTH: f32 = 4.0;
# const ARENA_HEIGHT: f32 = 100.0;
# const ARENA_WIDTH: f32 = 100.0;
# enum Side {
#   Left,
#   Right,
# }
# pub struct Paddle {
#   side: Side,
# }
# impl amethyst::ecs::Component for Paddle {
#   type Storage = amethyst::ecs::VecStorage<Paddle>;
# }
# #[derive(SystemDesc)]
# pub struct PaddleSystem;
# impl<'s> System<'s> for PaddleSystem {
#  type SystemData = (
#    WriteStorage<'s, Transform>,
#    ReadStorage<'s, Paddle>,
#    Read<'s, InputHandler<StringBindings>>,
#  );
fn run(&mut self, (mut transforms, paddles, input): Self::SystemData) {
    for (paddle, transform) in (&paddles, &mut transforms).join() {
        let movement = match paddle.side {
            Side::Left => input.axis_value("left_paddle"),
            Side::Right => input.axis_value("right_paddle"),
        };
        if let Some(mv_amount) = movement {
            let scaled_amount = 1.2 * mv_amount as f32;
            let paddle_y = transform.translation().y;
            transform.set_translation_y(
                (paddle_y + scaled_amount)
                    .min(ARENA_HEIGHT - PADDLE_HEIGHT * 0.5)
                    .max(PADDLE_HEIGHT * 0.5),
            );
        }
    }
}
# }

Automatic set up of resources by a system.

You might remember that we had troubles because Amethyst requires us to register storage for Paddle before we could use it.

Now that we have a system in place that uses the Paddle component, we no longer need to manually register it with the world: the system will take care of that for us, as well as set up the storage.

# extern crate amethyst;
# use amethyst::assets::Handle;
# use amethyst::ecs::World;
# use amethyst::prelude::*;
# use amethyst::renderer::SpriteSheet;
# struct Paddle;
# impl amethyst::ecs::Component for Paddle {
#   type Storage = amethyst::ecs::VecStorage<Paddle>;
# }
# fn initialise_paddles(world: &mut World, spritesheet: Handle<SpriteSheet>) { }
# fn initialise_camera(world: &mut World) { }
# fn load_sprite_sheet(world: &mut World) -> Handle<SpriteSheet> { unimplemented!() }
# struct MyState;
# impl SimpleState for MyState {
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
    let world = data.world;

    // Load the spritesheet necessary to render the graphics.
    let sprite_sheet_handle = load_sprite_sheet(world);

    world.register::<Paddle>(); // <<-- No longer needed

    initialise_paddles(world, sprite_sheet_handle);
    initialise_camera(world);
}
# }

Summary

In this chapter, we added an input handler to our game, so that we could capture keypresses. We then created a system that would interpret these keypresses, and move our game's paddles accordingly. In the next chapter, we'll explore another key concept in real-time games: time. We'll make our game aware of time, and add a ball for our paddles to bounce back and forth.

Making a ball move and bounce

In the previous chapter, we learned how to capture user input to make things move on the screen by creating a System ourselves. This chapter will reuse all the knowledge we acquired through the previous chapters to add a new object to our game: a ball that moves and bounces around!

First, let's define some other useful constants for this chapter in pong.rs:

pub const BALL_VELOCITY_X: f32 = 75.0;
pub const BALL_VELOCITY_Y: f32 = 50.0;
pub const BALL_RADIUS: f32 = 2.0;

This could also be done by using an external config file. This is especially useful when you want to edit values a lot. Here, we're keeping it simple.

Create our next Component: The ball Component!

In pong.rs, let's create the Ball Component.

# extern crate amethyst;
# use amethyst::ecs::prelude::{Component, DenseVecStorage};
pub struct Ball {
    pub velocity: [f32; 2],
    pub radius: f32,
}

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

A ball has a velocity and a radius, so we store that information in the component.

Then let's add an initialise_ball function the same way we wrote the initialise_paddles function.

# extern crate amethyst;
# use amethyst::prelude::*;
# use amethyst::assets::{Loader, AssetStorage, Handle};
# use amethyst::renderer::{Texture, SpriteRender, Sprite, SpriteSheet};
# use amethyst::ecs::World;
# use amethyst::core::transform::Transform;
# use amethyst::ecs::prelude::{Component, DenseVecStorage};
# pub struct Ball {
#    pub velocity: [f32; 2],
#    pub radius: f32,
# }
# impl Component for Ball {
#    type Storage = DenseVecStorage<Self>;
# }
# const PADDLE_HEIGHT: f32 = 16.0;
# const PADDLE_WIDTH: f32 = 4.0;
# const SPRITESHEET_SIZE: (f32, f32) = (8.0, 16.0);
# const BALL_RADIUS: f32 = 2.0;
# const BALL_VELOCITY_X: f32 = 75.0;
# const BALL_VELOCITY_Y: f32 = 50.0;
# const ARENA_HEIGHT: f32 = 100.0;
# const ARENA_WIDTH: f32 = 100.0;
/// Initialises one ball in the middle-ish of the arena.
fn initialise_ball(world: &mut World, sprite_sheet_handle: Handle<SpriteSheet>) {
    // Create the translation.
    let mut local_transform = Transform::default();
    local_transform.set_translation_xyz(ARENA_WIDTH / 2.0, ARENA_HEIGHT / 2.0, 0.0);

    // Assign the sprite for the ball
    let sprite_render = SpriteRender {
        sprite_sheet: sprite_sheet_handle,
        sprite_number: 1, // ball is the second sprite on the sprite sheet
    };

    world
        .create_entity()
        .with(sprite_render)
        .with(Ball {
            radius: BALL_RADIUS,
            velocity: [BALL_VELOCITY_X, BALL_VELOCITY_Y],
        })
        .with(local_transform)
        .build();
}

In a previous chapter we saw how to load a sprite sheet and get things drawn on the screen. Remember sprite sheet information is stored in pong_spritesheet.ron, and the ball sprite was the second one, whose index is 1.

Finally, let's make sure the code is working as intended by updating the on_start method:

# extern crate amethyst;
# use amethyst::prelude::*;
# use amethyst::assets::Handle;
# use amethyst::renderer::{Texture, SpriteSheet};
# use amethyst::ecs::World;
# struct Paddle;
# impl amethyst::ecs::Component for Paddle {
#   type Storage = amethyst::ecs::VecStorage<Self>;
# }
# struct Ball;
# impl amethyst::ecs::Component for Ball {
#   type Storage = amethyst::ecs::VecStorage<Self>;
# }
# fn initialise_ball(world: &mut World, sprite_sheet_handle: Handle<SpriteSheet>) { }
# fn initialise_paddles(world: &mut World, spritesheet: Handle<SpriteSheet>) { }
# fn initialise_camera(world: &mut World) { }
# fn load_sprite_sheet(world: &mut World) -> Handle<SpriteSheet> { unimplemented!() }
# struct MyState;
# impl SimpleState for MyState {
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
    let world = data.world;

    // Load the spritesheet necessary to render the graphics.
    let sprite_sheet_handle = load_sprite_sheet(world);

    world.register::<Ball>(); // <- add this line temporarily

    initialise_ball(world, sprite_sheet_handle.clone()); // <- add this line
    initialise_paddles(world, sprite_sheet_handle);
    initialise_camera(world);
}
# }

Don't forget to call clone on sprite_sheet_handle because initialise_paddles and initialise_ball consume the handle.

By running the game now, you should be able to see the two paddles and the ball in the center. In the next section, we're going to make this ball actually move!

Create systems to make the ball move

We're now ready to implement the MoveBallsSystem in systems/move_balls.rs:

# extern crate amethyst;
# use amethyst::ecs::prelude::{Component, DenseVecStorage};
#
# mod pong {
#     use amethyst::ecs::prelude::*;
#
#     pub struct Ball {
#        pub velocity: [f32; 2],
#        pub radius: f32,
#     }
#     impl Component for Ball {
#        type Storage = DenseVecStorage<Self>;
#     }
# }
#
use amethyst::{
    core::timing::Time,
    core::transform::Transform,
    core::SystemDesc,
    derive::SystemDesc,
    ecs::prelude::{Join, Read, ReadStorage, System, SystemData, World, WriteStorage},
};

use crate::pong::Ball;

#[derive(SystemDesc)]
pub struct MoveBallsSystem;

impl<'s> System<'s> for MoveBallsSystem {
    type SystemData = (
        ReadStorage<'s, Ball>,
        WriteStorage<'s, Transform>,
        Read<'s, Time>,
    );

    fn run(&mut self, (balls, mut locals, time): Self::SystemData) {
        // Move every ball according to its speed, and the time passed.
        for (ball, local) in (&balls, &mut locals).join() {
            local.prepend_translation_x(ball.velocity[0] * time.delta_seconds());
            local.prepend_translation_y(ball.velocity[1] * time.delta_seconds());
        }
    }
}
#
# fn main() {}

This system is responsible for moving all balls according to their speed and the elapsed time. Notice how the join() method is used to iterate over all ball entities. Here we only have one ball, but if we ever need multiple, the system will handle them out of the box. In this system, we also want framerate independence. That is, no matter the framerate, all objects move with the same speed. To achieve that, a delta time, which is the duration since the last frame, is used. This is commonly known as "delta timing". As you can see in the snippet, to gain access to time passed since the last frame, you need to use amethyst::core::timing::Time, a commonly used resource. It has a method called delta_seconds that does exactly what we want.

Now that our ball can move, let's implement a new System: BounceSystem in systems/bounce.rs. It will be responsible for detecting collisions between balls and paddles, as well as balls and the top and bottom edges of the arena. If a collision is detected, the ball bounces off. This is done by negating the velocity of the Ball component on the x or y axis.

# extern crate amethyst;
# use amethyst::ecs::prelude::{Component, DenseVecStorage};
#
# mod pong {
#     use amethyst::ecs::prelude::*;
#
#     pub struct Ball {
#        pub velocity: [f32; 2],
#        pub radius: f32,
#     }
#     impl Component for Ball {
#        type Storage = DenseVecStorage<Self>;
#     }
#
#     #[derive(PartialEq, Eq)]
#     pub enum Side {
#       Left,
#       Right,
#     }
#
#     pub struct Paddle {
#       pub side: Side,
#       pub width: f32,
#       pub height: f32,
#     }
#     impl Component for Paddle {
#       type Storage = VecStorage<Self>;
#     }
#
#     pub const ARENA_HEIGHT: f32 = 100.0;
# }
#
use amethyst::{
    core::{Transform, SystemDesc},
    derive::SystemDesc,
    ecs::prelude::{Join, ReadStorage, System, SystemData, World, WriteStorage},
};

use crate::pong::{Ball, Side, Paddle, ARENA_HEIGHT};

# #[derive(SystemDesc)]
pub struct BounceSystem;

impl<'s> System<'s> for BounceSystem {
    type SystemData = (
        WriteStorage<'s, Ball>,
        ReadStorage<'s, Paddle>,
        ReadStorage<'s, Transform>,
    );

    fn run(&mut self, (mut balls, paddles, transforms): Self::SystemData) {
        // Check whether a ball collided, and bounce off accordingly.
        //
        // We also check for the velocity of the ball every time, to prevent multiple collisions
        // from occurring.
        for (ball, transform) in (&mut balls, &transforms).join() {
            let ball_x = transform.translation().x;
            let ball_y = transform.translation().y;

            // Bounce at the top or the bottom of the arena.
            if (ball_y <= ball.radius && ball.velocity[1] < 0.0)
                || (ball_y >= ARENA_HEIGHT - ball.radius && ball.velocity[1] > 0.0)
            {
                ball.velocity[1] = -ball.velocity[1];
            }

            // Bounce at the paddles.
            for (paddle, paddle_transform) in (&paddles, &transforms).join() {
                let paddle_x = paddle_transform.translation().x - (paddle.width * 0.5);
                let paddle_y = paddle_transform.translation().y - (paddle.height * 0.5);

                // To determine whether the ball has collided with a paddle, we create a larger
                // rectangle around the current one, by subtracting the ball radius from the
                // lowest coordinates, and adding the ball radius to the highest ones. The ball
                // is then within the paddle if its center is within the larger wrapper
                // rectangle.
                if point_in_rect(
                    ball_x,
                    ball_y,
                    paddle_x - ball.radius,
                    paddle_y - ball.radius,
                    paddle_x + paddle.width + ball.radius,
                    paddle_y + paddle.height + ball.radius,
                ) {
                    if (paddle.side == Side::Left && ball.velocity[0] < 0.0)
                        || (paddle.side == Side::Right && ball.velocity[0] > 0.0)
                    {
                        ball.velocity[0] = -ball.velocity[0];
                    }
                }
            }
        }
    }
}

// A point is in a box when its coordinates are smaller or equal than the top
// right and larger or equal than the bottom left.
fn point_in_rect(x: f32, y: f32, left: f32, bottom: f32, right: f32, top: f32) -> bool {
    x >= left && x <= right && y >= bottom && y <= top
}
#
# fn main() {}

The following image illustrates how collisions with paddles are checked.

Collision explanotary drawing

Also, don't forget to add mod move_balls and mod bounce in systems/mod.rs as well as adding our new systems to the game data:

# extern crate amethyst;
# use amethyst::prelude::*;
# use amethyst::core::transform::TransformBundle;
# use amethyst::window::DisplayConfig;
# use amethyst::input::StringBindings;
# fn main() -> amethyst::Result<()> {
# let path = "./config/display.ron";
# let config = DisplayConfig::load(&path);
# mod systems {
# use amethyst;
# use amethyst::core::ecs::{System, SystemData, World};
# use amethyst::core::SystemDesc;
# use amethyst::derive::SystemDesc;
# #[derive(SystemDesc)]
# pub struct PaddleSystem;
# impl<'a> amethyst::ecs::System<'a> for PaddleSystem {
# type SystemData = ();
# fn run(&mut self, _: Self::SystemData) { }
# }
# #[derive(SystemDesc)]
# pub struct MoveBallsSystem;
# impl<'a> amethyst::ecs::System<'a> for MoveBallsSystem {
# type SystemData = ();
# fn run(&mut self, _: Self::SystemData) { }
# }
# #[derive(SystemDesc)]
# pub struct BounceSystem;
# impl<'a> amethyst::ecs::System<'a> for BounceSystem {
# type SystemData = ();
# fn run(&mut self, _: Self::SystemData) { }
# }
# }
# let input_bundle = amethyst::input::InputBundle::<StringBindings>::new();
let mut world = World::new();
let game_data = GameDataBuilder::default()
#    .with_bundle(TransformBundle::new())?
#    .with_bundle(input_bundle)?
#    .with(systems::PaddleSystem, "paddle_system", &["input_system"])
    // ...other systems...
    .with(systems::MoveBallsSystem, "ball_system", &[])
    .with(
        systems::BounceSystem,
        "collision_system",
        &["paddle_system", "ball_system"],
    );
# let assets_dir = "/";
# struct Pong;
# impl SimpleState for Pong { }
# let mut game = Application::new(assets_dir, Pong, game_data)?;
# Ok(())
# }

You should now have a ball moving and bouncing off paddles and off the top and bottom of the screen. However, you will quickly notice that if the ball goes out of the screen on the right or the left, it never comes back and the game is over. You might not even see that at all, as the ball might be already outside of the screen when the window comes up. You might have to dramatically reduce BALL_VELOCITY_X in order to see that in action. This obviously isn't a good solution for an actual game. To fix that problem and better see what's happening we have to spawn the ball with a slight delay.

Spawning ball with a delay

The ball now spawns and moves off screen instantly when the game starts. This might be disorienting, as you might be thrown into the game and lose your first point before you had the time to notice. We also have to give some time for the operating system and the renderer to initialize the window before the game starts. Usually, you would have a separate state with a game menu, so this isn't an issue. Our pong game throws you right into the action, so we have to fix that problem.

Let's delay the first time the ball spawns. This is also a good opportunity to use our game state struct to actually hold some data.

First, let's add a new method to our state: update. Let's add that update method just below on_start:

# extern crate amethyst;
# use amethyst::prelude::*;
# struct MyState;
# impl SimpleState for MyState {
fn update(&mut self, data: &mut StateData<'_, GameData<'_, '_>>) -> SimpleTrans {
    Trans::None
}
# }

That method allows you to transition out of state using its return value. Here, we do not want to change any state, so we return Trans::None.

Now we have to move paddle creation to that method and add some delay to it. Our update runs every frame, so in order to do something only once after a given time, we have to use our local state. Additionally, notice that initialise_paddles requires us to provide the sprite_sheet_handle, but it was created as a local variable inside on_start. For that reason, we have to make it a part of the state too.

Let's add some fields to our Pong struct:

# extern crate amethyst; use amethyst::renderer::SpriteSheet;
# use amethyst::assets::Handle;
#[derive(Default)]
pub struct Pong {
    ball_spawn_timer: Option<f32>,
    sprite_sheet_handle: Option<Handle<SpriteSheet>>,
}

Our timer is represented by Option<f32>, which will count down to zero when available, and be replaced with None after the time has passed. Our sprite sheet handle is also inside Option because we can't create it inside Pong constructor. It will be created inside the on_start method instead.

We've also added #[derive(Default)], which will automatically implement Default trait for us, which allows to create default empty state. Now let's use that inside our Application creation code in main.rs:

# extern crate amethyst;
# use amethyst::{
#     ecs::{World, WorldExt},
#     prelude::*,
# };
#
# #[derive(Default)] struct Pong;
# impl SimpleState for Pong { }
# fn main() -> amethyst::Result<()> {
#   let game_data = GameDataBuilder::default();
#   let assets_dir = "/";
#   let world = World::new();
let mut game = Application::new(assets_dir, Pong::default(), game_data)?;
#   Ok(())
# }

Now let's finish our timer and ball spawning code. We have to do two things:

  • First, we have to initialize our state and remove initialise_ball from on_start,
  • then we have to initialise_ball once after the time has passed inside update:
# extern crate amethyst;
# use amethyst::{assets::Handle, renderer::SpriteSheet};
# use amethyst::prelude::*;
use amethyst::core::timing::Time;

# struct Paddle;
# impl amethyst::ecs::Component for Paddle {
#   type Storage = amethyst::ecs::VecStorage<Self>;
# }
# struct Ball;
# impl amethyst::ecs::Component for Ball {
#   type Storage = amethyst::ecs::VecStorage<Self>;
# }
# fn initialise_ball(world: &mut World, sprite_sheet_handle: Handle<SpriteSheet>) { }
# fn initialise_paddles(world: &mut World, spritesheet: Handle<SpriteSheet>) { }
# fn initialise_camera(world: &mut World) { }
# fn load_sprite_sheet(world: &mut World) -> Handle<SpriteSheet> { unimplemented!() }
# #[derive(Default)] pub struct Pong {
#     ball_spawn_timer: Option<f32>,
#     sprite_sheet_handle: Option<Handle<SpriteSheet>>,
# }
# 
impl SimpleState for Pong {
    fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
        let world = data.world;

        // Wait one second before spawning the ball.
        self.ball_spawn_timer.replace(1.0);

        // Load the spritesheet necessary to render the graphics.
        // `spritesheet` is the layout of the sprites on the image;
        // `texture` is the pixel data.
        self.sprite_sheet_handle.replace(load_sprite_sheet(world));
        initialise_paddles(world, self.sprite_sheet_handle.clone().unwrap());
        initialise_camera(world);
    }

    fn update(&mut self, data: &mut StateData<'_, GameData<'_, '_>>) -> SimpleTrans {
        if let Some(mut timer) = self.ball_spawn_timer.take() {
            // If the timer isn't expired yet, subtract the time that passed since the last update.
            {
                let time = data.world.fetch::<Time>();
                timer -= time.delta_seconds();
            }
            if timer <= 0.0 {
                // When timer expire, spawn the ball
                initialise_ball(data.world, self.sprite_sheet_handle.clone().unwrap());
            } else {
                // If timer is not expired yet, put it back onto the state.
                self.ball_spawn_timer.replace(timer);
            }
        }
        Trans::None
    }
}

Now our ball will only show up after a set delay, giving us some breathing room after startup. This will give us a better opportunity to see what happens to the ball immediately when it spawns.

Summary

In this chapter, we finally added a ball to our game. As always, the full code is available under the pong_tutorial_04 example in the Amethyst repository. In the next chapter, we'll add a system checking when a player loses the game, and add a scoring system!

Winning Rounds and Keeping Score

Our last chapter ended on a bit of a cliffhanger. What happens when our ball reaches the left or right edge of the screen? It just keeps going! 😦

In this chapter, we'll fix that by putting the ball back into play after it leaves either side of the arena. We'll also add a scoreboard and keep track of who's winning and losing.

Winning and Losing Rounds

So let's fix the big current issue; having a game that only works for one round isn't very fun. We'll add a new system that will check if the ball has reached either edge of the arena and reset its position and velocity. We'll also make a note of who got the point for the round.

First, we'll add a new module to systems/mod.rs

pub use self::winner::WinnerSystem;

mod winner;

Then, we'll create systems/winner.rs:

# extern crate amethyst;
#
# mod pong {
#     use amethyst::ecs::prelude::*;
#
#     pub struct Ball {
#         pub radius: f32,
#         pub velocity: [f32; 2],
#     }
#     impl Component for Ball {
#         type Storage = DenseVecStorage<Self>;
#     }
#
#     pub const ARENA_WIDTH: f32 = 100.0;
# }
#
use amethyst::{
    core::transform::Transform,
    core::SystemDesc,
    derive::SystemDesc,
    ecs::prelude::{Join, System, SystemData, World, WriteStorage},
};

use crate::pong::{Ball, ARENA_WIDTH};

#[derive(SystemDesc)]
pub struct WinnerSystem;

impl<'s> System<'s> for WinnerSystem {
    type SystemData = (
        WriteStorage<'s, Ball>,
        WriteStorage<'s, Transform>,
    );

    fn run(&mut self, (mut balls, mut locals): Self::SystemData) {
        for (ball, transform) in (&mut balls, &mut locals).join() {
            let ball_x = transform.translation().x;

            let did_hit = if ball_x <= ball.radius {
                // Right player scored on the left side.
                println!("Player 2 Scores!");
                true
            } else if ball_x >= ARENA_WIDTH - ball.radius {
                // Left player scored on the right side.
                println!("Player 1 Scores!");
                true
            } else {
                false
            };

            if did_hit {
                ball.velocity[0] = -ball.velocity[0]; // Reverse Direction
                transform.set_translation_x(ARENA_WIDTH / 2.0); // Reset Position
            }
        }
    }
}
#
# fn main() {}

Here, we're creating a new system, joining on all Entities that have a Ball and a Transform component, and then checking each ball to see if it has reached either the left or right boundary of the arena. If so, we reverse its direction and put it back in the middle of the screen.

Now, we just need to add our new system to main.rs, and we should be able to keep playing after someone scores and log who got the point.

# extern crate amethyst;
#
# use amethyst::{
#    core::transform::TransformBundle,
#    ecs::{World, WorldExt},
#    prelude::*,
#    input::StringBindings,
#    window::DisplayConfig,
# };
#
# mod systems {
#     use amethyst;
#     use amethyst::core::SystemDesc;
#     use amethyst::core::ecs::{System, SystemData, World};
#     use amethyst::derive::SystemDesc;
#
#     #[derive(SystemDesc)]
#     pub struct PaddleSystem;
#     impl<'a> amethyst::ecs::System<'a> for PaddleSystem {
#         type SystemData = ();
#         fn run(&mut self, _: Self::SystemData) { }
#     }
#     #[derive(SystemDesc)]
#     pub struct MoveBallsSystem;
#     impl<'a> amethyst::ecs::System<'a> for MoveBallsSystem {
#         type SystemData = ();
#         fn run(&mut self, _: Self::SystemData) { }
#     }
#     #[derive(SystemDesc)]
#     pub struct BounceSystem;
#     impl<'a> amethyst::ecs::System<'a> for BounceSystem {
#         type SystemData = ();
#         fn run(&mut self, _: Self::SystemData) { }
#     }
#     #[derive(SystemDesc)]
#     pub struct WinnerSystem;
#     impl<'a> amethyst::ecs::System<'a> for WinnerSystem {
#         type SystemData = ();
#         fn run(&mut self, _: Self::SystemData) { }
#     }
# }
#
# fn main() -> amethyst::Result<()> {
#
# let path = "./config/display.ron";
# let config = DisplayConfig::load(&path);
# let input_bundle = amethyst::input::InputBundle::<StringBindings>::new();
#
# let mut world = World::new();
let game_data = GameDataBuilder::default()
#    .with_bundle(TransformBundle::new())?
#    .with_bundle(input_bundle)?
#    .with(systems::PaddleSystem, "paddle_system", &["input_system"])
#    .with(systems::MoveBallsSystem, "ball_system", &[])
#    .with(
#        systems::BounceSystem,
#        "collision_system",
#        &["paddle_system", "ball_system"],
#    )
    .with(systems::WinnerSystem, "winner_system", &["ball_system"]);
#
# let assets_dir = "/";
# struct Pong;
# impl SimpleState for Pong { }
# let mut game = Application::new(assets_dir, Pong, game_data)?;
# Ok(())
# }

Adding a Scoreboard

We have a pretty functional Pong game now! At this point, the least fun thing about the game is just that players have to keep track of the score themselves. Our game should be able to do that for us.

In this section, we'll set up UI rendering for our game and create a scoreboard to display our players' scores.

First, let's add the UI rendering in main.rs. Add the following imports:

# extern crate amethyst;
use amethyst::ui::{RenderUi, UiBundle};

Then, add a RenderUi plugin to your RenderBundle like so:

# extern crate amethyst;
# use amethyst::{
#     ecs::{World, WorldExt},
#     prelude::*,
#     renderer::{
#         types::DefaultBackend,
#         RenderingBundle,
#     },
#     ui::RenderUi,
# };
# fn main() -> Result<(), amethyst::Error>{
# let mut world = World::new();
# let game_data = GameDataBuilder::default()
    .with_bundle(RenderingBundle::<DefaultBackend>::new()
        // ...
            .with_plugin(RenderUi::default()),
    )?;
# Ok(()) }

Finally, add the UiBundle after the InputBundle:

# extern crate amethyst;
# use amethyst::{
#     ecs::{World, WorldExt},
#     input::StringBindings,
#     prelude::*,
# };
# use amethyst::ui::UiBundle;
# fn main() -> Result<(), amethyst::Error>{
# let display_config_path = "";
# struct Pong;
# let mut world = World::new();
# let game_data = GameDataBuilder::default()
.with_bundle(UiBundle::<StringBindings>::new())?
# ;
# 
# Ok(())
# }

We're adding a RenderUi to our RenderBundle, and we're also adding the UiBundle to our game data. This allows us to start rendering UI visuals to our game in addition to the existing background and sprites.

Note: We're using a UiBundle with type StringBindings here because the UiBundle needs to know what types our InputHandler is using to map actions and axes. So just know that your UiBundle type should match your InputHandler type. You can read more about those here: UiBundle, InputHandler.

Now we have everything set up so we can start rendering a scoreboard in our game. We'll start by creating some structures in pong.rs:

# extern crate amethyst;
use amethyst::{
    // --snip--
    ecs::prelude::{Component, DenseVecStorage, Entity},
};

/// ScoreBoard contains the actual score data
#[derive(Default)]
pub struct ScoreBoard {
    pub score_left: i32,
    pub score_right: i32,
}

/// ScoreText contains the ui text components that display the score
pub struct ScoreText {
    pub p1_score: Entity,
    pub p2_score: Entity,
}

Don't glimpse over the #[derive(Default)] annotation for the ScoreBoard struct!

ScoreBoard is just a container that will allow us to keep track of each player's score. We'll use this in another module later in this chapter, so we've gone ahead and marked it as public (same with ScoreText). ScoreText is also a container, but this one holds handles to the UI Entitys that will be rendered to the screen. We'll create those next:

# extern crate amethyst;
#
use amethyst::{
#     assets::{AssetStorage, Loader},
#     ecs::Entity,
#     prelude::*,
    // ...
    ui::{Anchor, TtfFormat, UiText, UiTransform},
};

# pub struct Pong;
# 
impl SimpleState for Pong {
    fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
#       let world = data.world;
        // --snip--

        initialise_scoreboard(world);
    }
}
// ...

/// Initialises a ui scoreboard
fn initialise_scoreboard(world: &mut World) {
    let font = world.read_resource::<Loader>().load(
        "font/square.ttf",
        TtfFormat,
        (),
        &world.read_resource(),
    );
    let p1_transform = UiTransform::new(
        "P1".to_string(), Anchor::TopMiddle, Anchor::TopMiddle,
        -50., -50., 1., 200., 50.,
    );
    let p2_transform = UiTransform::new(
        "P2".to_string(), Anchor::TopMiddle, Anchor::TopMiddle,
        50., -50., 1., 200., 50.,
    );

    let p1_score = world
        .create_entity()
        .with(p1_transform)
        .with(UiText::new(
            font.clone(),
            "0".to_string(),
            [1., 1., 1., 1.],
            50.,
        )).build();

    let p2_score = world
        .create_entity()
        .with(p2_transform)
        .with(UiText::new(
            font.clone(),
            "0".to_string(),
            [1., 1., 1., 1.],
            50.,
        )).build();

# pub struct ScoreText {pub p1_score: Entity,pub p2_score: Entity,}
    world.insert(ScoreText { p1_score, p2_score });
}

Here, we add some UI imports and create a new initialise_scoreboard function, which we'll call in the on_start method of the Pong game state.

Inside initialise_scoreboard, we're first going to load up a font which we've saved to assets/font/square.ttf (download). We pull in the TtfFormat to match this font type, load the font as a resource in the world, and then save the handle to a font variable (which we'll use to create our UiText components).

Next, we create a transform for each of our two scores by giving them a unique id (P1 and P2), a UI Anchor at the top middle of our window, and then adjust their global x, y, and z coordinates, width, height, and tab-order.

After creating the font and transforms, we'll create an Entity in the world for each of our players' scores, with their transform and a UiText component (with a font handle, initial text, color, and font_size).

Finally, we initialize a ScoreText structure containing each of our UI Entitys and add it as a resource to the world so we can access it from our Systems later.

If we've done everything right so far, we should see 0 0 at the top of our game window. You'll notice that the scores don't update yet when the ball makes it to either side, so we'll add that next!

Updating the Scoreboard

All that's left for us to do now is update the UI whenever a player scores a point. You'll see just how easy this is with our ECS design. All we have to do is modify our WinnerSystem to access the players' scores and update them accordingly:

# extern crate amethyst;
#
# mod pong {
#     use amethyst::ecs::prelude::*;
#
#     pub struct Ball {
#         pub radius: f32,
#         pub velocity: [f32; 2],
#     }
#     impl Component for Ball {
#         type Storage = DenseVecStorage<Self>;
#     }
#
#     #[derive(Default)]
#     pub struct ScoreBoard {
#         pub score_left: i32,
#         pub score_right: i32,
#     }
#
#     pub struct ScoreText {
#         pub p1_score: Entity,
#         pub p2_score: Entity,
#     }
#
#     pub const ARENA_WIDTH: f32 = 100.0;
# }
#
use amethyst::{
#     core::transform::Transform,
#     core::SystemDesc,
#     derive::SystemDesc,
    // --snip--
    ecs::prelude::{Join, ReadExpect, System, SystemData, World, Write, WriteStorage},
    ui::UiText,
};

use crate::pong::{Ball, ScoreBoard, ScoreText, ARENA_WIDTH};

#[derive(SystemDesc)]
pub struct WinnerSystem;

impl<'s> System<'s> for WinnerSystem {
    type SystemData = (
        WriteStorage<'s, Ball>,
        WriteStorage<'s, Transform>,
        WriteStorage<'s, UiText>,
        Write<'s, ScoreBoard>,
        ReadExpect<'s, ScoreText>,
    );

    fn run(&mut self, (
        mut balls,
        mut locals,
        mut ui_text,
        mut scores,
        score_text
    ): Self::SystemData) {
        for (ball, transform) in (&mut balls, &mut locals).join() {
#             let ball_x = transform.translation().x;
            // --snip--

            let did_hit = if ball_x <= ball.radius {
                // Right player scored on the left side.
                // We top the score at 999 to avoid text overlap.
                scores.score_right = (scores.score_right + 1)
                    .min(999);

                if let Some(text) = ui_text.get_mut(score_text.p2_score) {
                    text.text = scores.score_right.to_string();
                }
                true
            } else if ball_x >= ARENA_WIDTH - ball.radius {
                // Left player scored on the right side.
                // We top the score at 999 to avoid text overlap.
                scores.score_left = (scores.score_left + 1)
                    .min(999);
                if let Some(text) = ui_text.get_mut(score_text.p1_score) {
                    text.text = scores.score_left.to_string();
                }
                true
            } else {
                false
            };

            if did_hit {
#                 ball.velocity[0] = -ball.velocity[0]; // Reverse Direction
#                 transform.set_translation_x(ARENA_WIDTH / 2.0); // Reset Position
                // --snip--

                // Print the scoreboard.
                println!(
                    "Score: | {:^3} | {:^3} |",
                    scores.score_left, scores.score_right
                );
            }
        }
    }
}
#
# fn main() {}

We've added a fair few changes here, so let's go through them. First, we want to be able to read and write our scores, so we add the UiText storage, which holds all UiText components, to our SystemData. We'll want to select our players' scores from that, so we also add the ScoreText structure which holds handles to the UiText components that we want. Finally, we add the ScoreBoard resource so we can keep track of the actual score data.

We're using Write here to pull in the ScoreBoard instead of with WriteStorage because we want mutable access to ScoreBoard, which is not a collection of components but rather a single resource item. This item is strictly required in all cases, but if we wanted it to be optional we could use Option<Write<'s, ScoreBoard>> instead.

We also use ReadExpect to access the ScoreText resource immutably. Again, ScoreText is a single resource item rather than a collection of components. With ReadExpect, we are asserting that ScoreText must already exist and will panic if it does not. We do this instead of just using Read because we are manually adding the ScoreText resource to the game in pong.rs > initialise_scoreboard instead of having the system create this resource for us automatically.

Inside our run method (after updating the signature to match our SystemData changes), we replace the println! statements with code that will update our UiText components. We first update the score stored in score_board by adding 1 to it and clamping it to not exceed 999 (mostly because we don't want our scores to overlap each other in the window). Then, we use the UiText Entity handle that we stored in our ScoreText resource to get a mutable reference to our UiText component. Lastly, we set the text of the UiText component to the player's score, after converting it to a string.

Summary

And that's it! Our game now keeps track of the score for us and displays it at the top of our window.

Pong Game with Scores

Now don't go just yet, because, in the next chapter, we'll make our Pong game even better by adding sound effects and even some music!

Adding audio

Now that we have a functional pong game, let's spice things up by adding some audio. In this chapter, we'll add sound effects and background music.

Adding the Sounds Resource

Let's get started by creating an audio subdirectory under assets. Then download the bounce sound and the score sound and put them in audio/assets.

Next, we'll create a Resource to store our sound effects in. In main.rs, add:

mod audio;

Create a file called audio.rs:

# extern crate amethyst;
#
use amethyst::{
    assets::Loader,
    audio::{OggFormat, SourceHandle},
    ecs::{World, WorldExt},
};

const BOUNCE_SOUND: &str = "audio/bounce.ogg";
const SCORE_SOUND: &str = "audio/score.ogg";

pub struct Sounds {
    pub score_sfx: SourceHandle,
    pub bounce_sfx: SourceHandle,
}

/// Loads an ogg audio track.
fn load_audio_track(loader: &Loader, world: &World, file: &str) -> SourceHandle {
    loader.load(file, OggFormat, (), &world.read_resource())
}

/// Initialise audio in the world. This will eventually include
/// the background tracks as well as the sound effects, but for now
/// we'll just work on sound effects.
pub fn initialise_audio(world: &mut World) {
    let sound_effects = {
        let loader = world.read_resource::<Loader>();

        let sound = Sounds {
            bounce_sfx: load_audio_track(&loader, &world, BOUNCE_SOUND),
            score_sfx: load_audio_track(&loader, &world, SCORE_SOUND),
        };

        sound
    };

    // Add sound effects to the world. We have to do this in another scope because
    // world won't let us insert new resources as long as `Loader` is borrowed.
    world.insert(sound_effects);
}

Then, we'll need to add the Sounds Resource to our World. Update pong.rs:

use crate::audio::initialise_audio;

impl SimpleState for Pong {
    fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
        // --snip--

        initialise_audio(world);
    }
}

Finally, we'll need our game to include the Audio Bundle. In main.rs:

# extern crate amethyst;
#
# use amethyst::GameDataBuilder;
use amethyst::audio::AudioBundle;

fn main() -> amethyst::Result<()> {
    // --snip--

    let game_data = GameDataBuilder::default()
        // ... other bundles
        .with_bundle(AudioBundle::default())?
        // ... systems
    ;

    // --snip--
# Ok(())
}

Playing the bounce sound

Let's start by creating a function to play the bounce sound. In audio.rs, add:

# extern crate amethyst;
#
use amethyst::{
    assets::AssetStorage,
    audio::{output::Output, Source, SourceHandle},
};
#
# pub struct Sounds {
#     pub score_sfx: SourceHandle,
#     pub bounce_sfx: SourceHandle,
# }
#
pub fn play_bounce_sound(sounds: &Sounds, storage: &AssetStorage<Source>, output: Option<&Output>) {
    if let Some(ref output) = output.as_ref() {
        if let Some(sound) = storage.get(&sounds.bounce_sfx) {
            output.play_once(sound, 1.0);
        }
    }
}

Then, we'll update the Bounce System to play the sound whenever the ball bounces. Update systems/bounce.rs:

use std::ops::Deref;

use amethyst::{
    assets::AssetStorage,
    audio::{output::Output, Source},
    ecs::{Read, ReadExpect},
};

use crate::audio::{play_bounce_sound, Sounds};

impl<'s> System<'s> for BounceSystem {
    type SystemData = (
        WriteStorage<'s, Ball>,
        ReadStorage<'s, Paddle>,
        ReadStorage<'s, Transform>,
        Read<'s, AssetStorage<Source>>,
        ReadExpect<'s, Sounds>,
        Option<Read<'s, Output>>,
    );

    fn run(
        &mut self,
        (mut balls, paddles, transforms, storage, sounds, audio_output): Self::SystemData,
    ) {
        for (ball, transform) in (&mut balls, &transforms).join() {
            // --snip--

            // Bounce at the top or the bottom of the arena.
            if (ball_y <= ball.radius && ball.velocity[1] < 0.0)
                || (ball_y >= ARENA_HEIGHT - ball.radius && ball.velocity[1] > 0.0)
            {
                ball.velocity[1] = -ball.velocity[1];
                play_bounce_sound(&*sounds, &storage, audio_output.as_ref().map(|o| o.deref()));
            }

            // Bounce at the paddles.
            for (paddle, paddle_transform) in (&paddles, &transforms).join() {
                // --snip--

                if point_in_rect(
                    // --snip--
                ) {
                    if (paddle.side == Side::Left && ball.velocity[0] < 0.0)
                        || (paddle.side == Side::Right && ball.velocity[0] > 0.0)
                    {
                        ball.velocity[0] = -ball.velocity[0];
                        play_bounce_sound(&*sounds, &storage, audio_output.as_ref().map(|o| o.deref()));
                    }
                }
            }
        }
    }
}

Now try running your game (cargo run). Don't forget to turn up your volume!

Playing the score sound

Just as we did for the bounce sound, let's create a function to play the score sound. Update audio.rs:

# extern crate amethyst;
#
# use amethyst::{
#     audio::{output::Output, Source, SourceHandle},
#     assets::AssetStorage,
# };
#
# pub struct Sounds {
#     pub score_sfx: SourceHandle,
#     pub bounce_sfx: SourceHandle,
# }
#
pub fn play_score_sound(sounds: &Sounds, storage: &AssetStorage<Source>, output: Option<&Output>) {
    if let Some(ref output) = output.as_ref() {
        if let Some(sound) = storage.get(&sounds.score_sfx) {
            output.play_once(sound, 1.0);
        }
    }
}

Then, we'll update our Winner System to play the score sound whenever a player scores. Update systems/winner.rs:

use amethyst::{
    assets::AssetStorage,
    audio::{output::Output, Source},
    ecs::Read,
};
use crate::audio::{play_score_sound, Sounds};
use std::ops::Deref;

impl<'s> System<'s> for WinnerSystem {
    type SystemData = (
        WriteStorage<'s, Ball>,
        WriteStorage<'s, Transform>,
        WriteStorage<'s, UiText>,
        Write<'s, ScoreBoard>,
        ReadExpect<'s, ScoreText>,
        Read<'s, AssetStorage<Source>>,
        ReadExpect<'s, Sounds>,
        Option<Read<'s, Output>>,
    );


    fn run(&mut self, (
        mut balls,
        mut locals,
        mut ui_text,
        mut scores,
        score_text,
        storage,
        sounds,
        audio_output,
    ): Self::SystemData)  {
        for (ball, transform) in (&mut balls, &mut locals).join() {
            // --snip--

            if did_hit {
                ball.velocity[0] = -ball.velocity[0]; // Reverse Direction
                transform.set_translation_x(ARENA_WIDTH / 2.0); // Reset Position
                play_score_sound(&*sounds, &storage, audio_output.as_ref().map(|o| o.deref()));

                // Print the scoreboard.
                println!(
                    "Score: | {:^3} | {:^3} |",
                    scores.score_left, scores.score_right
                );
            }
        }
    }
}

Now try running your game. Yay, we successfully added sound effects to our game! 🎉

Next, let's take our game to the next level by adding some background music.

Adding background music

Let's start by downloading Albatross and Where's My Jetpack? Put these files in the assets/audio directory.

In audio.rs, add the paths to the music tracks below the paths to the sound effects:

const BOUNCE_SOUND: &str = "audio/bounce.ogg";
const SCORE_SOUND: &str = "audio/score.ogg";

const MUSIC_TRACKS: &[&str] = &[
    "audio/Computer_Music_All-Stars_-_Wheres_My_Jetpack.ogg",
    "audio/Computer_Music_All-Stars_-_Albatross_v2.ogg",
];

Then, create a Music Resource:

# extern crate amethyst;
#
use std::{iter::Cycle, vec::IntoIter};
#
# use amethyst::audio::SourceHandle;

pub struct Music {
    pub music: Cycle<IntoIter<SourceHandle>>,
}

Since we only have two music tracks, we use a Cycle to infinitely alternate between the two.

Next, we need to add the Music Resource to our World. Update initialise_audio:

# extern crate amethyst;
#
# use std::{iter::Cycle, vec::IntoIter};
#
use amethyst::{
    audio::{AudioSink, SourceHandle},
    assets::Loader,
    ecs::{World, WorldExt},
};
#
# const BOUNCE_SOUND: &str = "audio/bounce.ogg";
# const SCORE_SOUND: &str = "audio/score.ogg";
#
# const MUSIC_TRACKS: &[&str] = &[
#     "audio/Computer_Music_All-Stars_-_Wheres_My_Jetpack.ogg",
#     "audio/Computer_Music_All-Stars_-_Albatross_v2.ogg",
# ];
#
# fn load_audio_track(loader: &Loader, world: &World, file: &str) -> SourceHandle {
#     unimplemented!()
# }
#
# pub struct Music {
#     pub music: Cycle<IntoIter<SourceHandle>>,
# }
#
# pub struct Sounds {
#     pub score_sfx: SourceHandle,
#     pub bounce_sfx: SourceHandle,
# }

pub fn initialise_audio(world: &mut World) {
    let (sound_effects, music) = {
        let loader = world.read_resource::<Loader>();

        let mut sink = world.write_resource::<AudioSink>();
        sink.set_volume(0.25); // Music is a bit loud, reduce the volume.

        let music = MUSIC_TRACKS
            .iter()
            .map(|file| load_audio_track(&loader, &world, file))
            .collect::<Vec<_>>()
            .into_iter()
            .cycle();
        let music = Music { music };

        let sound = Sounds {
            bounce_sfx: load_audio_track(&loader, &world, BOUNCE_SOUND),
            score_sfx: load_audio_track(&loader, &world, SCORE_SOUND),
        };

        (sound, music)
    };

    // Add sound effects and music to the world. We have to do this in another scope because
    // world won't let us insert new resources as long as `Loader` is borrowed.
    world.insert(sound_effects);
    world.insert(music);
}

Finally, let's add a DJ System to our game to play the music. In main.rs:

use amethyst::audio::DjSystem;
use crate::audio::Music;

fn main() -> amethyst::Result<()> {
    // --snip--

    let game_data = GameDataBuilder::default()
        // ... bundles
        .with(
            DjSystem::new(|music: &mut Music| music.music.next()),
            "dj_system",
            &[],
        )
        // ... other systems
        ;

    // --snip--
# Ok(())
}

Now run your game and enjoy the tunes!

Tutorial completed!

And... that's where the pong chapter ends. We hope you found it useful!

You can find the entire code with balls, score and music on the example pages available here.

Next up in this book we will explain how Amethyst does Math, Input, Assets and so on. Whenever you have a need for more learning-by-example materials just come back to this page for an overview of available resources.

Other tutorials or examples

Amethyst Quickstarter

Seed project for 2D games. This project template will get you from 0 to drawing something on the screen in no time.

Showcase Game: Evoli

An ecosystem-simulation game, 3D

Showcase Game: Space Menace

An action 2D platformer

Showcase Game: Survivor

(unannounced, 2D)

For more examples from the community you can check out this list of Games made with Amethyst.

Come talk to us

You can get additional help by leaving a post on our forum or on our Discord server. We'd also love to hear your ideas for other tutorials we should consider adding to this book.

If you want to extend this tutorial (e.g., add a main menu, add pause/resume functionality, etc.), feel free to ping us on Discord or in a GitHub issue!

Math

Amethyst uses nalgebra under the hood as its math library and it is re-exported for use under the amethyst::core::math namespace. As the documentation for nalgebra is already very good, we will not go into detail here about how to use it. Instead we will redirect you to the excellent nalgebra website where you can find the documentation for nalgebra along with excellent examples and quick references.

Input

Input provides data on devices like a keyboard, a mouse and a controller. This data most importantly consists of buttons being pressed and buttons or devices being moved.

In games this data is used to activate certain events, for example:

  • Pausing the game when the user presses escape.
  • Moving the player when the user moves the joystick of a controller.

For these events to take place the game needs to check for any buttons that are pressed and take action accordingly.

This section explains how input works in Amethyst and how you can bind actions to user input.

Handling Input

Amethyst uses an InputHandler to handle user input. You initialise this InputHandler by creating an InputBundle and adding it to the game data.

# extern crate amethyst;
use amethyst::{
    prelude::*,
    input::{InputBundle, StringBindings},
};

# struct Example;
# impl SimpleState for Example {}
fn main() -> amethyst::Result<()> {
    // StringBindings is the default BindingTypes
    let input_bundle = InputBundle::<StringBindings>::new();

    let mut world = World::new();
    let game_data = GameDataBuilder::default()
    //..
    .with_bundle(input_bundle)?
    //..
#   ;

    Ok(())
}

To use the InputHandler inside a System you have to add it to the SystemData. With this you can check for events from input devices.

# extern crate amethyst;
use amethyst::{
    prelude::*,
    input::{InputHandler, ControllerButton, VirtualKeyCode, StringBindings},
    core::SystemDesc,
    derive::SystemDesc,
    ecs::{Read, System, SystemData, World},
};

#[derive(SystemDesc)]
struct ExampleSystem;

impl<'s> System<'s> for ExampleSystem {
    // The same BindingTypes from the InputBundle needs to be inside the InputHandler
    type SystemData = Read<'s, InputHandler<StringBindings>>;

    fn run(&mut self, input: Self::SystemData) {
        // Gets mouse coordinates
        if let Some((x, y)) = input.mouse_position() {
            //..
        }
        
        // Gets all connected controllers
        let controllers = input.connected_controllers();
        for controller in controllers {
            // Checks if the A button is down on each connected controller
            let buttonA = input.controller_button_is_down(controller, ControllerButton::A);
            //..
        }

        // Checks if the A button is down on the keyboard
        let buttonA = input.key_is_down(VirtualKeyCode::A);
        //..
    }
}

You can find all the methods from InputHandler here.

Now you have to add the System to the game data, just like you would do with any other System. A System that uses an InputHandler needs "input_system" inside its dependencies.

# extern crate amethyst;
# use amethyst::{prelude::*, ecs::*, core::SystemDesc, derive::SystemDesc};
# #[derive(SystemDesc)]
# struct ExampleSystem; 
# impl<'a> System<'a> for ExampleSystem { type SystemData = (); fn run(&mut self, _: ()) {}}
#
let game_data = GameDataBuilder::default()
    //..
    .with(ExampleSystem, "example_system", &["input_system"])
    //..
#   ;

Defining Key Bindings in a File

Instead of hard coding in all the key bindings, you can store all the bindings in a config file. A config file for key bindings with the RON format looks something like this:

(
    axes: {
        "vertical": Emulated(pos: Key(W), neg: Key(S)),
        "horizontal": Emulated(pos: Key(D), neg: Key(A)),
    },
    actions: {
        "shoot": [[Key(Space)]],
    },
)

The axis values range from -1.0 to 1.0. For an Emulated axis controller such as keyboard buttons, the values are distinct:

  • 0.0 when neither, or both the neg or pos buttons are pressed.
  • -1.0 when the neg button is pressed.
  • 1.0 when the pos button is pressed.

Values between 0.0 and 1.0 are possible when using a controller such as a joystick. This can be enabled via the "sdl_controller" feature.

The action is a boolean, which is set to true when the buttons are pressed. The action binding is defined by a two-level array:

  • The inner array specifies the buttons that must be pressed at the same time to send the action.
  • The outer array specifies different combinations of those buttons that send the action.

The possible inputs you can specify for axes are listed here. The possible inputs you can specify for actions are listed here.

To add these bindings to the InputBundle you simply need to call the with_bindings_from_file function on the InputBundle.

# extern crate amethyst;
# use amethyst::{prelude::*, input::*, utils::*};
# fn main() -> amethyst::Result::<()> {
let root = application_root_dir()?;
let bindings_config = root.join("config").join("bindings.ron");

let input_bundle = InputBundle::<StringBindings>::new()
    .with_bindings_from_file(bindings_config)?;

//..
# Ok(()) }

And now you can get the axis and action values from the InputHandler.

# extern crate amethyst;
use amethyst::{
    prelude::*,
    core::{Transform, SystemDesc},
    derive::SystemDesc,
    ecs::{Component, DenseVecStorage, Join, Read, ReadStorage, System, SystemData, World, WriteStorage},
    input::{InputHandler, StringBindings},
};

struct Player {
    id: usize,
}

impl Player {
    pub fn shoot(&self) {
        println!("PEW! {}", self.id);
    }
}

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

#[derive(SystemDesc)]
struct MovementSystem;

impl<'s> System<'s> for MovementSystem {
    type SystemData = (
        WriteStorage<'s, Transform>,
        ReadStorage<'s, Player>,
        Read<'s, InputHandler<StringBindings>>,
    );
    
    fn run(&mut self, (mut transforms, players, input): Self::SystemData) {
        for (player, transform) in (&players, &mut transforms).join() {
            let horizontal = input.axis_value("horizontal").unwrap_or(0.0);
            let vertical = input.axis_value("vertical").unwrap_or(0.0);
            
            let shoot = input.action_is_down("shoot").unwrap_or(false);
            
            transform.move_up(horizontal);
            transform.move_right(vertical);
            
            if shoot {
                player.shoot();
            }
        }
    }
}

How to Define Custom Control Bindings

Instead of using StringBindings for an InputBundle you probably want to use a custom type in production, as StringBindings are mainly meant to be used for prototyping and not very efficient.

Using a custom type to handle input instead of using String has many advantages:

  • A String uses quite a lot of memory compared to something like an enum.
  • Inputting a String when retrieving input data is error-prone if you mistype it or change the name.
  • A custom type can hold additional information.

Defining Custom Input BindingTypes

Defining a custom type for the InputBundle is done by implementing the BindingTypes trait. This trait contains two types, an Axis type and an Action type. These types are usually defined as enums.

# extern crate amethyst;
# extern crate serde;
use std::fmt::{self, Display};

use amethyst::input::{BindingTypes, Bindings};
use serde::{Serialize, Deserialize};

#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
enum AxisBinding {
    Horizontal,
    Vertical,
}

#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
enum ActionBinding {
    Shoot,
}

impl Display for AxisBinding {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:?}", self)
    }
}

impl Display for ActionBinding {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:?}", self)
    }
}

#[derive(Debug)]
struct MovementBindingTypes;

impl BindingTypes for MovementBindingTypes {
    type Axis = AxisBinding;
    type Action = ActionBinding;
}

The Axis and Action type both need to derive all the traits listed above, the first five are used by Amethyst and the last two are for reading and writing to files correctly. They also need to implement Display if you want to add a bindings config file.

For serializing and deserializing you need to add serde to the dependencies like this:

serde = { version = "1", features = ["derive"] }

If you want to add additional information you can add it to the enum or change the Axis and Action types to a struct. For example:

#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
enum AxisBinding {
    Horizontal(usize),
    Vertical(usize),
}

#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
enum ActionBinding {
    Shoot(usize),
}

//..

We can now use this custom type in our InputBundle and create a RON config file for our bindings.

The config file might look something like this:

(
    axes: {
        Vertical(0): Emulated(pos: Key(W), neg: Key(S)),
        Horizontal(0): Emulated(pos: Key(D), neg: Key(A)),
        Vertical(1): Emulated(pos: Key(Up), neg: Key(Down)),
        Horizontal(1): Emulated(pos: Key(Right), neg: Key(Left)),
    },
    actions: {
        Shoot(0): [[Key(Space)]],
        Shoot(1): [[Key(Return)]],
    },
)

Here the number after the binding type could be the ID of the player, but you can supply any other data as long as it derives the right traits.

With the config file we can create an InputBundle like in the previous section.

# extern crate amethyst;
# use amethyst::input::StringBindings as MovementBindingTypes;
use amethyst::input::InputBundle;

# fn main() -> amethyst::Result<()> {
#
# let input_config = "input.ron";
#
let input_bundle = 
    InputBundle::<MovementBindingTypes>::new()
        .with_bindings_from_file(input_config)?;
#
# Ok(())
# }

And add the InputBundle to the game data just like before.

# extern crate amethyst;
# use amethyst::prelude::*;
# use amethyst::input::{InputBundle, StringBindings};
#
# fn main() -> amethyst::Result<()> {
# let input_bundle = InputBundle::<StringBindings>::default();
#
let mut world = World::new();
let game_data = GameDataBuilder::default()
    //..
    .with_bundle(input_bundle)?
    //..
#   ;
#
# Ok(())
# }

Using the InputHandler with a Custom BindingTypes

Now that we have added an InputBundle with a custom BindingTypes, we can use the InputHandler just like with StringBindings, but instead of using Strings we use our custom enums.

# extern crate amethyst;
use amethyst::{
    core::{Transform, SystemDesc},
    derive::SystemDesc,
    ecs::{Component, DenseVecStorage, Join, Read, ReadStorage, System, SystemData, World, WriteStorage},
    input::{AxisBinding, InputHandler},
};

struct Player {
    id: usize,
}

impl Player {
    pub fn shoot(&self) {
        println!("PEW! {}", self.id);
    }
}

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

#[derive(SystemDesc)]
struct MovementSystem;

impl<'s> System<'s> for MovementSystem {
    type SystemData = (
        WriteStorage<'s, Transform>,
        ReadStorage<'s, Player>,
        Read<'s, InputHandler<MovementBindingTypes>>,
    );

    fn run(&mut self, (mut transform, player, input): Self::SystemData) {
        for (player, transform) in (&player, &mut transform).join() {
            let horizontal = input
                .axis_value(&AxisBinding::Horizontal(player.id))
                .unwrap_or(0.0);
            let vertical = input
                .axis_value(&AxisBinding::Vertical(player.id))
                .unwrap_or(0.0);
            
            let shoot = input
                .action_is_down(&ActionBinding::Shoot(player.id))
                .unwrap_or(false);
            
            transform.move_up(horizontal);
            transform.move_right(vertical);
            
            if shoot {
                player.shoot();
            }
        }
    }
}

And don't forget to add the MovementSystem to the game data.

# extern crate amethyst;
# use amethyst::prelude::*;
# use amethyst::ecs::*;
# use amethyst::core::SystemDesc;
# use amethyst::derive::SystemDesc;
# #[derive(SystemDesc)]
# struct MovementSystem;
# impl<'a> System<'a> for MovementSystem {type SystemData=(); fn run(&mut self, _: ()) {}}
let game_data = GameDataBuilder::default()
//..
    .with(MovementSystem, "movement_system", &["input_system"])
//..
#   ;

Assets

Assets are data that's loaded by a game when it is run. These may be textures, sounds, game level scripts, and so on. In fact, any data that is loaded at runtime may be considered an asset. These are usually stored as files, and distributed alongside the game.

When used well, assets enhance the gaming experience. For example, in an asteroid shooter, when a bullet hits an asteroid we can do the following:

  • Draw broken pieces of the asteroid falling away.
  • Display a fireball animation.
  • Play an explosion sound.

Handles

In a game, the same asset may be used by different game objects. For example, a fireball texture asset can be used by many different objects that shoot fireballs. Loading the texture mutiple times is an inefficient use of memory; loading it once, and using references to the same loaded asset is much more efficient. We call these references, handles.

Formats

A format is a way of encoding the information of an asset so that it can be stored and read later. For example, a texture may be stored as a Bitmap (BMP), Portable Network Graphic (PNG), or Targa (TGA). Game levels can be stored using RON, JSON, TOML or any other suitable encoding.

Each format has its own strengths and weaknesses. For example, RON has direct mapping from the storage format to the in-memory object type. JSON is widely used, so it is easy to find a JSON parser in any programming language. TOML is easier for people to read.

How to Use Assets

This guide covers the basic usage of assets into Amethyst for existing supported formats. For a list of supported formats, please use this search for "Format" in the API documentation, and filter by the following crates:

  • amethyst_assets
  • amethyst_audio
  • amethyst_gltf
  • amethyst_locale
  • amethyst_ui

Steps

  1. Instantiate the Amethyst application with the assets directory.

    # extern crate amethyst;
    #
    use amethyst::{
        prelude::*,
    #   ecs::{World, WorldExt},
        utils::application_root_dir,
    };
    #
    # pub struct LoadingState;
    # impl SimpleState for LoadingState {}
    
    fn main() -> amethyst::Result<()> {
        // Sets up the application to read assets in
        // `<app_dir>/assets`
        let app_root = application_root_dir()?;
        let assets_dir = app_root.join("assets");
    
        //..
    #   let world = World::new();
    #   let game_data = GameDataBuilder::default();
    
        let mut game = Application::new(assets_dir, LoadingState, game_data)?;
    #
    #   game.run();
    #   Ok(())
    }
    
  2. Ensure that the Processor<A> system for asset type A is registered in the dispatcher.

    For asset type A, Processor<A> is a System that will asynchronously load A assets. Usually the crate that provides A will also register Processor<A> through a SystemBundle. Examples:

    • FontAsset is provided by amethyst_ui, UiBundle registers Processor<FontAsset>.
    • Source is provided by amethyst_audio, AudioBundle registers Processor<Source>.
    • SpriteSheet is not added by a bundle, so Processor<SpriteSheet> needs to be added to the builder.
  3. Use the Loader resource to load the asset.

    # extern crate amethyst;
    # use amethyst::{
    #     assets::{AssetStorage, Handle, Loader, ProgressCounter},
    #     ecs::{World, WorldExt},
    #     prelude::*,
    #     renderer::{formats::texture::ImageFormat, Texture},
    #     utils::application_root_dir,
    # };
    #
    pub struct LoadingState {
        /// Tracks loaded assets.
        progress_counter: ProgressCounter,
        /// Handle to the player texture.
        texture_handle: Option<Handle<Texture>>,
    }
    
    impl SimpleState for LoadingState {
        fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
            let loader = &data.world.read_resource::<Loader>();
            let texture_handle = loader.load(
                "player.png",
                ImageFormat::default(),
                &mut self.progress_counter,
                &data.world.read_resource::<AssetStorage<Texture>>(),
            );
    
            self.texture_handle = Some(texture_handle);
        }
    }
    #
    # fn main() -> amethyst::Result<()> {
    #   let app_root = application_root_dir()?;
    #   let assets_dir = app_root.join("assets");
    #
    #   let game_data = GameDataBuilder::default();
    #   let mut game = Application::new(
    #       assets_dir,
    #       LoadingState {
    #           progress_counter: ProgressCounter::new(),
    #           texture_handle: None,
    #       },
    #       game_data,
    #   )?;
    #
    #   game.run();
    #   Ok(())
    # }
    
  4. Wait for the asset to be loaded.

    When loader.load(..) is used to load an Asset, the method returns immediately with a handle for the asset. The asset loading is handled asynchronously in the background, so if the handle is used to retrieve the asset, such as with world.read_resource::<AssetStorage<Texture>>().get(texture_handle), it will return None until the Texture has finished loading.

    # extern crate amethyst;
    # use amethyst::{
    #     assets::{Handle, ProgressCounter},
    #     prelude::*,
    #     renderer::Texture,
    # };
    #
    # pub struct GameState {
    #     /// Handle to the player texture.
    #     texture_handle: Handle<Texture>,
    # }
    #
    # impl SimpleState for GameState {}
    #
    # pub struct LoadingState {
    #     /// Tracks loaded assets.
    #     progress_counter: ProgressCounter,
    #     /// Handle to the player texture.
    #     texture_handle: Option<Handle<Texture>>,
    # }
    #
    impl SimpleState for LoadingState {
        fn update(
            &mut self,
            _data: &mut StateData<'_, GameData<'_, '_>>,
        ) -> SimpleTrans {
            if self.progress_counter.is_complete() {
                Trans::Switch(Box::new(GameState {
                    texture_handle: self.texture_handle
                        .take()
                        .expect(
                            "Expected `texture_handle` to exist when \
                            `progress_counter` is complete."
                        ),
                }))
            } else {
                Trans::None
            }
        }
    }
    

    The asset handle can now be used:

    # extern crate amethyst;
    # use amethyst::{
    #     assets::Handle,
    #     prelude::*,
    #     renderer::Texture,
    # };
    #
    # pub struct GameState {
    #     /// Handle to the player texture.
    #     texture_handle: Handle<Texture>,
    # }
    #
    impl SimpleState for GameState {
        fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
            // Create the player entity.
            data.world
                .create_entity()
                // Use the texture handle as a component
                .with(self.texture_handle.clone())
                .build();
        }
    }
    

How to Define Custom Assets

This guide explains how to define a new asset type to be used in an Amethyst application. If you are defining a new asset type that may be useful to others, please send us a PR!

  1. Define the type and handle for your asset.

    # extern crate amethyst;
    # extern crate serde_derive;
    #
    use amethyst::{
        assets::Handle,
        ecs::VecStorage,
    };
    
    /// Custom asset representing an energy blast.
    #[derive(Clone, Debug, Default, PartialEq, Eq)]
    pub struct EnergyBlast {
        /// How much HP to subtract.
        pub hp_damage: u32,
        /// How much MP to subtract.
        pub mp_damage: u32,
    }
    
    /// A handle to a `EnergyBlast` asset.
    pub type EnergyBlastHandle = Handle<EnergyBlast>;
    
  2. Define the type that represents the serializable form of the asset.

    The serializable type can be one of:

    • The asset type itself, in which case you simply derive Serialize and Deserialize on the type:

      #[derive(Serialize, Deserialize, ..)]
      pub struct EnergyBlast { .. }
      
    • An enum with different variants – each for a different data layout:

      # extern crate serde_derive;
      #
      # use serde_derive::{Deserialize, Serialize};
      
      /// Separate serializable type to support different versions
      /// of energy blast configuration.
      #[derive(Clone, Debug, Deserialize, Serialize)]
      pub enum EnergyBlastData {
          /// Early version only could damage HP.
          Version1 { hp_damage: u32 },
          /// Add support for subtracting MP.
          Version2 { hp_damage: u32, mp_damage: u32 },
      }
      
  3. Implement the Asset trait on the asset type.

    # extern crate amethyst;
    # extern crate serde_derive;
    #
    # use amethyst::{
    #     assets::{Asset, Handle},
    #     ecs::VecStorage,
    # };
    # use serde_derive::{Deserialize, Serialize};
    #
    # /// Custom asset representing an energy blast.
    # #[derive(Clone, Debug, Default, PartialEq, Eq)]
    # pub struct EnergyBlast {
    #     /// How much HP to subtract.
    #     pub hp_damage: u32,
    #     /// How much MP to subtract.
    #     pub mp_damage: u32,
    # }
    #
    impl Asset for EnergyBlast {
        const NAME: &'static str = "my_crate::EnergyBlast";
        // use `Self` if the type is directly serialized.
        type Data = EnergyBlastData;
        type HandleStorage = VecStorage<EnergyBlastHandle>;
    }
    #
    # /// A handle to a `EnergyBlast` asset.
    # pub type EnergyBlastHandle = Handle<EnergyBlast>;
    #
    # /// Separate serializable type to support different versions
    # /// of energy blast configuration.
    # #[derive(Clone, Debug, Deserialize, Serialize)]
    # pub enum EnergyBlastData {
    #     /// Early version only could damage HP.
    #     Version1 { hp_damage: u32 },
    #     /// Add support for subtracting MP.
    #     Version2 { hp_damage: u32, mp_damage: u32 },
    # }
    
  4. Implement the conversion function for A::Data into a ProcessingState<A> result.

    The Processor<A> system uses this function to convert the deserialized asset data into the asset.

    # extern crate amethyst;
    # extern crate serde_derive;
    #
    # use amethyst::{
    #     error::Error,
    #     assets::{Asset, Handle, ProcessingState},
    #     ecs::VecStorage,
    # };
    # use serde_derive::{Deserialize, Serialize};
    #
    # /// Custom asset representing an energy blast.
    # #[derive(Clone, Debug, Default, PartialEq, Eq)]
    # pub struct EnergyBlast {
    #     /// How much HP to subtract.
    #     pub hp_damage: u32,
    #     /// How much MP to subtract.
    #     pub mp_damage: u32,
    # }
    #
    # /// A handle to a `EnergyBlast` asset.
    # pub type EnergyBlastHandle = Handle<EnergyBlast>;
    #
    # impl Asset for EnergyBlast {
    #     const NAME: &'static str = "my_crate::EnergyBlast";
    #     // use `Self` if the type is directly serialized.
    #     type Data = EnergyBlastData;
    #     type HandleStorage = VecStorage<EnergyBlastHandle>;
    # }
    #
    # /// Separate serializable type to support different versions
    # /// of energy blast configuration.
    # #[derive(Clone, Debug, Deserialize, Serialize)]
    # pub enum EnergyBlastData {
    #     /// Early version only could damage HP.
    #     Version1 { hp_damage: u32 },
    #     /// Add support for subtracting MP.
    #     Version2 { hp_damage: u32, mp_damage: u32 },
    # }
    #
    impl From<EnergyBlastData> for Result<ProcessingState<EnergyBlast>, Error> {
        fn from(energy_blast_data: EnergyBlastData)
            -> Result<ProcessingState<EnergyBlast>, Error> {
    
            match energy_blast_data {
                EnergyBlastData::Version1 { hp_damage } => {
                    Ok(ProcessingState::Loaded(EnergyBlast {
                        hp_damage,
                        ..Default::default()
                    }))
                }
                EnergyBlastData::Version2 { hp_damage, mp_damage } => {
                    Ok(ProcessingState::Loaded(EnergyBlast {
                        hp_damage,
                        mp_damage,
                    }))
                }
            }
        }
    }
    

    If your asset is stored using one of the existing supported formats such as RON or JSON, it can now be used:

    # extern crate amethyst;
    # extern crate serde_derive;
    #
    # use amethyst::{
    #     error::Error,
    #     assets::{AssetStorage, Loader, ProcessingState, ProgressCounter, RonFormat},
    #     ecs::{World, WorldExt},
    #     prelude::*,
    #     utils::application_root_dir,
    # };
    # use serde_derive::{Deserialize, Serialize};
    #
    # use amethyst::{
    #     assets::{Asset, Handle},
    #     ecs::VecStorage,
    # };
    #
    # /// Custom asset representing an energy blast.
    # #[derive(Clone, Debug, Default, PartialEq, Eq)]
    # pub struct EnergyBlast {
    #     /// How much HP to subtract.
    #     pub hp_damage: u32,
    #     /// How much MP to subtract.
    #     pub mp_damage: u32,
    # }
    #
    # /// A handle to a `EnergyBlast` asset.
    # pub type EnergyBlastHandle = Handle<EnergyBlast>;
    #
    # /// Separate serializable type to support different versions
    # /// of energy blast configuration.
    # #[derive(Clone, Debug, Deserialize, Serialize)]
    # pub enum EnergyBlastData {
    #     /// Early version only could damage HP.
    #     Version1 { hp_damage: u32 },
    #     /// Add support for subtracting MP.
    #     Version2 { hp_damage: u32, mp_damage: u32 },
    # }
    #
    # impl Asset for EnergyBlast {
    #     const NAME: &'static str = "my_crate::EnergyBlast";
    #     // use `Self` if the type is directly serialized.
    #     type Data = EnergyBlastData;
    #     type HandleStorage = VecStorage<EnergyBlastHandle>;
    # }
    #
    # impl From<EnergyBlastData> for Result<ProcessingState<EnergyBlast>, Error> {
    #     fn from(energy_blast_data: EnergyBlastData)
    #         -> Result<ProcessingState<EnergyBlast>, Error> {
    #
    #         use self::EnergyBlastData::*;
    #         match energy_blast_data {
    #             Version1 { hp_damage } => {
    #                 Ok(ProcessingState::Loaded(EnergyBlast {
    #                     hp_damage,
    #                     ..Default::default()
    #                 }))
    #             }
    #             Version2 { hp_damage, mp_damage } => {
    #                 Ok(ProcessingState::Loaded(EnergyBlast {
    #                     hp_damage,
    #                     mp_damage,
    #                 }))
    #             }
    #         }
    #     }
    # }
    #
    # pub struct LoadingState {
    #     /// Tracks loaded assets.
    #     progress_counter: ProgressCounter,
    #     /// Handle to the energy blast.
    #     energy_blast_handle: Option<EnergyBlastHandle>,
    # }
    #
    impl SimpleState for LoadingState {
        fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
            let loader = &data.world.read_resource::<Loader>();
            let energy_blast_handle = loader.load(
                "energy_blast.ron",
                RonFormat,
                &mut self.progress_counter,
                &data.world.read_resource::<AssetStorage<EnergyBlast>>(),
            );
    
            self.energy_blast_handle = Some(energy_blast_handle);
        }
    }
    #
    # fn main() -> amethyst::Result<()> {
    #   let app_root = application_root_dir()?;
    #   let assets_dir = app_root.join("assets");
    #
    #   let game_data = GameDataBuilder::default();
    #   let mut game = Application::new(
    #       assets_dir,
    #       LoadingState {
    #           progress_counter: ProgressCounter::new(),
    #           energy_blast_handle: None,
    #       },
    #       game_data,
    #   )?;
    #
    #   game.run();
    #   Ok(())
    # }
    

    If the asset data is stored in a format that is not supported by Amethyst, a custom format can be implemented and provided to the Loader to load the asset data.

How to Define Custom Formats

This guide explains how to define a new asset format. This will allow Amethyst to load assets stored in a particular encoding.

There is a trait in Amethyst for implementing a format: Format<A: Asset::Data>. Format provides a loading implementation that provides detection when an asset should be reloaded for hot reloading; you don't need to implement it since it has a default implementation. A blanket implementation will implement Format::import and we only need to implement Format::import_simple.

Format takes a type parameter for the asset data type it supports. This guide covers a type parameterized implementation of Format<D> where D is an arbitrary Asset::Data, so we can reuse it for any asset which can be loaded from deserializable asset data.

If you are defining a new format that may be useful to others, please send us a PR!

  1. Define a struct that represents the format.

    In most cases a unit struct is sufficient. When possible, this should implement Clone and Copy for ergonomic usage.

    /// Format for loading from `.mylang` files.
    #[derive(Clone, Copy, Debug, Default)]
    pub struct MyLangFormat;
    
  2. Implement the Format trait.

    This is where the logic to deserialize the asset data type is provided. Fields of the format struct can be used to specify additional parameters for deserialization; use a unit struct if this is not needed.

    In this example the RON deserializer is used, though it is already a supported format.

    # extern crate amethyst;
    # extern crate ron;
    # extern crate serde;
    #
    use amethyst::{
        error::Error,
        assets::{Asset, Format},
    };
    use serde::Deserialize;
    use ron::de::Deserializer; // Replace this in your implementation.
    
    /// Format for loading from `.mylang` files.
    #[derive(Clone, Copy, Debug, Default)]
    pub struct MyLangFormat;
    
    impl<D> Format<D> for MyLangFormat
    where
        D: for<'a> Deserialize<'a> + Send + Sync + 'static,
    {
        fn name(&self) -> &'static str {
            "MyLangFormat"
        }
    
        fn import_simple(&self, bytes: Vec<u8>) -> Result<D, Error> {
            let mut deserializer = Deserializer::from_bytes(&bytes)?;
            let val = D::deserialize(&mut deserializer)?;
            deserializer.end()?;
    
            Ok(val)
        }
    }
    

    The custom format can now be used:

    # extern crate amethyst;
    # extern crate ron;
    # extern crate serde;
    # extern crate serde_derive;
    #
    # use amethyst::{
    #     error::Error,
    #     assets::{
    #         Asset, AssetStorage, Handle, Loader, Processor, ProgressCounter,
    #         ProcessingState, Format,
    #     },
    #     ecs::{VecStorage, World, WorldExt},
    #     prelude::*,
    #     utils::application_root_dir,
    # };
    # use ron::de::Deserializer;
    # use serde::Deserialize as DeserializeTrait;
    # use serde_derive::{Deserialize, Serialize};
    #
    # /// Custom asset representing an energy blast.
    # #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
    # pub struct EnergyBlast {
    #     /// How much HP to subtract.
    #     pub hp_damage: u32,
    #     /// How much MP to subtract.
    #     pub mp_damage: u32,
    # }
    #
    # /// A handle to a `EnergyBlast` asset.
    # pub type EnergyBlastHandle = Handle<EnergyBlast>;
    #
    # impl Asset for EnergyBlast {
    #     const NAME: &'static str = "my_crate::EnergyBlast";
    #     type Data = Self;
    #     type HandleStorage = VecStorage<EnergyBlastHandle>;
    # }
    #
    # impl From<EnergyBlast> for Result<ProcessingState<EnergyBlast>, Error> {
    #     fn from(energy_blast: EnergyBlast) -> Result<ProcessingState<EnergyBlast>, Error> {
    #       Ok(ProcessingState::Loaded(energy_blast))
    #     }
    # }
    #
    # pub struct LoadingState {
    #     /// Tracks loaded assets.
    #     progress_counter: ProgressCounter,
    #     /// Handle to the energy blast.
    #     energy_blast_handle: Option<EnergyBlastHandle>,
    # }
    #
    # /// Format for loading from `.mylang` files.
    #  #[derive(Clone, Copy, Debug, Default)]
    #  pub struct MyLangFormat;
    #
    #  impl<D> Format<D> for MyLangFormat
    #  where
    #      D: for<'a> DeserializeTrait<'a> + Send + Sync + 'static,
    #  {
    #      fn name(&self) -> &'static str {
    #          "MyLangFormat"
    #      }
    #
    #      fn import_simple(&self, bytes: Vec<u8>) -> Result<D, Error> {
    #          let mut deserializer = Deserializer::from_bytes(&bytes)?;
    #          let val = D::deserialize(&mut deserializer)?;
    #          deserializer.end()?;
    #
    #          Ok(val)
    #      }
    #  }
    #
    impl SimpleState for LoadingState {
        fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
            let loader = &data.world.read_resource::<Loader>();
            let energy_blast_handle = loader.load(
                "energy_blast.mylang",
                MyLangFormat,
                &mut self.progress_counter,
                &data.world.read_resource::<AssetStorage<EnergyBlast>>(),
            );
    
            self.energy_blast_handle = Some(energy_blast_handle);
        }
    #
    #     fn update(
    #         &mut self,
    #         _data: &mut StateData<'_, GameData<'_, '_>>,
    #     ) -> SimpleTrans {
    #         Trans::Quit
    #     }
    }
    #
    # fn main() -> amethyst::Result<()> {
    #     amethyst::start_logger(Default::default());
    #     let app_root = application_root_dir()?;
    #     let assets_dir = app_root.join("assets");
    #
    #     let game_data = GameDataBuilder::default()
    #         .with(Processor::<EnergyBlast>::new(), "", &[]);
    #     let mut game = Application::new(
    #         assets_dir,
    #         LoadingState {
    #             progress_counter: ProgressCounter::new(),
    #             energy_blast_handle: None,
    #         },
    #         game_data,
    #     )?;
    #
    #     game.run();
    #     Ok(())
    # }
    

Prefabs

Premade fabrications, or prefabs, are templates that specify components to attach to an entity. For example, imagine a monster entity has the following components:

  • Position
  • Velocity
  • Texture
  • Health points
  • Attack damage

It is certainly possible to define the values for each monster in code. However, if the components are initialized in code, then the executable needs to be recompiled whenever the component values are changed. Waiting a number of minutes to every time a small change is made is both inefficient and frustrating.

In data-oriented design, instantiating monsters is logic and is part of the executable, but the values to use for the components is data. The executable can instantiate any monster using the data for the monsters read from a prefab file like the following:

// monster_weak.ron
//
// This is simply an example of what a prefab can look like.
// Other game engines may store prefabs in binary formats which require
// an editor to read and update.

#![enable(implicit_some)]
Prefab (
    entities: [
        (
            data: (
                position: (0.0, 0.0, 0.0),
                velocity: (0.0, 0.0, 0.0),
                texture: Asset(File("textures/monster.png", PngFormat, ())),
                health: 100,
                attack: 10,
            ),
        ),
    ],
)

The prefab is distributed alongside the executable as part of the game.

Uses

Prefabs have the following properties:

  • All entity instances created based on that prefab will receive changes made on the prefab.
  • Prefabs may nest other prefabs, allowing larger prefabs to be composed of other smaller prefabs.

These make prefabs ideal to use to define scenes or levels:

  • City prefab composed of terrain, buildings, and foliage prefabs.
  • Maze prefab composed of walls, a player, and monster prefabs.

Prefabs in Amethyst

Note: This page assumes you have read and understood assets.

Many game engines – including Amethyst – treat prefabs as assets, and so they are usually stored as files and loaded at runtime. After loading the asset(s), prefabs have additional processing to turn them into Components and attach them to entities.

Representation

There are two representations of a prefab:

  • Stored representation, distributed alongside the application.
  • Loaded representation, used at runtime to instantiate entities with components.

The remainder of this page explains these at a conceptual level; subsequent pages contain guides on how Amethyst applies this at a code level.

The Basics

Note: The prefab examples on this page include the PrefabData type names. These are written out for clarity. However, as per the RON specification, these are not strictly required.

In its stored form, a prefab is a serialized list of entities and their components that should be instantiated together. To begin, we will look at a simple prefab that attaches a simple component to a single entity. We will use the following Position component:

# extern crate amethyst;
# extern crate derivative;
# extern crate serde;
#
# use amethyst::{
#     assets::{Prefab, PrefabData},
#     derive::PrefabData,
#     ecs::{
#         storage::DenseVecStorage,
#         Component, Entity, WriteStorage,
#     },
#     prelude::*,
#     Error,
# };
# use derivative::Derivative;
# use serde::{Deserialize, Serialize};
#
#[derive(Clone, Copy, Component, Debug, Default, Deserialize, Serialize, PrefabData)]
#[prefab(Component)]
# #[serde(deny_unknown_fields)]
pub struct Position(pub f32, pub f32, pub f32);

The important derives are the Component and PrefabDataComponent means it can be attached to an entity; PrefabData means it can be loaded as part of a prefab. The #[prefab(Component)] attribute informs the PrefabData derive that this type is a Component, as opposed to being composed of fields which implement PrefabData. This will only be important when implementing a custom prefab.

Here is an example .ron file of a prefab with an entity with a Position:

#![enable(implicit_some)]
Prefab(
    entities: [
        PrefabEntity(
            // parent: None // Optional
            data: Position(1.0, 2.0, 3.0),
        ),
    ],
)

The top level type is a Prefab, and holds a list of entities. These are not the Entity type used at runtime, but the PrefabEntity type – a template for what Components to attach to entities at runtime. Each of these holds two pieces of information:

  • data: Specifies the Components to attach to the entity.

    This must be a type that implements PrefabData. When this prefab is instantiated, it will attach a Position component to the entity.

  • parent: (Optional) index of this entity's Parent entity. The value is the index of the parent entity which resides within this prefab file.

When we load this prefab, the prefab entity is read as:

PrefabEntity { parent: None, data: Some(Position(1.0, 2.0, 3.0)) }

Next, we create an entity with the prefab handle, Handle<Prefab<Position>>:

EntityHandle<Prefab<Position>>
Entity(0, Generation(1))Handle { id: 0 }

In the background, the PrefabLoaderSystem will run, and attach the Position component:

EntityHandle<Prefab<Position>>Position
Entity(0, Generation(1))Handle { id: 0 }Position(1.0, 2.0, 3.0)

This can be seen by running the prefab_basic example from the Amethyst repository:

cargo run --example prefab_basic

Multiple Components

If there are multiple components to be attached to the entity, then we need a type that aggregates the Components:

# extern crate amethyst;
# extern crate derivative;
# extern crate serde;
#
# use amethyst::{
#     assets::{Prefab, PrefabData, ProgressCounter},
#     core::Named,
#     derive::PrefabData,
#     ecs::{
#         storage::{DenseVecStorage, VecStorage},
#         Component, Entity, WriteStorage,
#     },
#     prelude::*,
#     Error,
# };
# use derivative::Derivative;
# use serde::{Deserialize, Serialize};
#
# #[derive(Clone, Copy, Component, Debug, Default, Deserialize, Serialize, PrefabData)]
# #[prefab(Component)]
# #[serde(deny_unknown_fields)]
# pub struct Position(pub f32, pub f32, pub f32);
#
#[derive(Debug, Deserialize, Serialize, PrefabData)]
# #[serde(deny_unknown_fields)]
pub struct Player {
    player: Named,
    position: Position,
}

Here, the Player type is not a Component, but it does implement PrefabData. Each of its fields is a PrefabData as well as a Component.

The corresponding prefab file is written as follows:

#![enable(implicit_some)]
Prefab(
    entities: [
        PrefabEntity(
            data: Player(
                player: Named(name: "Zero"),
                position: Position(1.0, 2.0, 3.0),
            ),
        ),
    ],
)

When an entity is created with this prefab, Amethyst will recurse into each of the prefab data fields – Named and Position – to attach their respective components to the entity.

Now, when we create an entity with the prefab handle, both components will be attached:

Handle<Prefab<Player>>PositionPlayer
Handle { id: 0 }Position(1.0, 2.0, 3.0)Named { name: "Zero" }

This can be seen by running the prefab_multi example from the Amethyst repository:

cargo run --example prefab_multi

Multiple Entities, Different Components

The next level is to instantiate multiple entities, each with their own set of Components. The current implementation of Prefab requires the data field to be the same type for every PrefabEntity in the list. This means that to have different types of entity in the same prefab they must be variants of an enum. For instance, a prefab like this:

#![enable(implicit_some)]
Prefab(
    entities: [
        // Player
        PrefabEntity(
            data: Player(
                player: Named(name: "Zero"),
                position: Position(1.0, 2.0, 3.0),
            ),
        ),
        // Weapon
        PrefabEntity(
            parent: 0,
            data: Weapon(
                weapon_type: Sword,
                position: Position(4.0, 5.0, 6.0),
            ),
        ),
    ],
)

Could be implemented using an enum like this:

# extern crate amethyst;
# extern crate derivative;
# extern crate serde;
#
# use amethyst::{
#     assets::{Prefab, PrefabData, ProgressCounter},
#     core::Named,
#     derive::PrefabData,
#     ecs::{
#         storage::{DenseVecStorage, VecStorage},
#         Component, Entity, WriteStorage,
#     },
#     prelude::*,
#     utils::application_root_dir,
#     Error,
# };
# use derivative::Derivative;
# use serde::{Deserialize, Serialize};
#
# #[derive(Clone, Copy, Component, Debug, Default, Deserialize, Serialize, PrefabData)]
# #[prefab(Component)]
# #[serde(deny_unknown_fields)]
# pub struct Position(pub f32, pub f32, pub f32);
#
#[derive(Clone, Copy, Component, Debug, Derivative, Deserialize, Serialize, PrefabData)]
#[derivative(Default)]
#[prefab(Component)]
#[storage(VecStorage)]
pub enum Weapon {
    #[derivative(Default)]
    Axe,
    Sword,
}

#[derive(Debug, Deserialize, Serialize, PrefabData)]
#[serde(deny_unknown_fields)]
pub enum CustomPrefabData {
    Player {
        name: Named,
        position: Option<Position>,
    },
    Weapon {
        weapon_type: Weapon,
        position: Option<Position>,
    },
}

When we run this, we start off by creating one entity:

EntityHandle<Prefab<CustomPrefabData>>>
Entity(0, Generation(1))Handle { id: 0 }

When the PrefabLoaderSystem runs, this becomes the following:

EntityHandle<Prefab<CustomPrefabData>>>ParentPositionPlayerWeapon
Entity(0, Generation(1))Handle { id: 0 }NonePosition(1.0, 2.0, 3.0)Named { name: "Zero" }None
Entity(1, Generation(1))NoneEntity(0, Generation(1))Position(4.0, 5.0, 6.0)NoneSword
  • The entity that the Handle<Prefab<T>> is attached will be augmented with Components from the first PrefabEntity.
  • A new entity is created for subsequent PrefabEntity entries in the entities list.

Note that the Weapon has a parent with index 0. Let's see what happens when multiple entities are created with this prefab. First, two entities are created with the prefab handle:

EntityHandle<Prefab<CustomPrefabData>>>
Entity(0, Generation(1))Handle { id: 0 }
Entity(1, Generation(1))Handle { id: 0 }

Next, the PrefabLoaderSystem runs and creates and augments the entities:

EntityHandle<Prefab<CustomPrefabData>>>ParentPositionPlayerWeapon
Entity(0, Generation(1))Handle { id: 0 }NonePosition(1.0, 2.0, 3.0)Named { name: "Zero" }None
Entity(1, Generation(1))Handle { id: 0 }NonePosition(1.0, 2.0, 3.0)Named { name: "Zero" }None
Entity(2, Generation(1))NoneEntity(0, Generation(1))Position(4.0, 5.0, 6.0)NoneSword
Entity(3, Generation(1))NoneEntity(1, Generation(1))Position(4.0, 5.0, 6.0)NoneSword

The sword entity 2 has player entity 0 as its parent, and sword entity 3 has player entity 1 as its parent.

This can be seen by running the prefab_custom example from the Amethyst repository:

cargo run --example prefab_custom

Phew, that was long! Now that you have an understanding of how prefabs work in Amethyst, the next page covers the technical aspects in more detail.

How to Define Prefabs: Prelude

This page is not a guide, but since prefabs are extremely complicated, this is a dedicated page to help you choose which guide to use.

If you are looking for a guide for how to define prefab data that combines the components of multiple existing prefabs, please see How to Define Prefabs: Aggregate.

If you are looking for a guide to define prefab data for a Component, first we need to figure out its type based on its serialized representation. The following table summarizes the types, and links to the relevant guide. For additional detail, refer to the code snippets below the table.

ComponentSerialized representationExample(s)Prefab DataGuide
YourTypeSelfYourTypePositionPositionSimple
YourTypeMultiple – V1(..), V2(..)CameraCameraPrefabAdapter
YourTypeSubset of YourTypeAudioListenerAudioPrefabAsset
Handle<A>Loaded from A::DataMesh, TextureMeshData, TexturePrefabAsset
ManyHandlesData that component stores handles ofMaterialMaterialPrefabMulti-Handle

Serialized Representation

  • Self

    This is where the Component type itself is completely serializable – the data is self-contained.

    # extern crate amethyst;
    # extern crate serde;
    #
    # use amethyst::ecs::{storage::DenseVecStorage, Component};
    # use serde::{Deserialize, Serialize};
    #
    #[derive(Component, Debug, Deserialize, Serialize /* .. */)]
    pub struct Position(pub f32, pub f32, pub f32);
    

    Applicable guide: How to Define Prefabs: Simple.

  • Multiple

    This is where are multiple ways to construct the component, and a user should be able to choose which one to use.

    # extern crate amethyst;
    # extern crate serde;
    #
    # use amethyst::ecs::{storage::DenseVecStorage, Component};
    # use serde::{Deserialize, Serialize};
    #
    # #[derive(Component, Debug, Deserialize, Serialize /* .. */)]
    # pub struct Position {
    #     pub x: f32,
    #     pub y: f32,
    #     pub z: f32,
    # };
    #
    impl From<(i32, i32, i32)> for Position {
        fn from((x, y, z): (i32, i32, i32)) -> Position {
            Position {
                x: x as f32,
                y: y as f32,
                z: z as f32,
            }
        }
    }
    
    impl From<(f32, f32, f32)> for Position {
        fn from((x, y, z): (f32, f32, f32)) -> Position {
            Position { x, y, z }
        }
    }
    

    Applicable guide: How to Define Prefabs: Adapter.

  • Component Subset

    This is where most of the component is serializable, but there is also data that is only accessible at runtime, such as a device ID or an asset handle.

    # extern crate amethyst_audio;
    # extern crate amethyst_core;
    #
    # use amethyst_audio::output::Output;
    # use amethyst_core::{
    #     math::Point3,
    #     ecs::{storage::HashMapStorage, Component},
    # };
    #
    #[derive(Debug, Component)]
    # #[storage(HashMapStorage)]
    pub struct AudioListener {
        /// Output used by this listener to emit sounds to
        pub output: Output, // <--- NOTE: Only available at runtime
        // ..
    #     /// Position of the left ear relative to the global transform on this entity.
    #     pub left_ear: Point3<f32>,
    #     /// Position of the right ear relative to the global transform on this entity.
    #     pub right_ear: Point3<f32>,
    }
    

    Applicable guide: How to Define Prefabs: Asset.

  • Asset

    When using Handle<A> as a component, A must impl Asset, and therefore A::Data must be serializable.

    This is where you want to load A as part of a prefab.

    Applicable guide: How to Define Prefabs: Asset.

  • Multi-Handle

    This is where the Component itself stores Handle<_>s.

    # extern crate amethyst;
    #
    # use amethyst::{
    #     assets::Handle,
    #     ecs::{storage::DenseVecStorage, Component},
    #     renderer::Texture,
    # };
    #
    /// Material struct.
    #[derive(Clone, PartialEq)]
    pub struct Material {
        /// Diffuse map.
        pub albedo: Handle<Texture>,
        /// Emission map.
        pub emission: Handle<Texture>,
        // ..
    }
    
    impl Component for Material {
        type Storage = DenseVecStorage<Self>;
    }
    

    Applicable guide: How to Define Prefabs: Multi-Handle.

How to Define Prefabs: Simple

This guide explains how to enable a [Component] to be used in a [Prefab]. This can be applied where the [Component] type itself is completely serializable – the data is self-contained:

# extern crate amethyst;
# extern crate serde;
#
# use amethyst::ecs::{storage::DenseVecStorage, Component};
# use serde::{Deserialize, Serialize};
#
#[derive(Component, Debug, Deserialize, Serialize /* .. */)]
pub struct Position(pub f32, pub f32, pub f32);

If you are attempting to adapt a more complex type, please choose the appropriate guide from the [available guides][bk_prefab_prelude].

Steps

  1. Ensure your crate has the following dependencies:

    [dependencies]
    amethyst = ".." # Minimum version 0.10
    serde = { version = "1.0", features = ["derive"] }
    
  2. Import the following items:

    use amethyst::{
        assets::{PrefabData, ProgressCounter},
        derive::PrefabData,
        ecs::Entity,
        Error,
    };
    use serde::{Deserialize, Serialize};
    
  3. Add the following attributes on your type:

    #[derive(Deserialize, Serialize, PrefabData)]
    #[prefab(Component)]
    #[serde(default)] // <--- optional
    #[serde(deny_unknown_fields)]
    

    Example:

    # extern crate amethyst;
    # extern crate derivative;
    # extern crate serde;
    #
    # use amethyst::{
    #     assets::{Prefab, PrefabData},
    #     derive::PrefabData,
    #     ecs::{
    #         storage::DenseVecStorage,
    #         Component, Entity, WriteStorage,
    #     },
    #     prelude::*,
    #     Error,
    # };
    # use derivative::Derivative;
    # use serde::{Deserialize, Serialize};
    #
    #[derive(Clone, Copy, Component, Debug, Default, Deserialize, Serialize, PrefabData)]
    #[prefab(Component)]
    #[serde(deny_unknown_fields)]
    pub struct Position(pub f32, pub f32, pub f32);
    

    The [PrefabData][api_pf_derive] derive implements the [PrefabData] trait for the type. The #[prefab(Component)] attribute informs the [PrefabData] derive that this type is a [Component], as opposed to being composed of fields which implement [PrefabData].

    The [#[serde(default)]] attribute allows fields to not be specified in the prefab, and the fields' default value will be used. If this attribute is not present, then all fields must be specified in the prefab.

    Finally, the [#[serde(deny_unknown_fields)]] ensures that deserialization produces an error if it encounters an unknown field. This will help expose mistakes in the prefab file, such as when there is a typo.

  4. Now the type can be used in a prefab:

    #![enable(implicit_some)]
    Prefab(
        entities: [
            PrefabEntity(
                data: Position(1.0, 2.0, 3.0),
            ),
        ],
    )
    

To see this in a complete example, run the [prefab_basic example][repo_prefab_basic] from the Amethyst repository:

cargo run --example prefab_basic

[#[serde(default)]]: https://serde.rs/container-attrs.html#default [#[serde(deny_unknown_fields)]]: https://serde.rs/container-attrs.html#deny_unknown_fields [Component]: https://docs-src.amethyst.rs/stable/specs/trait.Component.html [Prefab]: https://docs-src.amethyst.rs/stable/amethyst_assets/struct.Prefab.html [PrefabData]: https://docs-src.amethyst.rs/stable/amethyst_assets/trait.PrefabData.html#impl-PrefabData%3C%27a%3E [api_pf_derive]: https://docs-src.amethyst.rs/stable/amethyst_derive/derive.PrefabData.html [bk_prefab_prelude]: how_to_define_prefabs_prelude.html [repo_prefab_basic]: https://github.com/amethyst/amethyst/tree/master/examples/prefab_basic

How to Define Prefabs: Aggregate

This guide explains how to define a [PrefabData] that encapsulates other [PrefabData].

If you intend to include a [Component] that has not yet got a corresponding [PrefabData], please use an appropriate guide from the [available guides][bk_prefab_prelude] to create its [PrefabData] first.

Steps

  1. Ensure your crate has the following dependencies:

    [dependencies]
    amethyst = ".." # Minimum version 0.10
    serde = { version = "1.0", features = ["derive"] }
    
  2. Import the following items:

    use amethyst::{
        assets::{PrefabData, ProgressCounter},
        derive::PrefabData,
        ecs::Entity,
        Error,
    };
    use serde::{Deserialize, Serialize};
    
  3. Define the aggregate prefab data type.

    In these examples, Named, Position, and Weapon all derive [PrefabData].

    # extern crate amethyst;
    # extern crate serde;
    # use amethyst::{
    #     assets::{PrefabData, ProgressCounter},
    #     core::Named,
    #     derive::PrefabData,
    #     ecs::{
    #         storage::DenseVecStorage,
    #         Component, Entity, WriteStorage,
    #     },
    #     prelude::*,
    #     Error,
    # };
    # use serde::{Deserialize, Serialize};
    #
    #[derive(Clone, Copy, Component, Debug, Default, Deserialize, Serialize, PrefabData)]
    #[prefab(Component)]
    #[serde(deny_unknown_fields)]
    pub struct Position(pub f32, pub f32, pub f32);
    
    /// **Note:** All fields must be specified in the prefab. If a field is
    /// not specified, then the prefab will fail to load.
    #[derive(Deserialize, Serialize, PrefabData)]
    #[serde(deny_unknown_fields)]
    pub struct Player {
        name: Named,
        position: Position,
    }
    

    If you want to mix different types of entities within a single prefab then you must define an enum that implements PrefabData. Each variant is treated in the same way as PrefabData structs.

    # extern crate amethyst;
    # extern crate serde;
    # use amethyst::{
    #     assets::{PrefabData, ProgressCounter},
    #     core::Named,
    #     derive::PrefabData,
    #     ecs::{
    #         storage::{DenseVecStorage, VecStorage},
    #         Component, Entity, WriteStorage,
    #     },
    #     prelude::*,
    #     Error,
    # };
    # use serde::{Deserialize, Serialize};
    #
    #[derive(Clone, Copy, Component, Debug, Default, Deserialize, Serialize, PrefabData)]
    #[prefab(Component)]
    #[serde(deny_unknown_fields)]
    pub struct Position(pub f32, pub f32, pub f32);
    
    #[derive(Clone, Copy, Component, Debug, Deserialize, Serialize, PrefabData)]
    #[prefab(Component)]
    #[storage(VecStorage)]
    pub enum Weapon {
        Axe,
        Sword,
    }
    
    /// All fields implement `PrefabData`.
    ///
    /// **Note:** If a field is of type `Option<_>` and not specified in the prefab, it will default
    /// to `None`.
    #[derive(Debug, Deserialize, Serialize, PrefabData)]
    #[serde(deny_unknown_fields)]
    pub enum CustomPrefabData {
        Player {
            name: Named,
            position: Option<Position>,
        },
        Weapon {
            weapon_type: Weapon,
            position: Option<Position>,
        },
    }
    
    

    Note: There is an important limitation when building PrefabDatas, particularly enum PrefabDatas. No two fields in the PrefabData or in any nested PrefabDatas under it can access the same Component unless all accesses are reads. This is still true even if the fields appear in different variants of an enum. This means that the following PrefabData will fail at runtime when loaded:

    # extern crate amethyst;
    # extern crate serde;
    # use amethyst::{
    #     assets::{PrefabData, ProgressCounter},
    #     core::Named,
    #     derive::PrefabData,
    #     ecs::{
    #         storage::{DenseVecStorage, VecStorage},
    #         Component, Entity, WriteStorage,
    #     },
    #     prelude::*,
    #     renderer::sprite::prefab::SpriteScenePrefab,
    #     Error,
    # };
    # use serde::{Deserialize, Serialize};
    
    #[derive(Clone, Copy, Component, Debug, Default, Deserialize, Serialize, PrefabData)]
    #[prefab(Component)]
    #[serde(deny_unknown_fields)]
    pub struct SpecialPower;
    
    #[derive(Debug, Deserialize, Serialize, PrefabData)]
    #[serde(deny_unknown_fields)]
    pub enum CustomPrefabData {
        MundaneCreature {
            sprite: SpriteScenePrefab,
        },
        MagicalCreature {
            special_power: SpecialPower,
            sprite: SpriteScenePrefab,
        },
    }
    
    

    The problem is that both the SpriteScenePrefabs need to write to Transform and several other common Components. Because Amythest's underlyng ECS system determines what resources are accessed based on static types it can't determine that only one of the SpriteScenePrefabs will be accessed at a time and it attempts a double mutable borrow which fails. The solution is to define the PrefabData hierarchically so each component only appears once:

    # extern crate amethyst;
    # extern crate serde;
    # use amethyst::{
    #     assets::{PrefabData, ProgressCounter},
    #     core::Named,
    #     derive::PrefabData,
    #     ecs::{
    #         storage::{DenseVecStorage, VecStorage},
    #         Component, Entity, WriteStorage,
    #     },
    #     prelude::*,
    #     renderer::sprite::prefab::SpriteScenePrefab,
    #     Error,
    # };
    # use serde::{Deserialize, Serialize};
    
    #[derive(Clone, Copy, Component, Debug, Default, Deserialize, Serialize, PrefabData)]
    #[prefab(Component)]
    #[serde(deny_unknown_fields)]
    pub struct SpecialPower;
    
    #[derive(Debug, Deserialize, Serialize, PrefabData)]
    #[serde(deny_unknown_fields)]
    pub enum CreatureDetailsPrefab {
        MundaneCreature {
        },
        MagicalCreature {
            special_power: SpecialPower,
        },
    }
    #[derive(Debug, Deserialize, Serialize, PrefabData)]
    #[serde(deny_unknown_fields)]
    pub struct CustomPrefabData {
        sprite: SpriteScenePrefab,
        creature_details: CreatureDetailsPrefab,
    }
    
    

    The [PrefabData][api_pf_derive] derive implements the [PrefabData] trait for the type. The generated code will handle invoking the appropriate [PrefabData] methods when loading and attaching components to an entity. Note: This differs from the simple component [PrefabData] derive implementation – there is no #[prefab(Component)] attribute.

    The [#[serde(default)]] attribute allows fields to not be specified in the prefab, and the fields' default value will be used. If this attribute is not present, then all fields must be specified in the prefab.

    Finally, the [#[serde(deny_unknown_fields)]] ensures that deserialization produces an error if it encounters an unknown field. This will help expose mistakes in the prefab file, such as when there is a typo.

  4. Now the type can be used in a prefab.

    • struct prefab data:

      #![enable(implicit_some)]
      Prefab(
          entities: [
              PrefabEntity(
                  data: Player(
                      name: Named(name: "Zero"),
                      position: Position(1.0, 2.0, 3.0),
                  ),
              ),
          ],
      )
      
    • enum prefab data:

      #![enable(implicit_some)]
      Prefab(
          entities: [
              // Player
              PrefabEntity(
                  data: Player(
                      name: Named(name: "Zero"),
                      position: Position(1.0, 2.0, 3.0),
                  ),
              ),
              // Weapon
              PrefabEntity(
                  parent: 0,
                  data: Weapon(
                      weapon_type: Sword,
                      position: Position(4.0, 5.0, 6.0),
                  ),
              ),
          ],
      )
      

To see this in a complete example, run the [prefab_custom example][repo_prefab_custom] or the [prefab_multi example][repo_prefab_multi] from the Amethyst repository:

cargo run --example prefab_custom # superset prefab
cargo run --example prefab_multi # object prefab

[#[serde(default)]]: https://serde.rs/container-attrs.html#default [#[serde(deny_unknown_fields)]]: https://serde.rs/container-attrs.html#deny_unknown_fields [Component]: https://docs-src.amethyst.rs/stable/specs/trait.Component.html [Prefab]: https://docs-src.amethyst.rs/stable/amethyst_assets/struct.Prefab.html [PrefabData]: https://docs-src.amethyst.rs/stable/amethyst_assets/trait.PrefabData.html#impl-PrefabData%3C%27a%3E [api_pf_derive]: https://docs-src.amethyst.rs/stable/amethyst_derive/derive.PrefabData.html [bk_prefab_prelude]: how_to_define_prefabs_prelude.html [repo_prefab_custom]: https://github.com/amethyst/amethyst/tree/master/examples/prefab_custom [repo_prefab_multi]: https://github.com/amethyst/amethyst/tree/master/examples/prefab_multi

How to Define Prefabs: Adapter

This guide explains how to define a [PrefabData] for a [Component] using an intermediate type called an adapter. This pattern is used when there are multiple ways to serialize / construct the [Component]:

# extern crate amethyst;
# extern crate serde;
#
# use amethyst::ecs::{storage::DenseVecStorage, Component};
# use serde::{Deserialize, Serialize};
#
# #[derive(Component, Debug, Deserialize, Serialize /* .. */)]
# pub struct Position(pub f32, pub f32, pub f32);
#
impl From<(i32, i32, i32)> for Position {
    fn from((x, y, z): (i32, i32, i32)) -> Position {
        Position(x as f32, y as f32, z as f32)
    }
}

impl From<(f32, f32, f32)> for Position {
    fn from((x, y, z): (f32, f32, f32)) -> Position {
        Position(x, y, z)
    }
}

If you are attempting to adapt a more complex type, please choose the appropriate guide from the [available guides][bk_prefab_prelude].

Steps

  1. Ensure your crate has the following dependencies:

    [dependencies]
    amethyst = ".." # Minimum version 0.10
    serde = { version = "1.0", features = ["derive"] }
    
  2. Define the adapter prefab data type.

    Create a (de)serializable enum type with a variant for each representation. The following is an example of an adapter type for the [Position] component, which allows either i32 or f32 values to be specified in the prefab:

    # extern crate amethyst;
    # extern crate serde;
    #
    use amethyst::{
        assets::{PrefabData, ProgressCounter},
        ecs::{Entity, WriteStorage},
        Error,
    };
    use serde::{Deserialize, Serialize};
    
    #[derive(Clone, Copy, Deserialize, PartialEq, Serialize)]
    #[serde(deny_unknown_fields)]
    pub enum PositionPrefab {
        Pos3f { x: f32, y: f32, z: f32 },
        Pos3i { x: i32, y: i32, z: i32 },
    }
    

    The [#[serde(deny_unknown_fields)]] ensures that deserialization produces an error if it encounters an unknown field. This will help expose mistakes in the prefab file, such as when there is a typo.

    Note: You may already have a type that captures the multiple representations. For example, for the [Camera] component, the [Projection] enum captures the different representations:

    # extern crate amethyst;
    # extern crate serde;
    #
    # use amethyst::core::math::{Orthographic3, Perspective3};
    # use serde::{Deserialize, Serialize};
    #
    #[derive(Clone, Deserialize, PartialEq, Serialize)]
    pub enum Projection {
        Orthographic(Orthographic3<f32>),
        Perspective(Perspective3<f32>),
    }
    
  3. Implement the [PrefabData] trait for the adapter type.

    # extern crate amethyst;
    # extern crate serde;
    #
    # use amethyst::{
    #     assets::{PrefabData, ProgressCounter},
    #     ecs::{storage::DenseVecStorage, Component, Entity, WriteStorage},
    #     Error,
    # };
    # use serde::{Deserialize, Serialize};
    #
    # #[derive(Component, Debug, Deserialize, Serialize /* .. */)]
    # pub struct Position(pub f32, pub f32, pub f32);
    #
    # impl From<(i32, i32, i32)> for Position {
    #     fn from((x, y, z): (i32, i32, i32)) -> Position {
    #         Position(x as f32, y as f32, z as f32)
    #     }
    # }
    #
    # impl From<(f32, f32, f32)> for Position {
    #     fn from((x, y, z): (f32, f32, f32)) -> Position {
    #         Position(x, y, z)
    #     }
    # }
    #
    # #[derive(Clone, Copy, Deserialize, PartialEq, Serialize)]
    # #[serde(deny_unknown_fields)]
    # pub enum PositionPrefab {
    #     Pos3f { x: f32, y: f32, z: f32 },
    #     Pos3i { x: i32, y: i32, z: i32 },
    # }
    #
    impl<'a> PrefabData<'a> for PositionPrefab {
        // To attach the `Position` to the constructed entity,
        // we write to the `Position` component storage.
        type SystemData = WriteStorage<'a, Position>;
    
        // This associated type is not used in this pattern,
        // so the empty tuple is specified.
        type Result = ();
    
        fn add_to_entity(
            &self,
            entity: Entity,
            positions: &mut Self::SystemData,
            _entities: &[Entity],
            _children: &[Entity],
        ) -> Result<(), Error> {
            let position = match *self {
                PositionPrefab::Pos3f { x, y, z } => (x, y, z).into(),
                PositionPrefab::Pos3i { x, y, z } => (x, y, z).into(),
            };
            positions.insert(entity, position).map(|_| ())?;
            Ok(())
        }
    }
    
  4. Now the adapter type can be used in a prefab to attach the component to the entity.

    #![enable(implicit_some)]
    Prefab(
        entities: [
            PrefabEntity(
                data: Pos3f(x: 1.0, y: 2.0, z: 3.0),
            ),
            PrefabEntity(
                data: Pos3i(x: 4, y: 5, z: 6),
            ),
        ],
    )
    

To see this in a complete example, run the [prefab_adapter example][repo_prefab_adapter] from the Amethyst repository:

cargo run --example prefab_adapter

[#[serde(default)]]: https://serde.rs/container-attrs.html#default [#[serde(deny_unknown_fields)]]: https://serde.rs/container-attrs.html#deny_unknown_fields [Camera]: https://docs-src.amethyst.rs/stable/amethyst_renderer/struct.Camera.html [Component]: https://docs-src.amethyst.rs/stable/specs/trait.Component.html [Prefab]: https://docs-src.amethyst.rs/stable/amethyst_assets/struct.Prefab.html [PrefabData]: https://docs-src.amethyst.rs/stable/amethyst_assets/trait.PrefabData.html#impl-PrefabData%3C%27a%3E [Projection]: https://docs-src.amethyst.rs/stable/amethyst_renderer/enum.Projection.html [bk_prefab_prelude]: how_to_define_prefabs_prelude.html [repo_prefab_adapter]: https://github.com/amethyst/amethyst/tree/master/examples/prefab_adapter

How to Define Prefabs: Asset

Note: This guide is not yet written. Please check back later!

If you would like to contribute, please let us know in #1114

How to Define Prefabs: Multi-Handle

Note: This guide is not yet written. Please check back later!

If you would like to contribute, please let us know in #1114

Prefabs Technical Explanation

A Prefab in Amethyst is at the core a simple list of future entities, where each entry in the list consists of two pieces of optional data:

  • a parent index that refers to a different entry in the list
  • a data collection implementing the trait PrefabData

To instantiate a Prefab, we put a Handle<Prefab<T>> on an Entity. The Entity we put the Handle on is referred to as the main Entity, and the first entry in the list inside a Prefab refers to this Entity. All other entries in the list will spawn a new Entity on instantiation.

NOTE: This means that we currently cannot target multiple existing entities from a single Prefab. This restriction is likely to be removed in the future.

The lifetime of a Prefab can roughly be divided into three distinct parts:

Loading

This is the same as for all assets in Amethyst, the user initiates a load using Loader, a Source and a Format. The Format returns a Prefab, and the user is handed a Handle<Prefab<T>>, for some T that implements PrefabData.

Sub asset loading

A PrefabData implementation could refer to other assets that need to be loaded asynchronously, and we don't want the user get a Complete notification on their Progress before everything has been loaded.

Because of this, once the Format have loaded the Prefab from the Source, and a PrefabLoaderSystem runs process on the AssetStorage, the system will invoke the load_sub_assets function on the PrefabData implementation. If any asset loads are triggered during this, they must adhere to the following rules:

  • the given ProgressCounter must be used as a parameter to the load function on Loader, so load tracking works correctly
  • the function must return Ok(true) (unless an Error occurred)

Note that during this phase the PrefabData is mutable, which means it can morph inside the Prefab. An example of this is the AssetPrefab, which will morph into AssetPrefab::Handle.

Once all sub asset loading is finished, which the PrefabLoaderSystem will track using the ProgressCounter, a Complete signal will be sent upwards.

Prefab instantiation

This stage happens after the Prefab has been fully loaded and Complete has been signaled, and the Handle<Prefab<T>> is put on an Entity. At this point we know that all internal data has been loaded, and all sub assets have been processed. The PrefabLoaderSystem will then walk through the Prefab data immutably and create a new Entity for all but the first entry in the list, and then for each instance of PrefabData call the add_to_entity function.

Note that for prefabs that reference other prefabs, to make instantiation be performed inside a single frame, lower level PrefabLoaderSystems need to depend on the higher level ones. To see how this works out check the gltf example, where we have a scene prefab, and the gltf loader (which use the prefab system internally).

PrefabData

Ok, so what would a simple implementation of PrefabData look like?

Let's take a look at the implementation for Transform, which is a core concept in Amethyst:

# extern crate amethyst;
# use amethyst::assets::PrefabData;
# use amethyst::ecs::{WriteStorage, Entity, Component, NullStorage};
# use amethyst::Error;
#
# // We declare that struct for the sake of automated testing.
# #[derive(Default, Clone)]
# struct Transform;
# impl Component for Transform {
#   type Storage = NullStorage<Transform>;
# }
#
impl<'a> PrefabData<'a> for Transform {
    type SystemData = WriteStorage<'a, Transform>;
    type Result = ();

    fn add_to_entity(
        &self,
        entity: Entity,
        storage: &mut Self::SystemData,
        _: &[Entity],
        _: &[Entity],
    ) -> Result<(), Error> {
        storage.insert(entity, self.clone()).map(|_| ()).map_err(Into::into)
    }
}

First, we specify a SystemData type, this is the data required from World in order to load and instantiate this PrefabData. Here we only need to write to Transform.

Second, we specify what result the add_to_entity function returns. In our case this is unit (), for other implementations it could return a Handle etc. For an example of this, look at the TexturePrefab in the renderer crate.

Next, we define the add_to_entity function, which is used to actually instantiate data. In our case here, we insert the local Transform data on the referenced Entity. In this scenario we aren't using the third parameter to the function. This parameter contains a list of all entities affected by the Prefab, the first entry in the list will be the main Entity, and the rest will be the entities that were created for all the entries in the data list inside the Prefab.

Last of all, we can see that this does not implement load_sub_assets, which is because there are no secondary assets to load from Source here.

Let's look at a slightly more complex implementation, the AssetPrefab. This PrefabData is used to load extra Assets as part of a Prefab:

# extern crate amethyst;
# #[macro_use] extern crate serde_derive;
# use amethyst::assets::{Asset, AssetStorage, Loader, Format, Handle, ProgressCounter};
# use amethyst::assets::PrefabData;
# use amethyst::ecs::{WriteStorage, ReadExpect, Read, Entity};
# use amethyst::Error;
#
#[derive(Deserialize, Serialize)]
pub enum AssetPrefab<A, F>
where
    A: Asset,
    F: Format<A::Data>,
{
    /// From existing handle
    #[serde(skip)]
    Handle(Handle<A>),

    /// From file, (name, format, format options)
    File(String, F),
}

impl<'a, A, F> PrefabData<'a> for AssetPrefab<A, F>
where
    A: Asset,
    F: Format<A::Data> + Clone,
{
    type SystemData = (
        ReadExpect<'a, Loader>,
        WriteStorage<'a, Handle<A>>,
        Read<'a, AssetStorage<A>>,
    );

    type Result = Handle<A>;

    fn add_to_entity(
        &self,
        entity: Entity,
        system_data: &mut Self::SystemData,
        _: &[Entity],
        _: &[Entity],
    ) -> Result<Handle<A>, Error> {
        let handle = match *self {
            AssetPrefab::Handle(ref handle) => handle.clone(),
            AssetPrefab::File(ref name, ref format) => system_data.0.load(
                name.as_str(),
                format.clone(),
                (),
                &system_data.2,
            ),
        };
        Ok(system_data.1.insert(entity, handle.clone())?.unwrap())
    }

    fn load_sub_assets(
        &mut self,
        progress: &mut ProgressCounter,
        system_data: &mut Self::SystemData,
    ) -> Result<bool, Error> {
        let handle = match *self {
            AssetPrefab::File(ref name, ref format) => Some(system_data.0.load(
                name.as_str(),
                format.clone(),
                progress,
                &system_data.2,
            )),
            _ => None,
        };
        if let Some(handle) = handle {
            *self = AssetPrefab::Handle(handle);
        }
        Ok(true)
    }
}

So, there are two main differences to this PrefabData compared the Transform example. The first difference is that the add_to_entity function now return a Handle<A>. The second difference is that load_sub_assets is implemented, this is because we load a sub asset. The load_sub_assets function here will do the actual loading, and morph the internal representation to the AssetPrefab::Handle variant, so when add_to_entity runs later it will straight up use the internally stored Handle.

Special PrefabData implementations

There are a few special blanket implementations provided by the asset system:

  • Option<T> for all T: PrefabData.
  • Tuples of types that implemented PrefabData, up to a size of 20.

Deriving PrefabData implementations

Amethyst supplies a derive macro for creating the PrefabData implementation for the following scenarios:

  • Single Component
  • Aggregate PrefabData structs or enums which contain other PrefabData constructs, and optionally simple data Components

In addition, deriving a Prefab requires that amethyst::Error, amethyst::ecs::Entity and amethyst:assets::{PrefabData, ProgressCounter} are imported and visible in the current scope. This is due to how Rust macros work.

An example of a single Component derive:

# #[macro_use] extern crate amethyst;
# #[macro_use] extern crate serde_derive;
# use amethyst::{
#     assets::{
#         Asset, AssetStorage, Loader, Format, Handle, ProgressCounter, PrefabData
#     },
#     derive::PrefabData,
#     ecs::{
#         Component, DenseVecStorage, Entity, Read, ReadExpect, WriteStorage,
#     },
#     Error,
# };
#
#[derive(Clone, PrefabData)]
#[prefab(Component)]
pub struct SomeComponent {
    pub id: u64,
}

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

This will derive a PrefabData implementation that inserts SomeComponent on an Entity in the World.

Lets look at an example of an aggregate struct:

# #[macro_use] extern crate amethyst;
# #[macro_use] extern crate serde_derive;
# use amethyst::assets::{Asset, AssetStorage, Loader, Format, Handle, ProgressCounter, PrefabData, AssetPrefab};
# use amethyst::core::Transform;
# use amethyst::ecs::{WriteStorage, ReadExpect, Read, Entity, DenseVecStorage, Component};
# use amethyst::renderer::{Mesh, formats::mesh::ObjFormat};
# use amethyst::Error;

#[derive(PrefabData)]
pub struct MyScenePrefab {
    mesh: AssetPrefab<Mesh, ObjFormat>,
    transform: Transform,
}

This can now be used to create Prefabs with Transform and Mesh on entities.

One last example that also adds a custom pure data Component into the aggregate PrefabData:

# #[macro_use] extern crate amethyst;
# #[macro_use] extern crate serde_derive;
# use amethyst::assets::{Asset, AssetStorage, Loader, Format, Handle, ProgressCounter, PrefabData, AssetPrefab};
# use amethyst::core::Transform;
# use amethyst::ecs::{WriteStorage, ReadExpect, Read, Entity, DenseVecStorage, Component};
# use amethyst::renderer::{Mesh, formats::mesh::ObjFormat};
# use amethyst::Error;

#[derive(PrefabData)]
pub struct MyScenePrefab {
    mesh: AssetPrefab<Mesh, ObjFormat>,
    transform: Transform,

    #[prefab(Component)]
    some: SomeComponent,
}

#[derive(Clone)]
pub struct SomeComponent {
    pub id: u64,
}

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

You might notice here that SomeComponent has no PrefabData derive on its own, it is simply used directly in the aggregate PrefabData, and annotated so the derive knows to do a simple WriteStorage insert.

Working with Prefabs

So now we know how the Prefab system works on the inside, but how do we use it?

From the point of the user, there are a few parts to using a Prefab:

  • Loading it, using Loader + AssetStorage, or using the helper PrefabLoader, which is a simple wrapper around the former. For this to work we need a Format that returns Prefabs.
  • Managing the returned Handle<Prefab<T>>.
  • Waiting for the Prefab to be fully loaded, using Progress.
  • Requesting instantiation by placing the Handle<Prefab<T>> on an Entity in the World.

Prefab formats

There are a few provided formats that create Prefabs, some with very specific PrefabData, and two that are generic:

  • RonFormat - this format can be used to load Prefabs in ron format with any PrefabData that also implements serde::Deserialize.
  • JsonFormat - this format can be used to load Prefabs in Json format with any PrefabData that also implements serde::Deserialize. It can be enabled with the json feature flag.
  • GltfSceneFormat - used to load Gltf files
  • UiFormat - used to load UI components in a specialised DSL format.

For an example of a Prefab in ron format, look at examples/assets/prefab/example.ron. The PrefabData for this is:

(
    Option<GraphicsPrefab<ObjFormat, TextureFormat>>,
    Option<Transform>,
    Option<Light>,
    Option<CameraPrefab>,
)

For a more advanced example, and also a custom PrefabData implementation, look at the gltf example and examples/assets/prefab/puffy_scene.ron.

Animation

Animation in computer graphics can be viewed as controlled mutation of attributes of objects over time, using a predefined function. Examples of this are:

  • Changing coordinates of vertices — movement, scaling up or down
  • Changing the hue of a texture — for a "power up" effect

To determine the values each attribute should have at a particular point in time, we define a set of known values at certain points in the animation — called key frames — and a function to interpolate the value for the attribute.

This section will guide you in learning how to make use of the animation functionality in Amethyst.

Interpolation

Interpolation is the calculation of an attribute value that lies in between two key frames.

For example, if an object should move in a circle, then we can define an animation that mutates its X and Y coordinate attributes.

The animation definition can represent this using 5 key frames:

Key Frame #X coordinateY coordinate
00.01.0
11.00.0
20.0-1.0
3-1.00.0
40.01.0

Non-interpolation

For a perfect circle, the values in between the key frames can be calculated by the sin(..) function for the X coordinate, and the cos(..) function for the Y coordinate. So, if we were trying to calculate what the coordinates should be when t = 0.5, we could go sin( 0.5 * π ).

However, what if we do not have such perfect coordinate control, and we only have the values at the specified key frames?

Interpolation

To move in a circle, the X coordinate first increases with a larger step, and the step size decreases as it approaches the circle boundary on the X axis, where it then flips, and increases in the negative direction. For the Y coordinate, the magnitude of the step size increases downwards, then decreases once it has gotten past the halfway point.

The changing step size means, given the first two key frames, 0 and 1, the values do not change in constant step increments — linearly (LERP) —, but spherical linearly (SLERP).

The spherical linear function is a way of saying, given these two key frame values, and some proportion of time between the two key frames, what should the actual value be given that the step increments change as they would on a sphere?

Interpolation Functions

In computer graphics, there are a number of methods commonly used to calculate the interpolated values. The following functions are available in Amethyst, implemented by the minterpolate library, namely:

  • Linear
  • SphericalLinear
  • Step
  • CatmullRomSpline
  • CubicSpline

Amethyst also allows you to specify your own custom interpolation function.

Channel

An independent grouping or type of functions that operate on attributes of a component.

Some attributes may be mutated by different functions. These functions can be independent of each other, or they may also be dependent each other. An example of these are translation, scaling, and rotation.

Given the following functions are part of the same animation:

  • Translate the object to the right
  • Translate the object upwards
  • Scale the object up

We want to be able to individually apply related functions, i.e. "apply all translations", "apply all scalings", and "apply all rotations". Each of these groupings is called a channel.

Sampler

In Amethyst, a Sampler is the lowest level working block of an animation. It defines the interpolation function, and what attribute or set of attributes the function mutates.

The input holds the timing of the key frames. The output holds the values used in the interpolation function for each of the key frames.

You can imagine the interpolation function as fn(Time) -> ChannelValue

Definition

Animations can be defined for objects made of a single entity, or complex objects made up of multiple entities.

Right now we do not have a tutorial for defining an animation from scratch, but take a look at the following resources to get an idea of how to write one:

Controlling System Execution

When writing a game you'll eventually reach a point where you want to have more control over when certain Systems are executed, such as running them for specific States or pausing them when a certain condition is met. Right now you have these three options to achieve said control:

  • Custom GameData:

    Store multiple Dispatchers in a custom GameData. Each Dispatcher has its own assigned Systems and States determines which Dispatchers to run.

  • State-specific Dispatcher:

    A State contains its own Dispatcher with its own Systems and the State handles the execution.

  • Pausable Systems:

    When registering a System with a Dispatcher, specify the value of a Resource R. The System runs only if the Resource equals that value. This allows for more selective enabling and disabling of Systems.

This section contains guides that demonstrate each of these methods.

Custom GameData

So far we've been using the Amethyst supplied GameData struct to handle our Systems. This works well for smaller games and demos, but once we start building a larger game, we will quickly realise we need to manipulate the System dispatch based on game State, or we need to pass data between States that aren't Send + Sync which can't be added to World.

The solution to our troubles here is to create a custom GameData structure to house what we need that can not be added to World.

In this tutorial we will look at how one could structure a Paused State, which disables the game logic, only leaving a few core systems running that are essential (like rendering, input and UI).

Let's start by creating the GameData structure:

# extern crate amethyst;
# use amethyst::ecs::prelude::Dispatcher;
#
pub struct CustomGameData<'a, 'b> {
    core_dispatcher: Dispatcher<'a, 'b>,
    running_dispatcher: Dispatcher<'a, 'b>,
}

We also add a utility function for performing dispatch:

# extern crate amethyst;
# use amethyst::ecs::prelude::{Dispatcher, World};
#
# pub struct CustomGameData<'a, 'b> {
#     core_dispatcher: Dispatcher<'a, 'b>,
#     running_dispatcher: Dispatcher<'a, 'b>,
# }
#
impl<'a, 'b> CustomGameData<'a, 'b> {
    /// Update game data
    pub fn update(&mut self, world: &World, running: bool) {
        if running {
            self.running_dispatcher.dispatch(&world);
        }
        self.core_dispatcher.dispatch(&world);
    }
}

To be able to use this structure with Amethysts Application we need to create a builder that implements DataInit. This is the only requirement placed on the GameData structure.

# extern crate amethyst;
#
# use amethyst::ecs::prelude::{Dispatcher, DispatcherBuilder, System, World, WorldExt};
# use amethyst::core::SystemBundle;
# use amethyst::{Error, DataInit};
#
# pub struct CustomGameData<'a, 'b> {
#     core_dispatcher: Dispatcher<'a, 'b>,
#     running_dispatcher: Dispatcher<'a, 'b>,
# }
#
use amethyst::core::ArcThreadPool;

pub struct CustomGameDataBuilder<'a, 'b> {
    pub core: DispatcherBuilder<'a, 'b>,
    pub running: DispatcherBuilder<'a, 'b>,
}

impl<'a, 'b> Default for CustomGameDataBuilder<'a, 'b> {
    fn default() -> Self {
        CustomGameDataBuilder::new()
    }
}

impl<'a, 'b> CustomGameDataBuilder<'a, 'b> {
    pub fn new() -> Self {
        CustomGameDataBuilder {
            core: DispatcherBuilder::new(),
            running: DispatcherBuilder::new(),
        }
    }

    pub fn with_base_bundle<B>(mut self, world: &mut World, bundle: B) -> Result<Self, Error>
    where
        B: SystemBundle<'a, 'b>,
    {
        bundle.build(world, &mut self.core)?;
        Ok(self)
    }

    pub fn with_running<S>(mut self, system: S, name: &str, dependencies: &[&str]) -> Self
    where
        for<'c> S: System<'c> + Send + 'a,
    {
        self.running.add(system, name, dependencies);
        self
    }
}

impl<'a, 'b> DataInit<CustomGameData<'a, 'b>> for CustomGameDataBuilder<'a, 'b> {
    fn build(self, world: &mut World) -> CustomGameData<'a, 'b> {
        // Get a handle to the `ThreadPool`.
        let pool = (*world.read_resource::<ArcThreadPool>()).clone();

        let mut core_dispatcher = self.core.with_pool(pool.clone()).build();
        let mut running_dispatcher = self.running.with_pool(pool.clone()).build();
        core_dispatcher.setup(world);
        running_dispatcher.setup(world);

        CustomGameData { core_dispatcher, running_dispatcher }
    }
}

We can now use CustomGameData in place of the provided GameData when building our Application, but first we should create some States.

# extern crate amethyst;
#
# use amethyst::ecs::prelude::{Dispatcher, World};
# use amethyst::prelude::{State, StateData, StateEvent, Trans};
# use amethyst::input::{is_close_requested, is_key_down, VirtualKeyCode};
#
# pub struct CustomGameData<'a, 'b> {
#     core_dispatcher: Dispatcher<'a, 'b>,
#     running_dispatcher: Dispatcher<'a, 'b>,
# }
#
# impl<'a, 'b> CustomGameData<'a, 'b> {
#     /// Update game data
#     pub fn update(&mut self, world: &World, running: bool) {
#         if running {
#             self.running_dispatcher.dispatch(&world);
#         }
#         self.core_dispatcher.dispatch(&world);
#     }
# }
#
# fn initialise(world: &World) {}
# fn create_paused_ui(world: &World) {}
# fn delete_paused_ui(world: &World) {}
#
struct Main;
struct Paused;

impl<'a, 'b> State<CustomGameData<'a, 'b>, StateEvent> for Paused {
    fn on_start(&mut self, data: StateData<CustomGameData>) {
        create_paused_ui(data.world);
    }

    fn handle_event(
        &mut self,
        data: StateData<CustomGameData>,
        event: StateEvent,
    ) -> Trans<CustomGameData<'a, 'b>, StateEvent> {
        if let StateEvent::Window(event) = &event {
            if is_close_requested(&event) || is_key_down(&event, VirtualKeyCode::Escape) {
                Trans::Quit
            } else if is_key_down(&event, VirtualKeyCode::Space) {
                delete_paused_ui(data.world);
                Trans::Pop
            } else {
                Trans::None
            }
        } else {
            Trans::None
        }
    }

    fn update(&mut self, data: StateData<CustomGameData>) -> Trans<CustomGameData<'a, 'b>, StateEvent> {
        data.data.update(&data.world, false); // false to say we should not dispatch running
        Trans::None
    }
}

impl<'a, 'b> State<CustomGameData<'a, 'b>, StateEvent> for Main {
    fn on_start(&mut self, data: StateData<CustomGameData>) {
        initialise(data.world);
    }

    fn handle_event(
        &mut self,
        _: StateData<CustomGameData>,
        event: StateEvent,
    ) -> Trans<CustomGameData<'a, 'b>, StateEvent> {
        if let StateEvent::Window(event) = &event {
            if is_close_requested(&event) || is_key_down(&event, VirtualKeyCode::Escape) {
                Trans::Quit
            } else if is_key_down(&event, VirtualKeyCode::Space) {
                Trans::Push(Box::new(Paused))
            } else {
                Trans::None
            }
        } else {
            Trans::None
        }
    }

    fn update(&mut self, data: StateData<CustomGameData>) -> Trans<CustomGameData<'a, 'b>, StateEvent> {
        data.data.update(&data.world, true); // true to say we should dispatch running
        Trans::None
    }
}

The only thing that remains now is to use our CustomGameDataBuilder when building the Application.

# extern crate amethyst;
#
# use amethyst::{
#     core::{transform::TransformBundle, SystemBundle},
#     ecs::{Dispatcher, DispatcherBuilder, World, WorldExt},
#     input::{InputBundle, StringBindings},
#     prelude::*,
#     renderer::{
#         plugins::{RenderFlat2D, RenderToWindow},
#         types::DefaultBackend,
#         RenderingBundle,
#     },
#     ui::{RenderUi, UiBundle},
#     utils::application_root_dir,
#     DataInit, Error,
# };
#
# pub struct CustomGameData<'a, 'b> {
#     core_dispatcher: Dispatcher<'a, 'b>,
#     running_dispatcher: Dispatcher<'a, 'b>,
# }
#
# pub struct CustomGameDataBuilder<'a, 'b> {
#     pub core: DispatcherBuilder<'a, 'b>,
#     pub running: DispatcherBuilder<'a, 'b>,
# }
#
# impl<'a, 'b> Default for CustomGameDataBuilder<'a, 'b> {
#     fn default() -> Self { unimplemented!() }
# }
#
# impl<'a, 'b> CustomGameDataBuilder<'a, 'b> {
#     pub fn new() -> Self { unimplemented!() }
#     pub fn with_base_bundle<B>(mut self, world: &mut World, bundle: B) -> Result<Self, Error>
#     where
#         B: SystemBundle<'a, 'b>,
#     {
#         unimplemented!()
#     }
#
#     pub fn with_running<S>(mut self, system: S, name: &str, dependencies: &[&str]) -> Self
#     where
#         for<'c> S: System<'c> + Send + 'a,
#     {
#         unimplemented!()
#     }
# }
#
# impl<'a, 'b> DataInit<CustomGameData<'a, 'b>> for CustomGameDataBuilder<'a, 'b> {
#     fn build(self, world: &mut World) -> CustomGameData<'a, 'b> { unimplemented!() }
# }
#
# fn main() -> amethyst::Result<()> {
#
let mut world = World::new();
let game_data = CustomGameDataBuilder::default()
    .with_running(ExampleSystem, "example_system", &[])
    .with_base_bundle(
        &mut world,
        RenderingBundle::<DefaultBackend>::new()
            // The RenderToWindow plugin provides all the scaffolding for opening a window and
            // drawing on it
            .with_plugin(
                RenderToWindow::from_config_path(display_config_path)
                    .with_clear([0.34, 0.36, 0.52, 1.0]),
            )
            .with_plugin(RenderFlat2D::default())
            .with_plugin(RenderUi::default()),
    )?
    .with_base_bundle(&mut world, TransformBundle::new())?
    .with_base_bundle(&mut world, UiBundle::<StringBindings>::new())?
    .with_base_bundle(
        &mut world,
        InputBundle::<StringBindings>::new().with_bindings_from_file(key_bindings_path)?,
    )?;

let mut game = Application::new(assets_directory, Main, game_data)?;
game.run();
#
# }

Those are the basics of creating a custom GameData structure. Now get out there and build your game!

How to Define State Dispatcher

This guide explains how to define a state-specific Dispatcher whose Systems are only executed within the context of a defined State.

First of all we required a DispatcherBuilder. The DispatcherBuilder handles the actual creation of the Dispatcher and the assignment of Systems to our Dispatcher.

# extern crate amethyst;
#
# use amethyst::{
#     ecs::prelude::*,
#     prelude::*,
# };
# 
let mut dispatcher_builder = DispatcherBuilder::new();

To add Systems to the DispatcherBuilder we use a similar syntax to the one we used to add Systems to GameData.

# extern crate amethyst;
#
# use amethyst::{
#     ecs::prelude::*,
#     prelude::*,
# };
#
# struct MoveBallsSystem; struct MovePaddlesSystem;
# impl<'a> System<'a> for MoveBallsSystem { type SystemData = (); fn run(&mut self, _: ()) {} }
# impl<'a> System<'a> for MovePaddlesSystem { type SystemData = (); fn run(&mut self, _: ()) {} }
let mut dispatcher_builder = DispatcherBuilder::new();

dispatcher_builder.add(MoveBallsSystem, "move_balls_system", &[]);
dispatcher_builder.add(MovePaddlesSystem, "move_paddles_system", &[]);

Alternatively we can add Bundles of Systems to our DispatcherBuilder directly.

# extern crate amethyst;
#
# use amethyst::{
#     core::bundle::SystemBundle,
#     ecs::{DispatcherBuilder, World, WorldExt},
#     prelude::*,
# };
# #[derive(Default)] struct PongSystemsBundle;
# impl<'a, 'b> SystemBundle<'a, 'b> for PongSystemsBundle {
#     fn build(self, _: &mut World, _: &mut DispatcherBuilder<'a, 'b>) -> Result<(), amethyst::Error> {
#         Ok(())
#     }
# }
#
# let mut world = World::new();
let mut dispatcher_builder = DispatcherBuilder::new();

PongSystemsBundle::default()
    .build(&mut world, &mut dispatcher_builder)
    .expect("Failed to register PongSystemsBundle");

The DispatcherBuilder can be initialized and populated wherever desired, be it inside the State or in an external location. However, the Dispatcher needs to modify the Worlds resources in order to initialize the resources used by its Systems. Therefore, we need to defer building the Dispatcher until we can access the World. This is commonly done in the States on_start method. To showcase how this is done, we'll create a SimpleState with a dispatcher field and a on_start method that builds the Dispatcher.

# extern crate amethyst;
#
# use amethyst::{
#     ecs::prelude::*,
#     prelude::*,
#     core::ArcThreadPool,
# };
#
# struct MoveBallsSystem; struct MovePaddlesSystem;
# impl<'a> System<'a> for MoveBallsSystem { type SystemData = (); fn run(&mut self, _: ()) {} }
# impl<'a> System<'a> for MovePaddlesSystem { type SystemData = (); fn run(&mut self, _: ()) {} }
#
#[derive(Default)]
pub struct CustomState<'a, 'b> {
    /// The `State` specific `Dispatcher`, containing `System`s only relevant for this `State`.
    dispatcher: Option<Dispatcher<'a, 'b>>,
}

impl<'a, 'b> SimpleState for CustomState<'a, 'b> {
    fn on_start(&mut self, mut data: StateData<'_, GameData<'_, '_>>) {
        let world = &mut data.world;
        
        // Create the `DispatcherBuilder` and register some `System`s that should only run for this `State`.
        let mut dispatcher_builder = DispatcherBuilder::new();
        dispatcher_builder.add(MoveBallsSystem, "move_balls_system", &[]);
        dispatcher_builder.add(MovePaddlesSystem, "move_paddles_system", &[]);

        // Build and setup the `Dispatcher`.
        let mut dispatcher = dispatcher_builder
            .with_pool((*world.read_resource::<ArcThreadPool>()).clone())
            .build();
        dispatcher.setup(world);

        self.dispatcher = Some(dispatcher);
    }
}

By default, the dispatcher will create its own pool of worker threads to execute systems in, but Amethyst's main dispatcher already has a thread pool setup and configured. As reusing it is more efficient, we pull the global pool from the world and attach the dispatcher to it with .with_pool().

The CustomState requires two annotations ('a and 'b) to satisfy the lifetimes of the Dispatcher. Now that we have our Dispatcher we need to ensure that it is executed. We do this in the States update method.

# extern crate amethyst;
#
# use amethyst::{
#     ecs::prelude::*,
#     prelude::*,
# };
# 
# #[derive(Default)]
# pub struct CustomState<'a, 'b> {
#     /// The `State` specific `Dispatcher`, containing `System`s only relevant for this `State`.
#     dispatcher: Option<Dispatcher<'a, 'b>>,
# }
# struct MoveBallsSystem; struct MovePaddlesSystem;
# impl<'a> System<'a> for MoveBallsSystem { type SystemData = (); fn run(&mut self, _: ()) {} }
# impl<'a> System<'a> for MovePaddlesSystem { type SystemData = (); fn run(&mut self, _: ()) {} }
# 
impl<'a, 'b> SimpleState for CustomState<'a, 'b> {
    fn on_start(&mut self, mut data: StateData<'_, GameData<'_, '_>>) {
        let world = &mut data.world;
         
        // Create the `DispatcherBuilder` and register some `System`s that should only run for this `State`.
        let mut dispatcher_builder = DispatcherBuilder::new();
        dispatcher_builder.add(MoveBallsSystem, "move_balls_system", &[]);
        dispatcher_builder.add(MovePaddlesSystem, "move_paddles_system", &[]);
 
        // Build and setup the `Dispatcher`.
        let mut dispatcher = dispatcher_builder.build();
        dispatcher.setup(world);
 
        self.dispatcher = Some(dispatcher);
    }
 
    fn update(&mut self, data: &mut StateData<GameData>) -> SimpleTrans {
        if let Some(dispatcher) = self.dispatcher.as_mut() {
            dispatcher.dispatch(&data.world);
        }

        Trans::None
    }
}

Now, any Systems in this State-specific Dispatcher will only run while this State is active and the update method is called.

Pausable Systems

Custom GameData and state-specific Systems are great when it comes to handling groups of System. But when it comes single Systems or a group of Systems spread over multiple Dispatchers or States, pausable Sytems come in handy.

Pausable Systems can be enabled or disabled depending on the value of aResource registered to your World. When this value changes, the state of your System changes as well.

Let's get started by creating a new Resource that represents the state of our game.

#[derive(PartialEq)]
pub enum CurrentState {
    Running,
    Paused,
}

impl Default for CurrentState {
    fn default() -> Self {
        CurrentState::Paused
    }
}

We'll use this enum Resource to control whether or not our System is running. Next we'll register our System and set it as pausable.

# extern crate amethyst;
#
# use amethyst::{
#     ecs::prelude::*,
#     prelude::*,
# };
# 
# #[derive(PartialEq)]
# pub enum CurrentState {
#     Running,
#     Paused,
# }
# 
# impl Default for CurrentState {
#     fn default() -> Self {
#         CurrentState::Paused
#     }
# }
#
# #[derive(Default)] struct MovementSystem;
# 
# impl<'a> System<'a> for MovementSystem {
#   type SystemData = ();
#
#   fn run(&mut self, data: Self::SystemData) {}
# }
# let mut dispatcher = DispatcherBuilder::new();
dispatcher.add(
    MovementSystem::default().pausable(CurrentState::Running),
    "movement_system",
    &["input_system"],
);

pausable(CurrentState::Running) creates a wrapper around our System that controls its execution depending on the CurrentState Resource registered with the World. As long as the value of the Resource is set to CurrentState::Running, the System is executed.

To register the Resource or change its value, we can use the following code:

# extern crate amethyst;
# use amethyst::prelude::*;
# #[derive(PartialEq)]
# pub enum CurrentState {
#    Running,
#    Paused,
# }
# 
# impl Default for CurrentState {
#     fn default() -> Self {
#         CurrentState::Paused
#     }
# }
# 
struct GameplayState;

impl SimpleState for GameplayState {
    fn update(&mut self, data: &mut StateData<'_, GameData<'_, '_>>) -> SimpleTrans {
#       let my_condition = true;
        if (my_condition) {
            *data.world.write_resource::<CurrentState>() = CurrentState::Paused;
        }
        
        Trans::None
    }
}

However, this cannot be done inside the pausable System itself. A pausable System can only access its pause Resource with immutable Read and cannot modify the value, thus the System cannot decide on its own if it should run on not. This has to be done from a different location.

Sprites

Sprites are 2D images that represent an object or background. Sprites are represented by two main chunks of data:

  • Texture: The image made of pixels.
  • Sprite Layout: The (rectangular) coordinates of the sprites on that image.

In Amethyst, these are represented by the Texture and SpriteSheet types respectively. The pages in this section will explain how to set up your application to load and display sprites.

Note: The code snippets in this section explain the parts of setting up sprite rendering separately. For complete application examples, please refer to the sprites_ordered example in the examples directory.

Set Up The Render plugin

Amethyst supports drawing sprites using the RenderFlat2D render plugin. To enable this you have to do the following:

# extern crate amethyst;
#
use amethyst::{
    ecs::{World, WorldExt},
    prelude::*,
    renderer::{
        plugins::RenderFlat2D,
        types::DefaultBackend,
        RenderingBundle,
    }
};
# fn main() -> Result<(), amethyst::Error> {
#
# let game_data = GameDataBuilder::default()
#     .with_bundle(
#
// inside your rendering bundle setup
RenderingBundle::<DefaultBackend>::new()
    .with_plugin(RenderFlat2D::default())

# )?;
# Ok(()) }

Load The Texture

The first part of loading sprites into Amethyst is to read the image into memory.

The following snippet shows how to load a PNG / JPEG / GIF / ICO image:

# extern crate amethyst;
use amethyst::assets::{AssetStorage, Handle, Loader};
use amethyst::prelude::*;
use amethyst::renderer::{formats::texture::ImageFormat, Texture};

pub fn load_texture<N>(name: N, world: &World) -> Handle<Texture>
where
    N: Into<String>,
{
    let loader = world.read_resource::<Loader>();
    loader.load(
        name,
        ImageFormat::default(),
        (),
        &world.read_resource::<AssetStorage<Texture>>(),
    )
}

#[derive(Debug)]
struct ExampleState;

impl SimpleState for ExampleState {
    fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
        let texture_handle = load_texture("texture/sprite_sheet.png", &data.world);
    }
}
#
# fn main() {}

There is one thing that may surprise you.

The loaded texture will use nearest filtering, i.e. the pixels won't be interpolated. If you want to tweak the sampling, you can change ImageFormat::default() to ImageFormat(my_config), and create your own my_config like this:

# extern crate amethyst;
use amethyst::renderer::rendy::hal::image::{Filter, SamplerInfo, WrapMode};
use amethyst::renderer::rendy::texture::image::{ImageTextureConfig, Repr, TextureKind};

let my_config = ImageTextureConfig {
    // Determine format automatically
    format: None,
    // Color channel
    repr: Repr::Srgb,
    // Two-dimensional texture
    kind: TextureKind::D2,
    sampler_info: SamplerInfo::new(Filter::Linear, WrapMode::Clamp),
    // Don't generate mipmaps for this image
    generate_mips: false,
    premultiply_alpha: true,
};

Define The SpriteSheet

With the texture loaded, Amethyst still needs to know where the sprites are on the image. There are two ways to load a sprite sheet definition: from a file or from code.

Load the sheet from a file

The easiest way to load your sprites is to use a sprite sheet definition ron file. Here is an example of such a definition file:

(
    // Width of the texture used by the sprite sheet
    texture_width: 48,
    // Height of the texture used by the sprite sheet
    texture_height: 16,
    // List of sprites the sheet holds
    sprites: [
        (
            // Horizontal position of the sprite in the sprite sheet
            x: 0,
            // Vertical position of the sprite in the sprite sheet
            y: 0,
            // Width of the sprite
            width: 16,
            // Height of the sprite
            height: 16,
            // Number of pixels to shift the sprite to the left and down relative to the entity holding it when rendering
            offsets: Some((0.0, 0.0)), // This is optional and defaults to (0.0, 0.0)
        ),
        (
            x: 16,
            y: 0,
            width: 32,
            height: 16,
        ),
        // etc...
    ],
)

offsets: Some((0.0, 0.0)), can be replaced by offsets: (0.0, 0.0), if the line #![enable(implicit_some)] is added at the top of the definition file.

Then, you can load it using the texture handle of the sheet's image you loaded earlier:

# extern crate amethyst;
# use amethyst::assets::{Loader, AssetStorage, Handle};
# use amethyst::ecs::{World, WorldExt};
# use amethyst::renderer::{SpriteSheetFormat, SpriteSheet, Texture};
#
# fn load_texture() -> Handle<Texture> {
#    unimplemented!()
# }
#
# fn load_sprite_sheet() {
#   let world = World::new();
#   let loader = world.read_resource::<Loader>();
#   let texture_handle = load_texture();
#   let spritesheet_storage = world.read_resource::<AssetStorage<SpriteSheet>>();
let spritesheet_handle = loader.load(
    "my_spritesheet.ron",
    SpriteSheetFormat(texture_handle),
    (),
    &spritesheet_storage,
);
# }

This will get you the Handle<SpriteSheet> you will then use to draw the sprites.

Load the sheet from code

While it is not the recommended way, it is also possible to manually build your sheet with code.

Importantly, we use pixel coordinates as well as texture coordinates to define the sprite layout. Pixel coordinates indicate the dimensions of the sprite to draw on screen; texture coordinates indicate which part of the image contains the sprite, and are expressed as a proportion of the image.

The following table lists the differences between the coordinate systems:

Pixel coordinatesTexture coordinates
Begin at the top left of the imageBegin at the bottom left of the image
Increase to the right and downIncrease to the right and up
Range from 0 to (width or height - 1)Range from 0.0 to 1.0

In Amethyst, pixel dimensions and texture coordinates are stored in the Sprite struct. Since texture coordinates can be derived from pixel coordinates, Amethyst provides the Sprite::from_pixel_values function to create a Sprite.

The following snippet shows you how to naively define a SpriteSheet. In a real application, you would typically use the sprite sheet from file feature, which is much more convenient.

# extern crate amethyst;
use amethyst::assets::Handle;
use amethyst::renderer::{sprite::TextureCoordinates, Sprite, SpriteSheet, Texture};

/// Returns a `SpriteSheet`.
///
/// # Parameters
///
/// * `texture`: Handle of the texture.
pub fn load_sprite_sheet(texture: Handle<Texture>) -> SpriteSheet {
    let sprite_count = 1; // number of sprites
    let mut sprites = Vec::with_capacity(sprite_count);

    let image_w = 100;
    let image_h = 20;
    let sprite_w = 10;
    let sprite_h = 10;

    // Here we are loading the 5th sprite on the bottom row.
    let offset_x = 50; // 5th sprite * 10 pixel sprite width
    let offset_y = 10; // Second row (1) * 10 pixel sprite height
    let offsets = [5.0; 2]; // Align the sprite with the middle of the entity.

    let sprite = Sprite::from_pixel_values(
        image_w, image_h, sprite_w, sprite_h, offset_x, offset_y, offsets, false, false,
    );
    sprites.push(sprite);

    SpriteSheet {
        texture,
        sprites,
    }
}

SpriteRender Component

After loading the SpriteSheet, you need to attach it to an entity using the SpriteRender component and indicate which sprite to draw. The SpriteRender component looks like this:

#[derive(Clone, Debug, PartialEq)]
pub struct SpriteRender {
    /// Handle to the sprite sheet of the sprite
    pub sprite_sheet: Handle<SpriteSheet>,
    /// Index of the sprite on the sprite sheet
    pub sprite_number: usize,
}

The sprite number is the index of the sprite loaded in the sprite sheet. What's left is the Handle<SpriteSheet>.

In the previous section you wrote a function that returns a SpriteSheet. This can be turned into a Handle<SpriteSheet> using the Loader resource as follows:

# extern crate amethyst;
use amethyst::assets::{AssetStorage, Loader, Handle};
# use amethyst::prelude::*;
use amethyst::renderer::{SpriteSheet, Texture};

# pub fn load_texture<N>(name: N, world: &World) -> Handle<Texture>
# where
#     N: Into<String>,
# {
#     unimplemented!();
# }
#
# pub fn load_sprite_sheet(texture: Handle<Texture>) -> SpriteSheet {
#     unimplemented!();
# }
#[derive(Debug)]
struct ExampleState;

impl SimpleState for ExampleState {
    fn on_start(&mut self, mut data: StateData<'_, GameData<'_, '_>>) {
#         let texture_handle = load_texture("texture/sprite_sheet.png", &data.world);
        // ...

        let sprite_sheet = load_sprite_sheet(texture_handle);
        let sprite_sheet_handle = {
            let loader = data.world.read_resource::<Loader>();
            loader.load_from_data(
                sprite_sheet,
                (),
                &data.world.read_resource::<AssetStorage<SpriteSheet>>(),
            )
        };
    }
}
#
# fn main() {}

Cool, finally we have all the parts, let's build a SpriteRender and attach it to an entity:

# extern crate amethyst;
# use amethyst::assets::{AssetStorage, Loader, Handle};
use amethyst::core::transform::Transform;
# use amethyst::prelude::*;
use amethyst::renderer::{
    SpriteRender, SpriteSheet,
    Texture, Transparent
};
use amethyst::window::ScreenDimensions;

# pub fn load_texture<N>(name: N, world: &World) -> Handle<Texture>
# where
#     N: Into<String>,
# {
#     unimplemented!();
# }
#
# pub fn load_sprite_sheet(texture: Handle<Texture>) -> SpriteSheet {
#     unimplemented!();
# }
#[derive(Debug)]
struct ExampleState;

impl SimpleState for ExampleState {
    fn on_start(&mut self, mut data: StateData<'_, GameData<'_, '_>>) {
#         let texture_handle = load_texture("texture/sprite_sheet.png", &data.world);
# 
#         let sprite_sheet = load_sprite_sheet(texture_handle);
#         let sprite_sheet_handle = {
#             let loader = data.world.read_resource::<Loader>();
#             loader.load_from_data(
#                 sprite_sheet,
#                 (),
#                 &data.world.read_resource::<AssetStorage<SpriteSheet>>(),
#             )
#         };
        // ...

        self.initialize_sprite(&mut data.world, sprite_sheet_handle);
    }
}

impl ExampleState {
    fn initialize_sprite(
        &mut self,
        world: &mut World,
        sprite_sheet_handle: Handle<SpriteSheet>,
    ) {
        let (width, height) = {
            let dim = world.read_resource::<ScreenDimensions>();
            (dim.width(), dim.height())
        };

        // Move the sprite to the middle of the window
        let mut sprite_transform = Transform::default();
        sprite_transform.set_translation_xyz(width / 2., height / 2., 0.);

        let sprite_render = SpriteRender {
            sprite_sheet: sprite_sheet_handle,
            sprite_number: 0, // First sprite
        };

        world
            .create_entity()
            .with(sprite_render)
            .with(sprite_transform)
            .with(Transparent) // If your sprite is transparent
            .build();
    }
}
#
# fn main() {}

Got that? Sweet!

Modify The Texture

The colors of the sprite will show up exactly as in the source file, but sometimes being able to slightly modify the overall color is useful - for instance, coloring an angry enemy more red, or making a frozen enemy blue. Amethyst has a Component called Tint to do this.

To use Tint, register Tint as a new Component with the world and build it as part of the entity. Tint will multiply the color values of the sprite by its own values, so a Tint with a white color will have no effect on the sprite.

# extern crate amethyst;
# use amethyst::assets::{AssetStorage, Loader, Handle};
use amethyst::core::transform::Transform;
# use amethyst::prelude::*;
use amethyst::renderer::{
    palette::Srgba,
    resources::Tint,
    SpriteRender, SpriteSheet,
    Texture, Transparent
};
use amethyst::window::ScreenDimensions;

# pub fn load_texture<N>(name: N, world: &World) -> Handle<Texture>
# where
#     N: Into<String>,
# {
#     unimplemented!();
# }
#
# pub fn load_sprite_sheet(texture: Handle<Texture>) -> SpriteSheet {
#     unimplemented!();
# }
#[derive(Debug)]
struct ExampleState;

impl SimpleState for ExampleState {
    fn on_start(&mut self, mut data: StateData<'_, GameData<'_, '_>>) {
#         let texture_handle = load_texture("texture/sprite_sheet.png", &data.world);
# 
#         let sprite_sheet = load_sprite_sheet(texture_handle);
#         let sprite_sheet_handle = {
#             let loader = data.world.read_resource::<Loader>();
#             loader.load_from_data(
#                 sprite_sheet,
#                 (),
#                 &data.world.read_resource::<AssetStorage<SpriteSheet>>(),
#             )
#         };
        // ...

        self.initialize_sprite(&mut data.world, sprite_sheet_handle);
    }
}

impl ExampleState {
    fn initialize_sprite(
        &mut self,
        world: &mut World,
        sprite_sheet_handle: Handle<SpriteSheet>,
    ) {
        // ..

#         let (width, height) = {
#             let dim = world.read_resource::<ScreenDimensions>();
#             (dim.width(), dim.height())
#         };
# 
#         // Move the sprite to the middle of the window
#         let mut sprite_transform = Transform::default();
#         sprite_transform.set_translation_xyz(width / 2., height / 2., 0.);
# 
#         let sprite_render = SpriteRender {
#             sprite_sheet: sprite_sheet_handle,
#             sprite_number: 0, // First sprite
#         };

        // White shows the sprite as normal.
        // You can change the color at any point to modify the sprite's tint.
        let tint = Tint(Srgba::new(1.0, 1.0, 1.0, 1.0));

        world
            .create_entity()
            .with(sprite_render)
            .with(sprite_transform)
            .with(tint)
            .build();
    }
}
#
# fn main() {}

Orthographic Camera

Finally, you need to tell Amethyst to draw in 2D space. This is done by creating an entity with a Camera component using orthographic projection. For more information about orthographic projection, refer to the OpenGL documentation.

The following snippet demonstrates how to set up a Camera that sees entities within screen bounds, where the entities' Z position is between -10.0 and 10.0:

# extern crate amethyst;
use amethyst::{
    core::{math::Orthographic3, transform::Transform},
    prelude::*,
    renderer::camera::{Camera, Projection},
    window::ScreenDimensions,
};

#[derive(Debug)]
struct ExampleState;

impl SimpleState for ExampleState {
    fn on_start(&mut self, mut data: StateData<'_, GameData<'_, '_>>) {
        // ...

        self.initialize_camera(&mut data.world);
    }
}

impl ExampleState {
    fn initialize_camera(&mut self, world: &mut World) {
        let (width, height) = {
            let dim = world.read_resource::<ScreenDimensions>();
            (dim.width(), dim.height())
        };

        // Translate the camera to Z coordinate 10.0, and it looks back toward
        // the origin with depth 20.0
        let mut transform = Transform::default();
        transform.set_translation_xyz(0., height, 10.);

        let mut camera = Camera::standard_3d(width, height);
        camera.set_projection(Projection::orthographic(
            0.0,
            width,
            0.0,
            height,
            0.0,
            20.0,
        ));

        let camera = world
            .create_entity()
            .with(transform)
            .with(camera)
            .build();
    }
}

And you're done! If you would like to see this in practice, check out the sprites or sprites_ordered examples in the examples directory.

Testing

Without a doubt, Amethyst contains many concepts for you to understand and remember. During development, normally each concept's types are written in its own module.

To test that these types work properly often requires them to be run in an Amethyst application. By now you know that there is much boilerplate required to setting up an application simply to test a single system.

The amethyst_test crate provides support to write tests ergonomically and expressively.

The following shows a simple example of testing a State. More examples are in following pages.

# extern crate amethyst;
# extern crate amethyst_test;
#
# use std::marker::PhantomData;
#
# use amethyst_test::prelude::*;
# use amethyst::{
#     ecs::prelude::*,
#     prelude::*,
# };
#
# #[derive(Debug)]
# struct LoadResource;
#
# #[derive(Debug)]
# struct LoadingState;
#
# impl LoadingState {
#     fn new() -> Self {
#         LoadingState
#     }
# }
#
# impl<'a, 'b, E> State<GameData<'a, 'b>, E> for LoadingState
# where
#     E: Send + Sync + 'static,
# {
#     fn update(&mut self, data: StateData<'_, GameData<'_, '_>>) -> Trans<GameData<'a, 'b>, E> {
#         data.data.update(&data.world);
#
#         data.world.insert(LoadResource);
#
#         Trans::Pop
#     }
# }
#
#[test]
fn loading_state_adds_load_resource() -> Result<(), Error> {
    AmethystApplication::blank()
        .with_state(|| LoadingState::new())
        .with_assertion(|world| {
            world.read_resource::<LoadResource>();
        })
        .run()
}

Anatomy of an Amethyst Test Function

The Amethyst application is initialized with one of the following functions, each providing a different set of bundles:

# extern crate amethyst;
# extern crate amethyst_test;
#
use amethyst_test::prelude::*;

#[test]
fn test_name() {
    // Start with no bundles
    AmethystApplication::blank();

    // Start with the following bundles:
    //
    // * `TransformBundle`
    // * `InputBundle`
    // * `UiBundle`
    //
    // The type parameters here are the Axis and Action types for the
    // `InputBundle` and `UiBundle`.
    use amethyst::input::StringBindings;
    AmethystApplication::ui_base::<StringBindings>();

    // If you need types from the rendering bundle, make sure you have
    // the `"test-support"` feature enabled:
    //
    // ```toml
    // # Cargo.toml
    // amethyst = { version = "..", features = ["test-support"] }
    // ```
    //
    // Then you can include the `RenderEmptyBundle`:
    use amethyst::renderer::{types::DefaultBackend, RenderEmptyBundle};
    AmethystApplication::blank()
        .with_bundle(RenderEmptyBundle::<DefaultBackend>::new());
}

Next, attach the logic for your test using the various .with_*(..) methods:

#[test]
fn test_name() {
    let visibility = false; // Whether the window should be shown
    AmethystApplication::render_base::<String, String, _>("test_name", visibility)
        .with_bundle(MyBundle::new())                // Registers a bundle.
        .with_bundle_fn(|| MyNonSendBundle::new())   // Registers a `!Send` bundle.
        .with_resource(MyResource::new())            // Adds a resource to the world.
        .with_system(|_| MySystem::new(), "my_sys", &[]) // Registers a system
                                                     // with the main dispatcher

        // These are run in the order they are invoked.
        // You may invoke them multiple times.
        .with_setup(|world| { /* do something */ })
        .with_state(|| MyState::new())
        .with_effect(|world| { /* do something */ })
        .with_assertion(|world| { /* do something */ })
         // ...
}

Finally, call .run() to run the application. This returns amethyst::Result<()>, so we return that as part of the function:

# extern crate amethyst;
# extern crate amethyst_test;
#
# use amethyst::Error;
# use amethyst_test::prelude::*;
#
#[test]
fn test_name() -> Result<(), Error> {
    let visibility = false; // Whether the window should be shown
    AmethystApplication::render_base("test_name", visibility)
        // ...
        .run()
}

Test Examples

Testing a Bundle

# extern crate amethyst;
# extern crate amethyst_test;
#
# use amethyst_test::prelude::*;
# use amethyst::{
#     core::bundle::SystemBundle,
#     core::SystemDesc,
#     derive::SystemDesc,
#     ecs::prelude::*,
#     prelude::*,
#     Error,
# };
#
# #[derive(Debug)]
# struct ApplicationResource;
#
# #[derive(Debug, SystemDesc)]
# #[system_desc(insert(ApplicationResource))]
# struct MySystem;
#
# impl<'s> System<'s> for MySystem {
#     type SystemData = ReadExpect<'s, ApplicationResource>;
#
#     fn run(&mut self, _: Self::SystemData) {}
# }
#
#[derive(Debug)]
struct MyBundle;

impl<'a, 'b> SystemBundle<'a, 'b> for MyBundle {
    fn build(self, world: &mut World, builder: &mut DispatcherBuilder<'a, 'b>) -> Result<(), Error> {
        // System that adds `ApplicationResource` to the `World`
        builder.add(MySystem.build(world), "my_system", &[]);
        Ok(())
    }
}

// #[test]
fn bundle_registers_system_with_resource() -> Result<(), Error> {
    AmethystApplication::blank()
        .with_bundle(MyBundle)
        .with_assertion(|world| {
            // The next line would panic if the resource wasn't added.
            world.read_resource::<ApplicationResource>();
        })
        .run()
}
#
# fn main() {
#     bundle_registers_system_with_resource();
# }

Testing a System

# extern crate amethyst;
# extern crate amethyst_test;
#
# use amethyst_test::prelude::*;
# use amethyst::{
#     core::SystemDesc,
#     derive::SystemDesc,
#     ecs::prelude::*,
#     prelude::*,
#     Error,
# };
#
struct MyComponent(pub i32);

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

#[derive(Debug, SystemDesc)]
struct MySystem;
impl<'s> System<'s> for MySystem {
    type SystemData = WriteStorage<'s, MyComponent>;
    fn run(&mut self, mut my_component_storage: Self::SystemData) {
        for mut my_component in (&mut my_component_storage).join() {
            my_component.0 += 1
        }
    }
}

// #[test]
fn system_increases_component_value_by_one() -> Result<(), Error> {
    AmethystApplication::blank()
        .with_system(MySystem, "my_system", &[])
        .with_effect(|world| {
            let entity = world.create_entity().with(MyComponent(0)).build();
            world.insert(EffectReturn(entity));
        })
        .with_assertion(|world| {
            let entity = world.read_resource::<EffectReturn<Entity>>().0.clone();

            let my_component_storage = world.read_storage::<MyComponent>();
            let my_component = my_component_storage
                .get(entity)
                .expect("Entity should have a `MyComponent` component.");

            // If the system ran, the value in the `MyComponent` should be 1.
            assert_eq!(1, my_component.0);
        })
        .run()
}
#
# fn main() {
#     system_increases_component_value_by_one();
# }

Testing a System in a Custom Dispatcher

This is useful when your system must run after some setup has been done, for example adding a resource:

# extern crate amethyst;
# extern crate amethyst_test;
#
# use amethyst_test::prelude::*;
# use amethyst::{
#     core::SystemDesc,
#     derive::SystemDesc,
#     ecs::prelude::*,
#     prelude::*,
#     Error,
# };
#
// !Default
struct MyResource(pub i32);

#[derive(Debug, SystemDesc)]
struct MySystem;

impl<'s> System<'s> for MySystem {
    type SystemData = WriteExpect<'s, MyResource>;

    fn run(&mut self, mut my_resource: Self::SystemData) {
        my_resource.0 += 1
    }
}

// #[test]
fn system_increases_resource_value_by_one() -> Result<(), Error> {
    AmethystApplication::blank()
        .with_setup(|world| {
            world.insert(MyResource(0));
        })
        .with_system_single(MySystem, "my_system", &[])
        .with_assertion(|world| {
            let my_resource = world.read_resource::<MyResource>();

            // If the system ran, the value in the `MyResource` should be 1.
            assert_eq!(1, my_resource.0);
        })
        .run()
}
#
# fn main() {
#     system_increases_resource_value_by_one();
# }

Glossary

Data-driven design

Describes a program that has its logic defined largely in data rather than in compiled code. Ideally, this would permit the user to edit their code and resources using offline tools and have the program hot-reload the changes at run-time for instant feedback without the need for recompilation. The bare minimum qualification for a data-driven program is the ability to read external content (text files, scripts, byte streams) and mutate its behavior accordingly.

Data-oriented programming

Not to be confused with data-driven design, data-oriented programming is a programming paradigm, like object-oriented programming (OOP) or procedural programming. Where OOP focuses on modeling a problem in terms of interacting objects, and procedural programming tries to model a problem in terms of sequential or recursive steps or procedures, data-oriented programming shifts the focus towards the data being operated on: the data type, its memory layout, how it will be processed. Software written in a data-oriented manner tends toward high-throughput pipelining, modularity, separation of concerns, and massive parallelism. If architected correctly, data-oriented software can be very cache-friendly and easy to scale on systems with multiple cores.

Note: Data-oriented programming does not necessarily imply that a program is data-driven. Data-driven behavior can be implemented with any programming approach you like.

Entity-component-system (ECS) model

Describes a game programming design pattern invented as a reaction to the deep-rooted problems with using inheritance (is-a relationship) to represent game objects, including the deadly diamond of death and god objects. The inheritance-based approach was especially common in the game industry during the 1990's and early 2000's.

This alternative model makes use of composition (has-a relationship) instead of inheritance to represent objects in the game world, flattening the hierarchy and eliminating the problems above, while increasing flexibility. The holistic ECS approach is broken into three key pieces:

  1. Entity: Represents a single object in the game world. Has no functionality on its own. The world owns a collection of entities (either in a flat list or a hierarchy). Each entity has a unique identifier or name, for the sake of ease of use.
  2. Component: A plain-old-data structure that describes a certain trait an entity can have. Can be "attached" to entities to grant them certain abilities, e.g. a Light component contains parameters to make an entity glow, or a Collidable component can grant an entity collision detection properties. These components do not have any logic. They contain only data.
  3. System: This is where the magic happens! Systems are centralized game engine subsystems that perform a specific function, such as rendering, physics, audio, etc. Every frame, they process each entity in the game world looking for components that are relevant to them, reading their contents, and performing actions. For example, a Rendering system could search for all entities that have Light, Mesh, or Emitter components and draw them to the screen.

This approach could potentially be stretched to fit the model-view-controller (MVC) paradigm popular in GUI and Web development circles: entities and components together represent the model, and systems represent either views (Rendering, Audio) or controllers (Input, AI, Physics), depending on their purpose.

Another great advantage of the ECS model is the ability to rapidly prototype a game simply by describing objects' characteristics in terms of creating entities and attaching components to them, with very little game code involved. And all of this data can be easily serialized or de-serialized into a human-friendly plain text format like RON (Json derivative).

For more detailed explanations of entity-component-system designs, please see this great post on Reddit and this Stack Overflow answer.

Appendix A: Config Files

In the full Pong example, the paddle sizes, ball sizes, colors, and arena size are all hard-coded into the implementation. This means that if you want to change any of these, you need to recompile the project. Wouldn't it be nice to not have to recompile the project each time you wanted to change one or all of these things?

Luckily, Amethyst uses RON configuration files and has infrastructure in the form of the Config trait to help us implement our own config files.

Structure of the Config File

The existing example uses the following constants:

const ARENA_HEIGHT: f32 = 100.0;
const ARENA_WIDTH: f32 = 100.0;
const PADDLE_HEIGHT: f32 = 15.0;
const PADDLE_WIDTH: f32 = 2.5;
const PADDLE_VELOCITY: f32 = 75.0;
const PADDLE_COLOR: [f32; 4] = [0.0, 0.0, 1.0, 1.0];

const BALL_VELOCITY_X: f32 = 75.0;
const BALL_VELOCITY_Y: f32 = 50.0;
const BALL_RADIUS: f32 = 2.5;
const BALL_COLOR: [f32; 4] = [1.0, 0.0, 0.0, 1.0];

to specify the look of the game. We want to replace this with something more flexible in the form of a config file. To start, let's create a new file, config.rs, to hold our configuration structures. Add the following use statements to the top of this file:

use std::path::Path;

use amethyst::config::Config;

For this project, we'll be placing a config.ron file in the same location as the display.ron and input.ron files (likely the config/ folder).

Chapters

Adding an Arena Config

To begin with, let's make the Arena dimensions configurable. Add this structure to a new file config.rs.

#[derive(Debug, Deserialize, Serialize)]
struct ArenaConfig {
    pub height: f32,
    pub width: f32,
}

impl Default for ArenaConfig {
    fn default() -> Self {
        ArenaConfig {
            height: 100.0,
            width: 100.0,
        }
    }
}

The default values match the values used in the full example, so if we don't use a config file things will look just like the Pong example. Another option would be to use [#serde(default)], which allows you to set the default value of a field if that field is not present in the config file. This is different than the Default trait in that you can set default values for some fields while requiring others be present. For now though, let's just use the Default trait.

Adding the Config to the World

Now, in main.rs, add the following lines:

use crate::config::ArenaConfig;

We'll need to load the config at startup, so let's add this to the run function in main.rs

let arena_config = ArenaConfig::load(&config);

Now that we have loaded our config, we want to add it to the world so other modules can access it. We do this by adding the config as a resource during Application creation:

    .with_resource(arena_config)
    .with_bundle(PongBundle::default())?

Now for the difficult part: replacing every use of ARENA_WIDTH and ARENA_HEIGHT with our config object. First, let's change our initialisation steps in pong.rs.

Add the following line to the top of pong.rs:

use crate::config::ArenaConfig;

Now, in the initialise_paddles() function, add the following lines after the initialisation of the left_transform and right_transform.

let (arena_height, arena_width) = {
    let config = &world.read_resource::<ArenaConfig>();
    (config.height, config.width)
};

Now replace all references to ARENA_HEIGHT with arena_height and all references to ARENA_WIDTH with arena_width. Do this for each initialisation function in pong.rs.

Accessing Config Files from Systems

It is actually simpler to access a Config file from a system than via the World directly. To access it in the System's run() function, add it to the SystemData type. This is what the BounceSystem looks like when it wants to access the ArenaConfig.

use crate::config::ArenaConfig;
...
type SystemData = (
    WriteStorage<'s, Ball>,
    ReadStorage<'s, Paddle>,
    ReadStorage<'s, Transform>,
    Read<'s, AssetStorage<Source>>,
    ReadExpect<'s, Sounds>,
    Read<'s, Option<Output>>,
    Read<'s, ArenaConfig>,
);
...
fn run(&mut self,
       (mut balls, paddles, transforms, storage, sounds, audio_output, arena_config): SystemData) {

Now, in the run() function, replace the reference to ARENA_HEIGHT with arena_config.height.

Add Read<'s, ArenaConfig> to the WinnerSystem and PaddleSystem as well, replacing the reference to ARENA_WIDTH with arena_config.width.

Making config.ron

Now for the final part: actually creating our config.ron file. This will be very simple right now, and expand as we add more configurable items. For now, just copy and paste the following into a new file. Feel free to modify the height and width if you want.

arena: (
    height: 100.0,
    width: 100.0,
)

Adding a Ball Config

For simplicity, we will wrap all of our Config objects into a single PongConfig object backed by a single config.ron file, but know that you can just as easily keep them in separate files and read from each file separately.

To prepare for our BallConfig, add the following line to the top of config.rs:

use amethyst::core::math::Vector2;

The BallConfig will replace the BALL_VELOCITY_X, BALL_VELOCITY_Y, BALL_RADIUS, and BALL_COLOR variables. We'll use a Vector2 to store the velocity for simplicity and to demonstrate how to add a non-trivial data type to a RON file. The BALL_COLOR was originally an array, but [Serde][serde] and RON handle arrays as tuples, so it will read in a tuple and convert the color values to an array if needed by a particular function (e.g., in pong.rs).

#[derive(Debug, Deserialize, Serialize)]
pub struct BallConfig {
    pub velocity: Vector2<f32>,
    pub radius: f32,
    pub color: (f32, f32, f32, f32),
}

We'll also add the Default trait to this config that will match what the full example uses.

impl Default for BallConfig {
    fn default() -> Self {
        BallConfig {
            velocity: Vector2::new(75.0, 50.0),
            radius: 2.5,
            color: (1.0, 0.0, 0.0, 1.0),
        }
    }
}

Still in config.rs, add the following structure definition at the very bottom. This structure will be backed by the whole config.ron file.

#[derive(Debug, Default, Deserialize, Serialize)]
pub struct PongConfig {
    pub arena: ArenaConfig,
    pub ball: BallConfig,
}

Replacing Ball Constants

Now we need to replace our usage of the BALL_* constants with our new BallConfig.

We use these values in pong.rs in the initialise_ball() function, so the substitution is even simpler than the ArenaConfig.

In pong.rs, underneath our loading of the ArenaConfig, add the following lines

let (velocity_x, velocity_y, radius, color) = {
    let config = world.read_resource::<BallConfig>();
    let c: [f32; 4] = [
        config.color.0,
        config.color.1,
        config.color.2,
        config.color.3,
    ];
    (config.velocity.x, config.velocity.y, config.radius, c)
};

Our functions expect a [f32; 4] array, so we had to convert the tuple to an array. This is relatively simple to do, but for more complex arrays it might be worth it to add a function to the impl BallConfig to avoid duplicating this effort.

Now, within the initialise_ball function, replace BALL_VELOCITY_X with velocity_x, BALL_VELOCITY_Y with velocity_y, BALL_RADIUS with radius, and BALL_COLOR with color.

Modifying the initialisation

Now we will modify our application initialisation. We don't want everyone to always access all the config files, so we need to add each resource separately so systems can use only what they want.

First, we need to change what main.rs is using. Change

use crate::config::ArenaConfig;

to

use crate::config::PongConfig;

Now, modify the run() function, from

let arena_config = ArenaConfig::load(&config);
[..]
    .with_bundle(PongBundle::default())?
[..]
    .with_resource(arena_config)

to

let pong_config = PongConfig::load(&config);
[..]
    .with_bundle(PongBundle::default())?
[..]
    .with_resource(pong_config.arena)
    .with_resource(pong_config.ball)

Adding the BallConfig to config.ron

Now we need to modify our configuration file to allow multiple structures to be included. This is actually very easy with RON; we just add an additional level of nesting.

(
    arena: (
        height: 100.0,
        width: 100.0,
    ),
    ball: (
        velocity: Vector2(
            x: 75.0,
            y: 50.0,
        ),
        radius: 2.5,
        color: (1.0, 0.647, 0.0, 1.0),
    ),
)

This configuration sets the ball to be orange, while retaining the same size and velocity as the original example.

Adding Paddle Configs

We're finally going to add a configuration struct for our Paddles. Because our Pong clone supports two players, we should let them configure each separately. Add the following to the config.rs file:

#[derive(Debug, Deserialize, Serialize)]
pub struct PaddleConfig {
    pub height: f32,
    pub width: f32,
    pub velocity: f32,
    pub color: (f32, f32, f32, f32),
}

impl Default for PaddleConfig {
    fn default() -> Self {
        PaddleConfig {
            height: 15.0,
            width: 2.5,
            velocity: 75.0,
            color: (0.0, 0.0, 1.0, 1.0),
        }
    }
}

Just like the BallConfig, we need to read in the color as a tuple instead of an array.

Now, to allow us to have two separate PaddleConfigs, we will wrap them in a bigger structure as follows:

#[derive(Debug, Default, Deserialize, Serialize)]
pub struct PaddlesConfig {
    pub left: PaddleConfig,
    pub right: PaddleConfig,
}

Now we need to add the PaddlesConfig to our PongConfig as shown below

pub struct PongConfig {
    pub arena: ArenaConfig,
    pub ball: BallConfig,
    pub paddles: PaddlesConfig,
}

and modify the main.rs's run() function to add our PaddleConfigs.

    .with_resource(pong_config.arena)
    .with_resource(pong_config.ball)
    .with_resource(pong_config.paddles)
    .with_bundle(PongBundle::default())?

We add the PaddlesConfig to the World, rather than as separate left and right configurations because Systems can only access resources with ID 0. Any resource added using World::add_resource is added using a default ID of 0. You must use World::add_resource_with_id to add multiple resources of the same type, but then the Systems cannot properly differentiate between them.

Replacing Constants with Configs

Replacing all instances of PADDLE_* will be similar to the BallConfig, as we only use those values for creating the paddle entities. However, we will need to separate the PaddlesConfig into left and right. To avoid issues with the borrow checker, we read the PaddlesConfig once and copy all of the values, unwrapping them in one big assignment statement. In initialise_paddles() in pong.rs, add this code below reading the ArenaConfig.

let (
    left_height,
    left_width,
    left_velocity,
    left_color,
    right_height,
    right_width,
    right_velocity,
    right_color,
) = {
    let config = &world.read_resource::<PaddlesConfig>();
    let cl: [f32; 4] = [
        config.left.color.0,
        config.left.color.1,
        config.left.color.2,
        config.left.color.3,
    ];
    let cr: [f32; 4] = [
        config.right.color.0,
        config.right.color.1,
        config.right.color.2,
        config.right.color.3,
    ];
    (
        config.left.height,
        config.left.width,
        config.left.velocity,
        cl,
        config.right.height,
        config.right.width,
        config.right.velocity,
        cr,
    )
};

Now, within this function, replace

let y = (arena_height - PADDLE_HEIGHT) / 2.0;

with

let left_y = (arena_height - left_height) / 2.0;
let right_y = (arena_height - right_height) / 2.0;

You will also need to repeat the calls to create_mesh and create_color_material() so that you have a left and right mesh and left and right color.

Now, use the left- and right-specific values in the world.create_entity() calls.

Modifying config.ron

Now for the final modification of our config.ron file. For fun, let's make the right paddle yellow and keep the left paddle blue so the final config.ron file will be as follows:

(
    arena: (
        height: 100.0,
        width: 100.0,
    ),
    ball: (
        velocity: Vector2(
            x: 75.0,
            y: 50.0,
        ),
        radius: 2.5,
        color: (1.0, 0.647, 0.0, 1.0),
    ),
    paddles: (
        left: (
            height: 15.0,
            width: 2.5,
            velocity: 75.0,
            color: (0.0, 0.0, 1.0, 1.0),
        ),
        right: (
            height: 15.0,
            width: 2.5,
            velocity: 75.0,
            color: (0.0, 1.0, 1.0, 1.0),
        ),
    )
)

Appendix B: Migration Notes

At times Amethyst goes through non-trivial changes which may impose additional effort to upgrade. This section contains migration notes to reduce this effort.

cgmath to nalgebra

How To Use

This cheat sheet is split up into the following sections:

  • Type Changes: Previously you used this::Type, now you use another::Thing
  • Logic Changes: Previously you had object.method(args), now you use object.other(stuff)

Most changes will have accompanying explanations and code examples on how to switch.

This document is by no means exhaustive, so if there is something missing, or if you can clarify any changes, please correct this!

The text is designed to be searchable, so if you are looking for a specific type or method, please use Ctrl + F: TypeName. If you cannot find it in the document, likely we missed it during writing. Please send us a pull request!

Type Changes

Many types retain the same type name, just under the nalgebra namespace:

-use amethyst::core::cgmath::{Vector2, Vector3, Matrix4};
+use amethyst::core::math::{Vector2, Vector3, Matrix4};

We will not list the names of every type with the same simple name, but will try to list the changes for types whose simple names are different:

-cgmath::Ortho
+math::Orthographic3

-cgmath::PerspectiveFov
+math::Perspective3

Logic Changes

  • cgmath to nalgebra functions:

    -Vector3::unit_z()
    +Vector3::z()
    
    -matrix4.z.truncate()
    +matrix4.column(2).xyz().into()
    
    -matrix4.transform_point(origin)
    +matrix4.transform_point(&origin)
    
  • amethyst::core::transform::Transform

    • Transformation values are accessed / mutated through accessor methods.

      -transform.translation = Vector3::new(5.0, 2.0, -0.5);
      -transform.scale = Vector3::new(2.0, 2.0, 2.0);
      -transform.rotation = Quaternion::new(1.0, 0.0, 0.0, 0.0);
      +transform.set_translation_xyz(5.0, 2.0, -0.5);
      +transform.set_scale(2.0, 2.0, 2.0);
      +transform.set_rotation(Unit::new_normalize(Quaternion::new(1.0, 0.0, 0.0, 0.0)));
      
      // Translations
      -transform.translation = Vector3::new(0.0, 0.0, 0.0);
      +*transform.translation_mut() = Vector3::new(0.0, 0.0, 0.0);
      
      -transform_0.translation - transform_1.translation
      +transform_0.translation() - transform_1.translation()
      
      -transform.translation[0] = x;
      +transform.set_translation_x(position.x);
      
      -translation.x += 0.1;
      -translation.y -= 0.1;
      +transform.prepend_translation_x(0.1);
      +transform.prepend_translation_y(-0.1);
      // or
      +transform.translation_mut().x += 0.1;
      +transform.translation_mut().y -= 0.1;
      
      -let ball_x = transform.translation[0];
      +let ball_x = transform.translation().x;
      
      -transform.set_position(Vector3::new(6.0, 6.0, -6.0));
      +transform.set_translation_xyz(6.0, 6.0, -6.0);
      // or
      *transform.translation_mut() = Vector3::new(6.0, 6.0, -6.0);
      
      // Rotations
      -transform.rotation = [1.0, 0.0, 0.0, 0.0].into();
      +use amethyst::core::math::{Quaternion, Unit};
      +
      +*transform.rotation_mut() = Unit::new_normalize(Quaternion::new(
      +    1.0, // w
      +    0.0, // x
      +    0.0, // y
      +    0.0, // z
      +));
      
      -use amethyst::core::cgmath::Deg;
      -
      -transform.set_rotation(Deg(75.96), Deg(0.0), Deg(0.0));
      +transform.set_rotation_x_axis(1.3257521);
      // or
      +transform.set_rotation_euler(1.3257521, 0.0, 0.0);
      
      // Scaling
      -transform.scale = Vector3::new(1.0, 1.0, 1.0);
      +*transform.scale_mut() = Vector3::new(1.0, 1.0, 1.0);
      
    • amethyst::core::transform::Transform prefabs no longer use labels

       // scene.ron
       data: (
           transform: (
      -        translation: (x: 0.0, y: 0.0, z: -4.0),
      -        rotation: (s: 0.0, v: (x: 0.0, y: 1.0, z: 0.0),),
      -        scale: (x: 4.0, y: 2.0, z: 1.0),
      +        translation: (0.0, 0.0, -4.0),
      +        rotation: (0.0, 0.0, 1.0, 0.0),
      +        scale: (4.0, 2.0, 1.0),
           ),
      
  • amethyst::renderer::GlobalTransform inverse.

    -global.0.invert()
    +global.0.try_inverse()
    
  • amethyst::renderer::Pos* fields use nalgebra types instead of arrays.

    Type change:

     pub struct PosTex {
    -    pub position: [f32; 3],
    -    pub tex_coord: [f32; 2],
    +    pub position: Vector3<f32>,
    +    pub tex_coord: Vector2<f32>,
     }
    

    Usage changes:

     PosTex {
    -    position: [0.0, 0.0, 0.0],
    -    tex_coord: [0.0, 0.0],
    +    position: Vector3::new(0.0, 0.0, 0.0),
    +    tex_coord: Vector2::new(0.0, 0.0),
     }
    
  • amethyst::core::math::Matrix4 construction.

    -Matrix4::from_translation(Vector3::new(x, y, z))
    +Matrix4::new_translation(&Vector3::new(x, y, z))
    
    // OR
    
    +use amethyst::core::math::Translation3;
    +
    +Translation3::new(x, y, z).to_homogeneous()
    
  • UnitQuarternion::rotation_between is right handed, previously they were left handed.

  • Orthographic projection has changed from (left, right, top, bottom) to (left, right, bottom, top).

     Projection::orthographic(
         0.0,           // left
         ARENA_WIDTH,   // right
    -    ARENA_HEIGHT,  // top
         0.0,           // bottom
    +    ARENA_HEIGHT,  // top
     )
    
    -use amethyst::core::cgmath::Ortho;
    -
    -Ortho { left, right, top, bottom, near, far }
    +use amethyst::core::math::Orthographic3;
    +
    +Orthographic3::new(left, right, bottom, top, near, far)
    
  • Perspective projection

    • Angles are specified in radians:

      use amethyst::renderer::Projection;
      -amethyst::core::cgmath::Deg;
      
       Projection::perspective(
           1.33333,
      -    Deg(90.0)
      +    std::f32::consts::FRAC_PI_2,
       )
      
      // scene.ron
       data: (
           camera: Perspective((
               aspect: 1.3,
      -        fovy: Rad (1.0471975512),
      +        fovy: 1.0471975512,
               // ...
           )),
       )
      
    • Prefab fields have been renamed:

      // scene.ron
       data: (
           camera: Perspective((
               aspect: 1.3,
      -        fovy: Rad (1.0471975512),
      +        fovy: 1.0471975512,
      -        near: 0.1,
      -        far: 2000.0,
      +        znear: 0.1,
      +        zfar: 2000.0,
           )),
       )
      
  • amethyst::renderer::SpotLight angle has changed from degrees to radians.

     SpotLight {
    -    angle: 60.0,
    +    angle: std::f32::consts::FRAC_PI_3,
         ..
     }
    
  • amethyst::renderer::SunLight angle has changed from degrees to radians.

     SunLight {
    -    ang_rad: 0.0093,
    +    ang_rad: 0.0093_f32.to_radians(),
         ..
     }
    

Rendy: Migration Guide

Audio

  • AudioFormat no longer exists, you have to use the lower level types -- Mp3Format, WavFormat, OggFormat, FlacFormat.

Assets

  • SimpleFormat trait has merged into Format.
  • Format::Options associated type has been removed; options are now stored in the format instance.
  • NAME associated constant is now a method call.
  • Format<A> type parameter now takes in Format<D>, where D is A::Data.
  • Implement import_simple instead of import.
  • Loader::load no longer takes in the Options parameter.

Input

  • Bindings<String, String> is now Bindings<StringBindings>.

  • Bindings<AX, AC> is now Bindings<T>, where T is a new type you must implement:

    pub struct ControlBindings;
    
    impl BindingTypes for ControlBindings {
        type Axis = PlayerAxisControl;
        type Action = PlayerActionControl;
    }
    

    Diff:

    -Bindings<PlayerAxisControl, PlayerActionControl>
    +Bindings<ControlBindings>
    
  • InputBundle type parameters:

    -InputBundle::<String, String>::new()
    +InputBundle::<StringBindings>::new()
    
  • UiBundle type parameters:

    +use amethyst::renderer::types::DefaultBackend;
    
    -UiBundle::<String, String>::new()
    +UiBundle::<DefaultBackend, StringBindings>::new()
    

Window

  • DisplayConfig's fullscreen field is now an Option<MonitorIdent>. MonitorIdent is MonitorIdent(u16, String), indicating the native monitor display ID, and its name.

  • WindowBundle is now separate from amethyst_renderer.

    use amethyst::window::WindowBundle;
    
    game_data.with_bundle(WindowBundle::from_config_file(display_config_path))?;
    

    This system is loaded automatically by the RenderToWindow render plugin.

Renderer

  • amethyst::renderer::VirtualKeyCode is now amethyst::input::VirtualKeyCode

  • amethyst::renderer::DisplayConfig is now amethyst::window::DisplayConfig

  • amethyst::renderer::WindowEvent is now amethyst::winit::WindowEvent

  • amethyst::renderer::Event is no longer re-exported. Use amethyst::winit::Event

  • amethyst::renderer::Transparent is now under amethyst::renderer::transparent::Transparent.

  • amethyst::renderer::Visibility is now under amethyst::renderer::visibility::Visibility.

  • TextureHandle type alias no longer exists, use Handle<Texture>.

  • Flipped component is removed. You can specify flipped during sprite loading, or mutating Transform at run time.

  • To load a texture in memory, you can't use [0.; 4].into() as the TextureData anymore. Use:

    use amethyst::{
        assets::{AssetStorage, Handle, Loader, Prefab, PrefabLoader},
        ecs::World,
        renderer::{
            loaders::load_from_srgba,
            palette::Srgba,
            types::TextureData,
            Texture,
        },
    };
    
    let loader = world.read_resource::<Loader>();
    let texture_assets = world.read_resource::<AssetStorage<Texture>>();
    let texture_builder = load_from_srgba(Srgba::new(0., 0., 0., 0.));
    let texture_handle: Handle<Texture> =
        loader.load_from_data(TextureData::from(texture_builder), (), &texture_assets);
    
  • RenderBundle and Pipeline are gone, now you need to use the RenderingBundle, for example:

    In main.rs:

    use amethyst::renderer::{types::DefaultBackend, RenderingSystem};
    
    let game_data = GameDataBuilder::default()
        .with_bundle(
            RenderingBundle::<DefaultBackend>::new()
                .with_plugin(
                    RenderToWindow::from_config_path(display_config)
                        .with_clear([0.34, 0.36, 0.52, 1.0]),
                )
                .with_plugin(RenderShaded3D::default())
                .with_plugin(RenderDebugLines::default())
                .with_plugin(RenderSkybox::with_colors(
                    Srgb::new(0.82, 0.51, 0.50),
                    Srgb::new(0.18, 0.11, 0.85),
                )),
        )?;
    
  • Render passes can be integrated into amethyst by using the newly introduced RenderPlugin trait, for example:

    pub struct RenderCustom {
        target: Target,
    }
    
    impl RenderTerrain {
        /// Set target to which 2d sprites will be rendered.
        pub fn with_target(mut self, target: Target) -> Self {
            self.target = target;
            self
        }
    }
    
    
    impl<B: Backend> RenderPlugin<B> for RenderCustom {
        fn on_build<'a, 'b>(
            &mut self,
            builder: &mut DispatcherBuilder<'a, 'b>,
        ) -> Result<(), Error> {
            // You can add systems that are needed by your renderpass here
            Ok(())
        }
    
        fn on_plan(
            &mut self,
            plan: &mut RenderPlan<B>,
            _factory: &mut Factory<B>,
            _res: &Resources,
        ) -> Result<(), Error> {
            plan.extend_target(self.target, |ctx| {
                ctx.add(RenderOrder::Opaque, DrawCustomDesc::new().builder())?;
                Ok(())
            });
            Ok(())
        }
    }
    
  • RenderBundle::with_sprite_sheet_processor() is replaced by:

    game_data.with(
        Processor::<SpriteSheet>::new(),
        "sprite_sheet_processor",
        &[],
    );
    

    This system is added automatically by each of the 3D render plugins (RenderPbr3D, RenderShaded3D, RenderFlat3D).

  • RenderBundle::with_sprite_visibility_sorting() is replaced by:

    use amethyst::rendy::sprite_visibility::SpriteVisibilitySortingSystem;
    
    game_data.with(
        SpriteVisibilitySortingSystem::new(),
        "sprite_visibility_system",
        &["transform_system"],
    );
    

    This system is added automatically by the RenderFlat2D render plugin.

  • Sprite transparency is no longer a separate flag. Instead of with_transparency, you add a second render pass using DrawFlat2DTransparent. See the sprites_ordered example.

Camera changes:

  • CameraPrefab is no longer nested:

    -camera: Perspective((aspect: 1.3, fovy: 1.0471975512, znear: 0.1, zfar: 2000.0))
    +camera: Perspective(aspect: 1.3, fovy: 1.0471975512, znear: 0.1, zfar: 2000.0)
    
  • nalgebra's Perspective3/Orthographic3 are no longer compatible, as they use OpenGL coordinates instead of Vulkan.

    Amethyst now has amethyst::rendy::camera::Orthographic and Perspective, respectively. These types are mostly feature-parity with nalgebra, but correct for vulkan. You can use as_matrix to get the inner Matrix4 value.

  • Camera now stores Projection, which means it is type-safe.

  • You can no longer serialize raw camera matrices, only camera parameter types.

Z-axis direction clarifications:

  • In Vulkan, Z+ is away.
  • in OpenGL, Z- is away.
  • In amethyst_renderer, Z- is away (world coordinates).
  • In amethyst_rendy, Z- is away (world coordinates).

Amethyst Test

  • The render_base function has been changed:

    -let visibility = false;
    -AmethystApplication::render_base("test_name", visibility);
    +use amethyst::renderer::{types::DefaultBackend, RenderEmptyBundle};
    +AmethystApplication::blank()
    +    .with_bundle(RenderEmptyBundle::<DefaultBackend>::new());
    
  • The mark_render() and .run() chained call is replaced by a single run_isolated() call.

Specs Migration

  • Specs migration

    Quick fix:

    • Add use amethyst::ecs::WorldExt to imports.
    • Replace world.add_resource with world.insert.
    • Regex replace \bResources\b with World. Check for false replacements.
    • Replace world.res with world.
    • Regex replace \bres\b with world.

    shred-derive is re-exported by amethyst. Migration steps:

    • Remove shred-derive from Cargo.toml.
    • Remove use amethyst::ecs::SystemData from imports (if present).
    • Add use amethyst::shred::{ResourceId, SystemData} to imports.
  • PrefabLoaderSystem is initialized by PrefabLoaderSystemDesc.

    Quick fix:

    • Find: PrefabLoaderSystem::<([A-Za-z]+)>::default\(\),
    • Replace: PrefabLoaderSystemDesc::<\1>::default()
  • GltfSceneLoaderSystem is initialized by GltfSceneLoaderSystemDesc.

    Quick fix:

    • Find: GltfSceneLoaderSystem::<([A-Za-z]+)>::default\(\),
    • Replace: GltfSceneLoaderSystemDesc::<\1>::default()
  • AmethystApplication::with_setup runs the function before the dispatcher.

    Quick fix:

    • Find: with_setup,
    • Replace: with_effect
  • Renamed UiTransformBuilder to UiTransformData.

  • Renamed UiTextBuilder to UiTextData.

  • Renamed UiButtonBuilder to UiButtonData.

Appendix C: Feature Gates

Various feature gate exist in Amethyst, with different purposes. In this chapter, we will go through each of the feature gate types.

Crate Enabling Feature Gates

To reduce compilation times, you can disable features that are not needed for your project.

When compiling, you can use the following Cargo parameters:

cargo (build/test/run) --no-default-features --features feature1,feature2,feature3

At the time of writing, the list of features of this type is the following:

  • animation
  • audio
  • gltf
  • locale
  • network
  • renderer
  • saveload
  • sdl_controller

The full list of available features is available in the Cargo.toml file. The available features might change from time to time.

Graphics features

Whenever you run your game, you'll need to enable one graphics backend. The following features are available for the backend:

  • empty
  • metal
  • vulkan

Rendy has multiple safety checks built-in to detect bugs in the data it gets submitted. However, those checks can become too costly for a smooth experience with larger games; you can disable them using the no-slow-safety-checks feature.

Additionally, there's a shader-compiler feature which allows compiling GLSL / HLSL to SPIR-V shaders. This is only needed if you're planning to compile shaders at runtime. Amethyst's built-in shaders come pre-compiled, and you can also precompile your own using glslc (provided by shaderc). Please note, that on Windows this feature requires Ninja to be installed.

Using Amethyst testing utility

As described in the Testing chapter, Amethyst has several utilities to help you test an application written using Amethyst. For some cases (especially when rendering components are involved in the test), you need to enable the test-support feature.

Profiling

To enable the profiler, you can use the following feature:

cargo (build/test/run) --features profiler

The next time you will run a project, upon closing it, a file will be created at the root of the project called thread_profile.json. You can open this file using the chromium browser (or google chrome) and navigating to chrome://tracing

Nightly

Enabling the nightly feature adds a bit of debug information when running into runtime issues. To use it, you need to use the nightly rust compiler toolchain.

Here is how to enable it:

cargo (build/test/run) --features nightly

The most common use of this feature is to find out the type name of the resource that is missing, such as when a Resources::fetch() or World::read_resource() invocation fails.

Amethyst as a dependency

When using Amethyst as a dependency of your project, you can use the following to disable default features and enable other ones.

[dependencies.amethyst]
version = "*"
default-features = false 
features = ["audio", "animation"] # you can add more or replace those