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::Result;
use amethyst::prelude::*;
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:

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 one method:

impl<'a, 'b> State<GameData<'a, 'b>> for Pong {
    fn handle_event(&mut self, _: StateData<GameData>, event: Event) -> Trans<GameData<'a, 'b>> {
        match event {
            Event::WindowEvent { event, .. } => match event {
                WindowEvent::KeyboardInput {
                    input:
                        KeyboardInput {
                            virtual_keycode: Some(VirtualKeyCode::Escape),
                            ..
                        },
                    ..
                } => Trans::Quit,
                _ => Trans::None,
            },
            _ => 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 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 for 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 use a run() function, which returns a Result and thus allows us to use ?.

fn run() -> Result<()> {

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

    Ok(())
}

Inside run() we first 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 run() 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.

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.

Now all that's left is to write a main() function to call our run() function:

fn main() {
    if let Err(e) = run() {
        println!("Error occurred during game execution: {}", e);
        ::std::process::exit(1);
    }
}

Success! Now we should be able to compile and run this code and get a window. It should look something like this:

Step one