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 suite 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 three sections (more coming). This page is the first. The others are:

Read the crate-level API documentation for more details.

Please note that the default github branch is develop, while the documentation in this document is based on the master/release branch. The documentation for the develop branch is located here.

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 multithread/multicores CPU. With 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.

Contributing

We are always happy to welcome new contributors!

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

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.

Required dependencies

If you are on Linux, you'll need to install libasound2-dev and libx11-xcb-dev.

Setting up Amethyst

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

amethyst new game

you should get Cargo.toml, src/main.rs and resources/display_config.ron. 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 resources folder and put a display_config.ron in it.
  • Start with one of the [examples][ex] from the Amethyst repository (e.g. window) for the source code. Watch out to use the right example for the version of Amethyst you specified in Cargo.toml.

We don't have any tutorials yet, but there's a Gitter room where you can ask in case you want an explanation for something. If you'd like to help out, a tutorial would be much appreciated!

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. If you've cloned the Amethyst repo, you can run any of the examples like so:

cargo run --example pong_tutorial_01

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

For instance, in the pong_tutorial_01 example we have:

let path = format!(
    "{}/examples/pong_tutorial_01/resources/display_config.ron",
    env!("CARGO_MANIFEST_DIR"));

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

let path = "./resources/display_config.ron";

Chapters

Opening (and closing!) a window

Let's start a new project:

amethyst new pong

If you run this project with cargo run, you'll end up with a window titled "pong" that renders a really delightful shade of green. Press Esc to quit. If you're having trouble getting the project to run, double check the Getting Started guide.

We've opened and closed a window, so we're basically done! But let's write this functionality ourselves so we're sure we know what's going on.

In src there's a main.rs file. Delete everything, then add these imports:

extern crate amethyst;

use amethyst::prelude::*;
use amethyst::input::{is_close_requested, is_key_down};
use amethyst::renderer::{DisplayConfig, DrawFlat, Event, KeyboardInput,
                         Pipeline, PosTex, RenderBundle, Stage,
                         VirtualKeyCode, WindowEvent};

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.

Now we create our core game struct:

pub struct Pong;

We'll be implementing the State trait on this struct, which is used by Amethyst's state machine to start, stop, and update the game. But for now we'll just implement two methods:

impl<'a, 'b> State<GameData<'a, 'b>> for Pong {
    fn handle_event(&mut self, _: StateData<GameData>, event: Event) -> Trans<GameData<'a, 'b>> {
        if is_close_requested(&event) || is_key_down(&event, VirtualKeyCode::Escape) {
            Trans::Quit
        } else {
            Trans::None
        }
    }

    fn update(&mut self, data: StateData<GameData>) -> Trans<GameData<'a, 'b>> {
        data.data.update(&data.world);
        Trans::None
    }
}

The handle_event method is executed for every event before updating, and it's used to react to events. It returns a Trans, which is an enum of state machine transitions. In this case, we're watching for the Escape keycode, and the CloseRequested event from the Window. If we receive it, we'll return Trans::Quit which will be used to clean up the State and close the application. All other keyboard input is ignored or now.

The update method is executed after events have happened. In this instance we're just using it to execute our dispatcher. More on that later.

Now that we know we can quit, 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.

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

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

    Ok(())
}

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

amethyst::start_logger(Default::default());

After the logger is started, we define a path for our display_config.ron file and load it.

let path = "./resources/display_config.ron";

let config = DisplayConfig::load(&path);

This .ron file was automatically generated by amethyst new. If you didn't use amethyst new, now would be a good time to create this config file inside a folder named resources. If you already have this file, we have some changes to make, anyway:

(
  title: "Pong!",
  dimensions: Some((500, 500)),
  max_dimensions: None,
  min_dimensions: None,
  fullscreen: false,
  multisampling: 0,
  visibility: true,
  vsync: true,
)

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

Now, back inside our main() function in main.rs, let's copy and paste some rendering code so we can keep moving. We'll cover rendering in more depth later in this tutorial.

let pipe = Pipeline::build().with_stage(
    Stage::with_backbuffer()
        .clear_target([0.0, 0.0, 0.0, 1.0], 1.0)
        .with_pass(DrawFlat::<PosTex>::new()),
);

The important thing to know right now is that this renders a black background. If you want a different color you can tweak the RGBA values inside the .clear_target method. Values range from 0.0 to 1.0, so to get that cool green color we started with back, for instance, you can try [0.00196, 0.23726, 0.21765, 1.0].

Now let's pack everything up and run it:

let game_data = GameDataBuilder::default().with_bundle(RenderBundle::new(pipe, Some(config)))?;
let mut game = Application::new("./", Pong, game_data)?;
game.run();
Ok(())

We've discovered Amethyst's root object: Application. It binds the OS event loop, state machines, timers and other core components in a central place. Here we're creating a new RenderBundle, adding the Pipeline we created, along with our config, and building. There is also a helper function with_basic_renderer on GameDataBuilder that you can use to create your Pipeline and RenderBundle, that performs most of the actions above. In the full pong example in the Amethyst repository that function is used instead.

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

Finally, let's create a texture folder in the root of the project. This will contain the spritesheet texture pong_spritesheet.png we will need to render the elements of the game.

Success! Now we should be able to compile and run this code 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, which are described by Components.

Amethyst uses Specs for its ECS (Entity-component system), which is a parallel Entity-component system written in Rust. You can learn more about Specs in the The Specs Book. Here's a basic explanation of ECS from there:

The term ECS is a 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 behaviour (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

Let's create a new file called pong.rs to hold our core game logic. We can move the Pong struct over here, and the impl State for Pong block as well. Then, in main.rs declare a module:

mod pong;

And in the run() function add:

use pong::Pong;

Now you can just delete various main.rs use statements until the Rust compiler stops complaining about unused imports. In pong.rs we'll need these use statements to make it through this chapter:

use amethyst::assets::{AssetStorage, Loader};
use amethyst::core::cgmath::{Vector3, Matrix4};
use amethyst::core::transform::{GlobalTransform, Transform};
use amethyst::ecs::prelude::{Component, DenseVecStorage};
use amethyst::input::{is_close_requested, is_key_down};
use amethyst::prelude::*;
use amethyst::renderer::{
    Camera, Event, PngFormat, Projection, Sprite, Texture, TextureHandle,
    VirtualKeyCode, WithSpriteRender,
};

Get around the World

First, in pong.rs, let's add a new method to our State implementation: on_start. This method is called, as you probably guessed, when the State starts. We will leave it empty for now, but it will become useful later down the line.

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 gets passed around everywhere. It carries with it all the elements of the runtime of our game: entities, components and systems. Remember when we added bundles in our main.rs, they were in fact adding all the systems they were holding inside the World before we actually ran the game.

Look at your game through 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 records a specific part of the world and can be moved around at will.

First, let's define some constants:

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

These constants will determine the size of our arena. So, as we're making a pong game, we want to create a camera that will cover the entire arena. Let's do it!

fn initialise_camera(world: &mut World) {
    world.create_entity()
        .with(Camera::from(Projection::orthographic(
            0.0,
            ARENA_WIDTH,
            ARENA_HEIGHT,
            0.0,
        )))
        .with(GlobalTransform(
            Matrix4::from_translation(Vector3::new(0.0, 0.0, 1.0)).into()
        ))
        .build();
}

We create an entity that will carry our camera, with an orthographic projection of the size of our arena (as we want it to cover it all). Ignore the GlobalTransform for now, we'll deal with it in more details later on. Note that as the origin of our camera is in the bottom left corner, we set ARENA_HEIGHT as the top and 0.0 as the bottom.

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.

Our first Component

In pong.rs let's create our first Component, a definition of a paddle.

#[derive(PartialEq, Eq)]
enum Side {
    Left,
    Right,
}

struct Paddle {
    pub side: Side,
    pub width: f32,
    pub height: f32,
}

impl Paddle {
    fn new(side: Side) -> Paddle {
        Paddle {
            side: side,
            width: 1.0,
            height: 1.0,
        }
    }
}

"But that just looks like a regular struct!" you might say. And you're right, here's the special sauce:

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

By implementing Component for our Paddle struct, and defining the way we'd like that Component data stored, we can now add the Paddle component to entities in our game. For more 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 math imports:

use amethyst::core::cgmath::{Vector3, Matrix4};
use amethyst::core::transform::{GlobalTransform, Transform};

Amethyst uses the cgmath crate under the hood and exposes it for our use. Today we just grabbed the Vector3 type, which is a very good math thing to have. (we also grabbed Matrix4 for the GlobalTransform earlier, but we won't use it here)

Transform and GlobalTransform are Amethyst ECS components which carry position and orientation information. Transform is relative to a parent if one exists, while GlobalTransform is, well, global.

Let's also define some constants for convenience:

const PADDLE_HEIGHT: f32 = 16.0;
const PADDLE_WIDTH: f32 = 4.0;

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.

/// 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.translation = Vector3::new(PADDLE_WIDTH * 0.5, y, 0.0);
    right_transform.translation = Vector3::new(ARENA_WIDTH - PADDLE_WIDTH * 0.5, y, 0.0);

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

    // Create right plank entity.
    world
        .create_entity()
        .with(Paddle::new(Side::Right))
        .with(GlobalTransform::default())
        .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.

Drawing

The first thing we will have to do is load the sprite sheet we will use for all our graphics in the game. Here, it is located in texture/pong_spritesheet.png. We will perform the loading in the on_start method.

let world = data.world;

// Load the spritesheet necessary to render the graphics.
let spritesheet = {
    let loader = world.read_resource::<Loader>();
    let texture_storage = world.read_resource::<AssetStorage<Texture>>();
    loader.load(
        "texture/pong_spritesheet.png",
        PngFormat,
        Default::default(),
        (),
        &texture_storage,
    )
};

The Loader is an asset loader which is defined as a resource (not a 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 from entities, in contrast to components, which are attached to specific entities. We'll explore this more later on.

The AssetStorage<Texture> is also a resource, this is where the loader will put 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> (also known as TextureHandle). 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.

Now that we have a handle to our sprite sheet's texture, we can communicate it to our initialise_paddle function by changing its signature to:

fn initialise_paddles(world: &mut World, spritesheet: TextureHandle)

We now need to define what part of the spritesheet we want to render. To do that, we need to create a Sprite, which is a fancy name to call a rectangle on the sprite sheet, before the creation of the entities. This is dead simple:

// Build the sprite for the paddles.
let sprite = Sprite {
    left: 0.0,
    right: PADDLE_WIDTH,
    top: 0.0,
    bottom: PADDLE_HEIGHT,
};

Here, we take the rectangle from (0.0 ; 0.0) to (PADDLE_WIDTH ; PADDLE_HEIGHT) on the sprite sheet to be displayed.

Then, using the WithSpriteRender trait, we can easily modify our entity creation code to have the entities display the sprite.

const SPRITESHEET_SIZE: (f32, f32) = (8.0, 16.0);

// Create a left plank entity.
world
    .create_entity()
    .with_sprite(&sprite, spritesheet.clone(), SPRITESHEET_SIZE)
    .expect("Failed to add sprite render on left paddle")
    .with(Paddle::new(Side::Left))
    .with(GlobalTransform::default())
    .with(left_transform)
    .build();

// Create right plank entity.
world
    .create_entity()
    .with_sprite(&sprite, spritesheet, SPRITESHEET_SIZE)
    .expect("Failed to add sprite render on right paddle")
    .with(Paddle::new(Side::Right))
    .with(GlobalTransform::default())
    .with(right_transform)
    .build();

Please note that we need to manually specify the size of our sprite sheet. This is because if we happened to add our sprite to the entity while the sprite sheet is not loaded yet, there would be no way for the renderer to get the size of the texture it needs to do its magic.

Behind the scene, the with_sprite method adds Mesh and Material components to your entity. It is a utility function to spare you from the rendering details, but Amethyst can expose all the precision of the rendering process if you need it. Many utility functions like this exist in Amethyst to make prototyping easier.

Here, we are using the with_sprite utility twice for the same sprite. Keep in mind however that another syntax exists in the SpriteRenderData struct when we need multiple entities to display the exact same sprite, leading to improved performance. Here, however, it is negligible.

Now let's add our initialise functions to the on_start function in impl State for Pong. It now looks like this:

fn on_start(&mut self, data: StateData<GameData>) {
    let world = data.world;

    // Load the spritesheet necessary to render the graphics.
    let spritesheet = {
        let loader = world.read_resource::<Loader>();
        let texture_storage = world.read_resource::<AssetStorage<Texture>>();
        loader.load(
            "texture/pong_spritesheet.png",
            PngFormat,
            Default::default(),
            (),
            &texture_storage,
        )
    };

    initialise_paddles(world, spritesheet);
    initialise_camera(world);
}

Okay! We've defined our Paddle component, and created two entities which have a Paddle component, a GlobalTransform component and a sprite. When our game starts, we'll add the left and right paddles to the world, along with a camera.

Before we continue, one last note. Components do not have to be registered manually to be used, however you need to have something that uses them to have them be registered automatically. As nothing uses our Paddle component yet, we will register it manually before we initialise our paddles in the on_start method by calling:

world.register::<Paddle>();

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

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.

Ah, oops. We forgot something.

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 wrapped up into "Bundles" which include related systems and resources. We can add these to our Application using the with_bundle method, and in fact we already have one of these in main.rs: the RenderBundle.

As it turns out, the system we're missing is TransformSystem, and we can add it with the TransformBundle.

let mut game = Application::build("./", Pong)?
    .with_bundle(TransformBundle::new())? // Add this bundle
    .with_bundle(RenderBundle::new(pipe, Some(config)))?
    .build()?;

Also we'll need to import that structure:

use amethyst::core::transform::TransformBundle;

Now when we run the game 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 describe the behavior of a single instance (eg, 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 resource file under the resources directory of our project, called bindings_config.ron:

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

In Amethyst, inputs can be either scalar inputs (a button that is either pressed or not), or axes (a range that represents an analog controller stick or relates two buttons as opposite ends of a range). In this file, we're creating two axes: W and S will move the left paddle up and down, and the Up and Down arrow keys will move the right paddle up and down.

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

use amethyst::input::InputBundle;

let binding_path = format!(
    "{}/resources/bindings_config.ron",
    env!("CARGO_MANIFEST_DIR")
);

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

let game_data = GameDataBuilder::default()
    .with_bundle(TransformBundle::new())?
    .with_bundle(RenderBundle::new(pipe, Some(config)))?
    .with_bundle(input_bundle)?;
let mut game = Application::new("./", Pong, game_data)?;
game.run();

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:

mod paddle;

pub use self::paddle::PaddleSystem;

We're finally ready to implement the PaddleSystem:

use amethyst::core::transform::components::Transform;
use amethyst::ecs::{Join, Read, ReadStorage, System, WriteStorage};
use amethyst::input::InputHandler;
use pong::{Paddle, Side};

pub struct PaddleSystem;

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

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

Note: We had to make our Paddle and Side public in pong.rs

Now lets add this system to our ApplicationBuilder in main.rs:

mod systems;

// in the run() function
let mut game = Application::build("./", Pong)?
    .with_bundle(TransformBundle::new())?
    .with_bundle(RenderBundle::new(pipe, Some(config)))?
    .with_bundle(input_bundle)?
    .with(systems::PaddleSystem, "paddle_system", &["input_system"])
    .build()?;

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 ran before our newly added system. Here, we require the input_system to be ran as we will use the user's input to move the paddles, so we need to have this data be prepared.

Back in paddle.rs, let's review what our system does, because there's quite a bit there.

We create a unit struct, called PaddleSystem, and implement the System trait for it. The trait specifies the lifetime of the components on which it operates. Inside the implementation, we define the SystemData the system operates on, a tuple of WriteStorage, ReadStorage, and Read. More specifically, the generic types we've used here tell us that the PaddleSystem mutates LocalTransform components, WriteStorage<'s, LocalTransform>, it reads Paddle components, ReadStorage<'s, Paddle>, and also accesses the InputHandler<String, String> resource we created earlier, using the Read structure.

Then, now that we have access to the storages of the components we want, we can iterate over them. We perform a join operation between 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, immutably for the Paddle and mutably 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.

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.

  fn run(&mut self, (mut transforms, paddles, input): Self::SystemData) {
    for (paddle, mut 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.translation[1] += 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, but this will do for now. 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'll make sure the paddle's anchor point never gets out of the arena. But as the anchor point is in the middle of the sprite, we also need to add a margin for the paddle to not go halfway out of the screen. Therefore, we will border the y value of the transform from ARENA_HEIGHT - PADDLE_HEIGHT * 0.5 (the top of the screen but a little bit lower) to PADDLE_HEIGHT * 0.5 (the bottom of the screen but a little bit higher).

Our run function should now look something like this:

  fn run(&mut self, (mut transforms, paddles, input): Self::SystemData) {
    for (paddle, mut 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.translation[1] = (transform.translation[1] + scaled_amount)
          .min(ARENA_HEIGHT - PADDLE_HEIGHT * 0.5)
          .max(PADDLE_HEIGHT * 0.5);
      }
    }
  }

Note: For the above to work, we'll have to mark PADDLE_HEIGHT and ARENA_HEIGHT as being public in pong.rs, and then import it in paddle.rs.

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.

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 coordinate Y coordinate
0 0.0 1.0
1 1.0 0.0
2 0.0 -1.0
3 -1.0 0.0
4 0.0 1.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:

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:

pub struct CustomGameData<'a, 'b> {
    core_dispatcher: Dispatcher<'a, 'b>,
    running_dispatcher: Dispatcher<'a, 'b>,
}

We also add a utility function for performing dispatch:

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.res);
        }
        self.core_dispatcher.dispatch(&world.res);
    }
}

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.

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, bundle: B) -> Result<Self>
    where
        B: SystemBundle<'a, 'b>,
    {
        bundle
            .build(&mut self.core)
            .map_err(|err| Error::Core(err))?;
        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> {
        let pool = world.read_resource::<ThreadPool>().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(&mut world.res);
        running_dispatcher.setup(&mut world.res);

        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.

struct Main;
struct Paused;

impl<'a, 'b> State<CustomGameData<'a, 'b>> for Paused {
    fn on_start(&mut self, data: StateData<CustomGameData>) {
        create_paused_ui(data.world);
    }

    fn handle_event(
        &mut self,
        data: StateData<CustomGameData>,
        event: Event,
    ) -> Trans<CustomGameData<'a, 'b>> {
        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
        }
    }

    fn update(&mut self, data: StateData<CustomGameData>) -> Trans<CustomGameData<'a, 'b>> {
        data.data.update(&data.world, false); // false to say we should not dispatch running
        Trans::None
    }
}

impl<'a, 'b> State<CustomGameData<'a, 'b>> for Main {
    fn on_start(&mut self, data: StateData<CustomGameData>) {
        initialise(data.world);
    }

    fn handle_event(
        &mut self,
        _: StateData<CustomGameData>,
        event: Event,
    ) -> Trans<CustomGameData<'a, 'b>> {
        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
        }
    }

    fn update(&mut self, data: StateData<CustomGameData>) -> Trans<CustomGameData<'a, 'b>> {
        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.

    let game_data = CustomGameDataBuilder::default()
        .with_running::<ExampleSystem>(ExampleSystem, "example_system", &[])
        .with_base_bundle(TransformBundle::new())?
        .with_base_bundle(UiBundle::<String, String>::new())?
        .with_base_bundle(RenderBundle::new(pipeline_builder, Some(display_config)))?
        .with_base_bundle(InputBundle::<String, String>::new())?;

    let mut game = Application::new(resources_directory, Main, game_data)?;
    game.run();

Those are the basics of creating a custom GameData structure. Now get out there and build your game!

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 YAML (what we use) or JSON.

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_COLOUR: [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_COLOUR: [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_config.ron and input.ron files (likely the resources/ 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 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 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 config::ArenaConfig;
...
type SystemData = (
    WriteStorage<'s, Ball>,
    ReadStorage<'s, Paddle>,
    ReadStorage<'s, Transform>,
    Fetch<'s, AssetStorage<Source>>,
    Fetch<'s, Sounds>,
    Fetch<'s, Option<Output>>,
    Fetch<'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 Fetch<'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,
)

Click here to continue to the next chapter

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::cgmath::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 colour 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 colour: (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,
            colour: (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 substition 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, colour) = {
    let config = world.read_resource::<BallConfig>();
    let c: [f32; 4] = [
        config.colour.0,
        config.colour.1,
        config.colour.2,
        config.colour.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 config::ArenaConfig;

to

use config::PongConfig;

Now, modify the run() function, from

let arena_config = ArenaConfig::load(&config);
[..]
    .with_resource(arena_config)
    .with_bundle(PongBundle::default())?

to

let pong_config = PongConfig::load(&config);
[..]
    .with_resource(pong_config.arena)
    .with_resource(pong_config.ball)
    .with_bundle(PongBundle::default())?

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.

Click here to continue to the last chapter, configuring paddles

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 colour: (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 colour 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_colour,
    right_height,
    right_width,
    right_velocity,
    right_colour,
) = {
    let config = &world.read_resource::<PaddlesConfig>();
    let cl: [f32; 4] = [
        config.left.colour.0,
        config.left.colour.1,
        config.left.colour.2,
        config.left.colour.3,
    ];
    let cr: [f32; 4] = [
        config.right.colour.0,
        config.right.colour.1,
        config.right.colour.2,
        config.right.colour.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_colour_material() so that you have a left and right mesh and left and right colour.

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),
        ),
    )
)