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