diff --git a/Cargo.lock b/Cargo.lock index 3ca2ac3..748fc1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,10 +88,11 @@ dependencies = [ ] [[package]] -name = "amethyst-cli-starter-2d" +name = "amethyst-starter-2d" version = "0.1.0" dependencies = [ "amethyst 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b74cefb..3e32a92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,12 @@ [package] -name = "amethyst-cli-starter-2d" +name = "amethyst-starter-2d" version = "0.1.0" -authors = [] +authors = ["Hilmar Wiegand "] edition = "2018" [dependencies] amethyst = "0.11.0" +log = { version = "0.4.6", features = ["serde"] } [features] default = ["vulkan"] diff --git a/README.md b/README.md index ba3b2b2..493f3f9 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,66 @@ -# amethyst-cli-starter-2d +

+ + Amethyst + +

+

+ Amethyst 2D Starter +

-## How to run +This project template will get you from 0 to drawing something on the screen in no time. If you're looking for a more in-depth introduction to the engine, please have a look at [our book](https://book.amethyst.rs/stable/)! -To run the game, use +## Quickstart -``` -cargo run --features "vulkan" +- Clone the repository + +```bash +git clone https://github.com/amethyst/amethyst-starter-2d.git +cd amethyst-starter-2d ``` -on Windows and Linux, and +- Build and run the project -``` -cargo run --features "metal" +```bash +cargo run ``` -on macOS. +#### For Mac Users -For building without any graphics backend, you can use +This starter uses vulkan as a renderer by default. You'll want to change the backend to use `metal`, which can be done by opening the `Cargo.toml` file and changing +```toml +[features] +default = ["vulkan"] ``` -cargo run --features "empty" + +to + +```toml +[features] +default = ["metal"] ``` -but be aware that as soon as you need any rendering you won't be able to run your game when using -the `empty` feature. +#### For Linux Users + +You might need to install some dependencies. Please refer to [this section](https://github.com/amethyst/amethyst#dependencies) of the README for more details. + +## Features + +This project contains the minimum amount of code needed to draw sprites to the screen. Here's a small summary of what you'll find in the source files: + +- `resources/display_config.ron` + Contains the window configuration (size, title). + +- `src/main.rs` + Creates the render graph, adds the required bundles, builds the game data with our own state and finally, starts the game's main event loop. + +- `src/render.rs` + Configures the RenderGraph, which defines draw passes, color formats and so on. + +- `src/state.rs` + Implements the main game state. In the `on_start` hook, the camera is initialized, and the sprites that will be drawn are loaded and their entities created. + In the `handle_event` hook, we print any keys that were pressed and close the window if the user presses escape or the OS requests that we quit. diff --git a/src/main.rs b/src/main.rs index 7b9682b..27bac37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,41 +1,14 @@ use amethyst::{ - assets::{AssetStorage, Loader, Processor}, - core::transform::{Transform, TransformBundle}, - ecs::prelude::{ReadExpect, Resources, SystemData}, + assets::Processor, + core::transform::TransformBundle, prelude::*, - renderer::{ - pass::DrawFlat2DDesc, - rendy::{ - factory::Factory, - graph::{ - render::{RenderGroupDesc, SubpassBuilder}, - GraphBuilder, - }, - hal::{format::Format, image}, - }, - types::DefaultBackend, - Camera, GraphCreator, ImageFormat, RenderingSystem, SpriteRender, SpriteSheet, - SpriteSheetFormat, Texture, - }, + renderer::{types::DefaultBackend, RenderingSystem, SpriteSheet}, utils::application_root_dir, - window::{ScreenDimensions, Window, WindowBundle}, + window::WindowBundle, }; -static WIDTH: u32 = 800; -static HEIGHT: u32 = 600; - -struct MyState; - -impl SimpleState for MyState { - fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) { - let world = data.world; - - init_camera(world); - - let sprites = load_sprites(world); - init_sprites(world, &sprites); - } -} +mod render; +mod state; fn main() -> amethyst::Result<()> { amethyst::start_logger(Default::default()); @@ -45,7 +18,7 @@ fn main() -> amethyst::Result<()> { let resources = app_root.join("resources"); let display_config = resources.join("display_config.ron"); - let render_graph = RenderGraph::default(); + let render_graph = render::RenderGraph::default(); let render_system = RenderingSystem::::new(render_graph); let game_data = GameDataBuilder::default() @@ -58,140 +31,8 @@ fn main() -> amethyst::Result<()> { ) .with_thread_local(render_system); - let mut game = Application::new(resources, MyState, game_data)?; + let mut game = Application::new(resources, state::MyState, game_data)?; game.run(); Ok(()) } - -fn init_camera(world: &mut World) { - let mut transform = Transform::default(); - transform.set_translation_xyz(WIDTH as f32 * 0.5, HEIGHT as f32 * 0.5, 1.); - - world - .create_entity() - .with(Camera::standard_2d(WIDTH as f32, HEIGHT as f32)) - .with(transform) - .build(); -} - -fn load_sprites(world: &mut World) -> Vec { - let texture_handle = { - let loader = world.read_resource::(); - let texture_storage = world.read_resource::>(); - loader.load( - "sprites/logo.png", - ImageFormat::default(), - (), - &texture_storage, - ) - }; - - let sheet_handle = { - let loader = world.read_resource::(); - let sheet_storage = world.read_resource::>(); - loader.load( - "sprites/logo.ron", - SpriteSheetFormat(texture_handle), - (), - &sheet_storage, - ) - }; - - (0..3) - .map(|i| SpriteRender { - sprite_sheet: sheet_handle.clone(), - sprite_number: i, - }) - .collect() -} - -fn init_sprites(world: &mut World, sprites: &[SpriteRender]) { - for (i, sprite) in sprites.iter().enumerate() { - let x = (i as f32 - 1.) * 100. + WIDTH as f32 * 0.5; - let y = (i as f32 - 1.) * 100. + HEIGHT as f32 * 0.5; - let mut transform = Transform::default(); - transform.set_translation_xyz(x, y, 0.); - - world - .create_entity() - .with(sprite.clone()) - .with(transform) - .build(); - } -} - -// TODO(happens): Can we provide this with a few parameters, -// for the most common cases? The fields could still be exposed -#[derive(Default)] -struct RenderGraph { - dimensions: Option, - surface_format: Option, - dirty: bool, -} - -// TODO(happens): Add explanations -impl GraphCreator for RenderGraph { - fn rebuild(&mut self, res: &Resources) -> bool { - // Rebuild when dimensions change, but wait until at least two frames have the same. - let new_dimensions = res.try_fetch::(); - use std::ops::Deref; - if self.dimensions.as_ref() != new_dimensions.as_ref().map(|d| d.deref()) { - self.dirty = true; - self.dimensions = new_dimensions.map(|d| d.clone()); - return false; - } - - self.dirty - } - - fn builder( - &mut self, - factory: &mut Factory, - res: &Resources, - ) -> GraphBuilder { - use amethyst::renderer::rendy::{ - graph::present::PresentNode, - hal::command::{ClearDepthStencil, ClearValue}, - }; - - self.dirty = false; - let window = >::fetch(res); - let surface = factory.create_surface(&window); - // cache surface format to speed things up - let surface_format = *self - .surface_format - .get_or_insert_with(|| factory.get_surface_format(&surface)); - let dimensions = self.dimensions.as_ref().unwrap(); - let window_kind = - image::Kind::D2(dimensions.width() as u32, dimensions.height() as u32, 1, 1); - - let mut graph_builder = GraphBuilder::new(); - let color = graph_builder.create_image( - window_kind, - 1, - surface_format, - Some(ClearValue::Color([0.34, 0.36, 0.52, 1.0].into())), - ); - - let depth = graph_builder.create_image( - window_kind, - 1, - Format::D32Sfloat, - Some(ClearValue::DepthStencil(ClearDepthStencil(1.0, 0))), - ); - - let opaque = graph_builder.add_node( - SubpassBuilder::new() - .with_group(DrawFlat2DDesc::new().builder()) - .with_color(color) - .with_depth_stencil(depth) - .into_pass(), - ); - - let _present = graph_builder - .add_node(PresentNode::builder(factory, surface, color).with_dependency(opaque)); - - graph_builder - } -} diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..032445b --- /dev/null +++ b/src/render.rs @@ -0,0 +1,90 @@ +use amethyst::{ + ecs::prelude::{ReadExpect, Resources, SystemData}, + renderer::{ + pass::DrawFlat2DDesc, + rendy::{ + factory::Factory, + graph::{ + render::{RenderGroupDesc, SubpassBuilder}, + GraphBuilder, + }, + hal::format::Format, + }, + types::DefaultBackend, + GraphCreator, Kind, + }, + window::{ScreenDimensions, Window}, +}; + +// Window background color +static CLEAR_COLOR: [f32; 4] = [0.34, 0.36, 0.52, 1.0]; + +#[derive(Default)] +pub struct RenderGraph { + dimensions: Option, + dirty: bool, +} + +impl GraphCreator for RenderGraph { + fn rebuild(&mut self, res: &Resources) -> bool { + use std::ops::Deref; + + // Only rebuild when dimensions have changed + let new_dimensions = res.try_fetch::(); + let new_dimensions = new_dimensions.as_ref().map(|d| d.deref()); + + if self.dimensions.as_ref() != new_dimensions { + self.dirty = true; + self.dimensions = new_dimensions.map(|d| d.clone()); + return false; + } + + self.dirty + } + + fn builder( + &mut self, + factory: &mut Factory, + res: &Resources, + ) -> GraphBuilder { + use amethyst::renderer::rendy::{ + graph::present::PresentNode, + hal::command::{ClearDepthStencil, ClearValue}, + }; + + // Since we're freshly building the graph, it will never + // be dirty after this function is called. + self.dirty = false; + + let window = >::fetch(res); + + let surface = factory.create_surface(&window); + let surface_format = factory.get_surface_format(&surface); + + let dimensions = self.dimensions.as_ref().unwrap(); + let window_kind = Kind::D2(dimensions.width() as u32, dimensions.height() as u32, 1, 1); + + let clear_color = ClearValue::Color(CLEAR_COLOR.into()); + let clear_depth = ClearValue::DepthStencil(ClearDepthStencil(1.0, 0)); + + // Build the RenderGraph + let mut builder = GraphBuilder::new(); + let color = builder.create_image(window_kind, 1, surface_format, Some(clear_color)); + let depth = builder.create_image(window_kind, 1, Format::D32Sfloat, Some(clear_depth)); + + // Add additional draw groups here for things like UI + let pass = builder.add_node( + SubpassBuilder::new() + .with_group(DrawFlat2DDesc::new().builder()) // Draw sprites + .with_color(color) + .with_depth_stencil(depth) + .into_pass(), + ); + + // Render the result to the surface + let present = PresentNode::builder(factory, surface, color).with_dependency(pass); + builder.add_node(present); + + builder + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..8819df6 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,130 @@ +use amethyst::{ + assets::{AssetStorage, Loader}, + core::transform::Transform, + input::{get_key, is_close_requested, is_key_down, VirtualKeyCode}, + prelude::*, + renderer::{Camera, ImageFormat, SpriteRender, SpriteSheet, SpriteSheetFormat, Texture}, + window::ScreenDimensions, +}; + +use log::info; + +pub struct MyState; + +impl SimpleState for MyState { + // On start will run when this state is initialized. For more + // state lifecycle hooks, see: + // https://book.amethyst.rs/stable/concepts/state.html#life-cycle + fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) { + let world = data.world; + + // Get the screen dimensions so we can initialize the camera and + // place our sprites correctly later. We'll clone this since we'll + // pass the world mutably to the following functions. + let dimensions = world.read_resource::().clone(); + + // Place the camera + init_camera(world, &dimensions); + + // Load our sprites and display them + let sprites = load_sprites(world); + init_sprites(world, &sprites, &dimensions); + } + + fn handle_event( + &mut self, + mut _data: StateData<'_, GameData<'_, '_>>, + event: StateEvent, + ) -> SimpleTrans { + if let StateEvent::Window(event) = &event { + // Check if the window should be closed + if is_close_requested(&event) || is_key_down(&event, VirtualKeyCode::Escape) { + return Trans::Quit; + } + + // Listen to any key events + if let Some(event) = get_key(&event) { + info!("handling key event: {:?}", event); + } + + // If you're looking for a more sophisticated event handling solution, + // including key bindings and gamepad support, please have a look at + // https://book.amethyst.rs/stable/pong-tutorial/pong-tutorial-03.html#capturing-user-input + } + + // Keep going + Trans::None + } +} + +fn init_camera(world: &mut World, dimensions: &ScreenDimensions) { + // Center the camera in the middle of the screen, and let it cover + // the entire screen + let mut transform = Transform::default(); + transform.set_translation_xyz(dimensions.width() * 0.5, dimensions.height() * 0.5, 1.); + + world + .create_entity() + .with(Camera::standard_2d(dimensions.width(), dimensions.height())) + .with(transform) + .build(); +} + +fn load_sprites(world: &mut World) -> Vec { + // Load the texture for our sprites. We'll later need to + // add a handle to this texture to our `SpriteRender`s, so + // we need to keep a reference to it. + let texture_handle = { + let loader = world.read_resource::(); + let texture_storage = world.read_resource::>(); + loader.load( + "sprites/logo.png", + ImageFormat::default(), + (), + &texture_storage, + ) + }; + + // Load the spritesheet definition file, which contains metadata on our + // spritesheet texture. + let sheet_handle = { + let loader = world.read_resource::(); + let sheet_storage = world.read_resource::>(); + loader.load( + "sprites/logo.ron", + SpriteSheetFormat(texture_handle), + (), + &sheet_storage, + ) + }; + + // Create our sprite renders. Each will have a handle to the texture + // that it renders from. The handle is safe to clone, since it just + // references the asset. + (0..3) + .map(|i| SpriteRender { + sprite_sheet: sheet_handle.clone(), + sprite_number: i, + }) + .collect() +} + +fn init_sprites(world: &mut World, sprites: &[SpriteRender], dimensions: &ScreenDimensions) { + for (i, sprite) in sprites.iter().enumerate() { + // Center our sprites around the center of the window + let x = (i as f32 - 1.) * 100. + dimensions.width() * 0.5; + let y = (i as f32 - 1.) * 100. + dimensions.height() * 0.5; + let mut transform = Transform::default(); + transform.set_translation_xyz(x, y, 0.); + + // Create an entity for each sprite and attach the `SpriteRender` as + // well as the transform. If you want to add behaviour to your sprites, + // you'll want to add a custom `Component` that will identify them, and a + // `System` that will iterate over them. See https://book.amethyst.rs/stable/concepts/system.html + world + .create_entity() + .with(sprite.clone()) + .with(transform) + .build(); + } +}