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:
