Update project structure, update README, add input handler

master
Hilmar Wiegand 5 years ago
parent da1cbadfea
commit 988a8f9b57

3
Cargo.lock generated

@ -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]]

@ -1,11 +1,12 @@
[package]
name = "amethyst-cli-starter-2d"
name = "amethyst-starter-2d"
version = "0.1.0"
authors = []
authors = ["Hilmar Wiegand <me@hwgnd.de>"]
edition = "2018"
[dependencies]
amethyst = "0.11.0"
log = { version = "0.4.6", features = ["serde"] }
[features]
default = ["vulkan"]

@ -1,26 +1,66 @@
# amethyst-cli-starter-2d
<p align="center">
<a href="https://amethyst.rs">
<img
alt="Amethyst"
src="https://amethyst.rs/brand/logo-standard.svg"
width="60"
/>
</a>
</p>
<h1 align="center">
Amethyst 2D Starter
</h1>
## 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.

@ -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::<DefaultBackend, _>::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<SpriteRender> {
let texture_handle = {
let loader = world.read_resource::<Loader>();
let texture_storage = world.read_resource::<AssetStorage<Texture>>();
loader.load(
"sprites/logo.png",
ImageFormat::default(),
(),
&texture_storage,
)
};
let sheet_handle = {
let loader = world.read_resource::<Loader>();
let sheet_storage = world.read_resource::<AssetStorage<SpriteSheet>>();
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<ScreenDimensions>,
surface_format: Option<Format>,
dirty: bool,
}
// TODO(happens): Add explanations
impl GraphCreator<DefaultBackend> 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::<ScreenDimensions>();
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<DefaultBackend>,
res: &Resources,
) -> GraphBuilder<DefaultBackend, Resources> {
use amethyst::renderer::rendy::{
graph::present::PresentNode,
hal::command::{ClearDepthStencil, ClearValue},
};
self.dirty = false;
let window = <ReadExpect<'_, 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
}
}

@ -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<ScreenDimensions>,
dirty: bool,
}
impl GraphCreator<DefaultBackend> 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::<ScreenDimensions>();
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<DefaultBackend>,
res: &Resources,
) -> GraphBuilder<DefaultBackend, Resources> {
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 = <ReadExpect<'_, 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
}
}

@ -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::<ScreenDimensions>().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<SpriteRender> {
// 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::<Loader>();
let texture_storage = world.read_resource::<AssetStorage<Texture>>();
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::<Loader>();
let sheet_storage = world.read_resource::<AssetStorage<SpriteSheet>>();
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();
}
}
Loading…
Cancel
Save