From 7375bc8ae39e1c0dd62e052ecb31c6ee6059dcb1 Mon Sep 17 00:00:00 2001 From: Mateusz Kwiatkowski Date: Tue, 20 Dec 2022 16:30:54 +0100 Subject: [PATCH 01/15] update code and book for chapter 1 --- books/en_US/src/c01-01-setup.md | 4 ++-- code/rust-sokoban-c01-01/Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/books/en_US/src/c01-01-setup.md b/books/en_US/src/c01-01-setup.md index bcce8a7..673b3fe 100644 --- a/books/en_US/src/c01-01-setup.md +++ b/books/en_US/src/c01-01-setup.md @@ -4,9 +4,9 @@ Let's install [rustup](https://www.rust-lang.org/tools/install), this will insta ``` $ rustc --version -rustc 1.40.0 +rustc 1.65.0 $ cargo --version -cargo 1.40.0 +cargo 1.65.0 ``` ## Creating a project diff --git a/code/rust-sokoban-c01-01/Cargo.toml b/code/rust-sokoban-c01-01/Cargo.toml index 2ceb2e6..f842aab 100644 --- a/code/rust-sokoban-c01-01/Cargo.toml +++ b/code/rust-sokoban-c01-01/Cargo.toml @@ -2,9 +2,9 @@ name = "rust-sokoban" version = "0.1.0" authors = ["Olivia "] -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ggez = "0.7" \ No newline at end of file +ggez = "0.8.1" From 0599266801602675dee01863a115bf5128754cbe Mon Sep 17 00:00:00 2001 From: Mateusz Kwiatkowski Date: Tue, 20 Dec 2022 16:37:21 +0100 Subject: [PATCH 02/15] update code and book for chapters 1.2 and 1.3 --- books/en_US/src/c01-03-entities-components.md | 6 +++--- code/rust-sokoban-c01-03/Cargo.toml | 6 +++--- code/rust-sokoban-c01-03/src/main.rs | 6 ------ 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/books/en_US/src/c01-03-entities-components.md b/books/en_US/src/c01-03-entities-components.md index 4b4ba4a..84a622b 100644 --- a/books/en_US/src/c01-03-entities-components.md +++ b/books/en_US/src/c01-03-entities-components.md @@ -8,7 +8,7 @@ This should hopefully be straight-forward, the position components stores the x, ```rust -{{#include ../../../code/rust-sokoban-c01-03/src/main.rs:13:42}} +{{#include ../../../code/rust-sokoban-c01-03/src/main.rs:16:36}} ``` Among the familiar Rust code we've got some new syntax, we're using a powerful Rust feature called `Procedural Macros` which is used in `#[storage(VecStorage)]`. These type of macros are essentially functions that at compile time consume some syntax and produce some new syntax. @@ -19,7 +19,7 @@ Among the familiar Rust code we've got some new syntax, we're using a powerful R In order for specs to be happy we have to tell it ahead of time what components we will be using. Let's create a function to register components into specs. ```rust -{{#include ../../../code/rust-sokoban-c01-03/src/main.rs:61:69}} +{{#include ../../../code/rust-sokoban-c01-03/src/main.rs:56:63}} ``` ## Creating entities @@ -28,7 +28,7 @@ An entity is simply a numeric identifier tied to a set of components. So the way This is how entity creation looks now. ```rust -{{#include ../../../code/rust-sokoban-c01-03/src/main.rs:71:124}} +{{#include ../../../code/rust-sokoban-c01-03/src/main.rs:65:117}} ``` ## Assets diff --git a/code/rust-sokoban-c01-03/Cargo.toml b/code/rust-sokoban-c01-03/Cargo.toml index 28f9106..a8c2d27 100644 --- a/code/rust-sokoban-c01-03/Cargo.toml +++ b/code/rust-sokoban-c01-03/Cargo.toml @@ -2,10 +2,10 @@ name = "rust-sokoban" version = "0.1.0" authors = ["Olivia "] -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ggez = "0.7" -specs = { version = "0.17.0", features = ["specs-derive"] } \ No newline at end of file +ggez = "0.8.1" +specs = { version = "0.18.0", features = ["specs-derive"] } diff --git a/code/rust-sokoban-c01-03/src/main.rs b/code/rust-sokoban-c01-03/src/main.rs index 68c1530..54ad4b8 100644 --- a/code/rust-sokoban-c01-03/src/main.rs +++ b/code/rust-sokoban-c01-03/src/main.rs @@ -1,12 +1,7 @@ // Rust sokoban // main.rs - - - use ggez::{conf, event, Context, GameResult}; use specs::{Builder, Component, VecStorage, World, WorldExt}; - - use std::path; // Components @@ -67,7 +62,6 @@ pub fn register_components(world: &mut World) { world.register::(); } -// Create a wall entity pub fn create_wall(world: &mut World, position: Position) { world .create_entity() From b60d4b3d81f0efbfab06d5adc82fbd43a53c8f71 Mon Sep 17 00:00:00 2001 From: Mateusz Kwiatkowski Date: Tue, 20 Dec 2022 17:17:02 +0100 Subject: [PATCH 03/15] add code and book for chapter 1.4 --- books/en_US/src/c01-04-rendering.md | 14 +++++++------- code/rust-sokoban-c01-04/Cargo.toml | 8 ++++---- code/rust-sokoban-c01-04/src/main.rs | 19 +++++++++++-------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/books/en_US/src/c01-04-rendering.md b/books/en_US/src/c01-04-rendering.md index 859c06f..611fe9b 100644 --- a/books/en_US/src/c01-04-rendering.md +++ b/books/en_US/src/c01-04-rendering.md @@ -6,7 +6,7 @@ It's time for our first system, the rendering system. This system will be respon First we'll define the `RenderingSystem` struct, it will need access to the ggez context in order to actually render. ```rust -{{#include ../../../code/rust-sokoban-c01-04/src/main.rs:47:49}} +{{#include ../../../code/rust-sokoban-c01-04/src/main.rs:48:50}} ``` We've got some new syntax here; `'a` is called a lifetime annotation. It's needed because the compiler can't see how long the reference in `RenderingSystem` is valid, meaning that we have to specify the lifetime annotation. @@ -16,15 +16,15 @@ We've got some new syntax here; `'a` is called a lifetime annotation. It's neede Now let's implement the System trait for our Rendering system. This doesn't do anything yet, we're just setting up the scaffolding. The definition of SystemData means that we will have access to the storage of position and renderable components, and the fact that it's read storage means we only get immutable access, which is exactly what we need. ```rust -{{#include ../../../code/rust-sokoban-c01-04/src/main.rs:51:57}} +{{#include ../../../code/rust-sokoban-c01-04/src/main.rs:52:58}} // implementation here -{{#include ../../../code/rust-sokoban-c01-04/src/main.rs:83:84}} +{{#include ../../../code/rust-sokoban-c01-04/src/main.rs:86:87}} ``` Finally let's run the rendering system in our draw loop. This means that every time the game updates we will render the latest state of all our entities. ```rust -{{#include ../../../code/rust-sokoban-c01-04/src/main.rs:97:111}} +{{#include ../../../code/rust-sokoban-c01-04/src/main.rs:100:114}} ``` Running the game now should compile, but it will probably not do anything yet, since we haven't filled in any of the implementation of the rendering system and also we haven't created any entities. @@ -34,7 +34,7 @@ Running the game now should compile, but it will probably not do anything yet, s **Note:** We're going to add [glam](https://lib.rs/crates/glam) as a dependency here that is a simple and fast 3D library that offers some performance improvements. ``` -{{#include ../../../code/rust-sokoban-c01-03/Cargo.toml:9:11}} +{{#include ../../../code/rust-sokoban-c01-04/Cargo.toml:9:12}} ``` Here is the implementation of the rendering system. It does a few things: @@ -44,7 +44,7 @@ Here is the implementation of the rendering system. It does a few things: * finally, present to the screen ```rust -{{#include ../../../code/rust-sokoban-c01-04/src/main.rs:56:83}} +{{#include ../../../code/rust-sokoban-c01-04/src/main.rs:57:86}} ``` ## Add some test entities @@ -52,7 +52,7 @@ Here is the implementation of the rendering system. It does a few things: Let's create some test entities to make sure things are working correctly. ```rust -{{#include ../../../code/rust-sokoban-c01-04/src/main.rs:179:204}} +{{#include ../../../code/rust-sokoban-c01-04/src/main.rs:182:207}} ``` Finally, let's put everything together and run. You should see something like this! This is super exciting, now we have a proper rendering system and we can actually see something on the screen for the first time. Next up, we're going to work on the gameplay so it can actually feel like a game! diff --git a/code/rust-sokoban-c01-04/Cargo.toml b/code/rust-sokoban-c01-04/Cargo.toml index 92e5cac..2cf62a8 100644 --- a/code/rust-sokoban-c01-04/Cargo.toml +++ b/code/rust-sokoban-c01-04/Cargo.toml @@ -2,11 +2,11 @@ name = "rust-sokoban" version = "0.1.0" authors = ["Olivia "] -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ggez = "0.7" -glam = { version = "0.20.0", features = ["mint"] } -specs = { version = "0.17.0", features = ["specs-derive"] } \ No newline at end of file +ggez = "0.8.1" +glam = { version = "0.22.0", features = ["mint"] } +specs = { version = "0.18.0", features = ["specs-derive"] } diff --git a/code/rust-sokoban-c01-04/src/main.rs b/code/rust-sokoban-c01-04/src/main.rs index 7f82725..ca3db9c 100644 --- a/code/rust-sokoban-c01-04/src/main.rs +++ b/code/rust-sokoban-c01-04/src/main.rs @@ -1,13 +1,14 @@ // Rust sokoban // main.rs - +use ggez::{ + conf, event, + graphics::{self, Canvas, DrawParam, Image}, + Context, GameResult, +}; use glam::Vec2; -use ggez::{conf, event, Context, GameResult, - graphics::{self, DrawParam, Image}}; use specs::{ join::Join, Builder, Component, ReadStorage, RunNow, System, VecStorage, World, WorldExt, }; - use std::path; const TILE_WIDTH: f32 = 32.0; @@ -57,7 +58,8 @@ impl<'a> System<'a> for RenderingSystem<'a> { let (positions, renderables) = data; // Clearing the screen (this gives us the background colour) - graphics::clear(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0)); + let mut canvas = + Canvas::from_frame(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0)); // Get all the renderables with their positions and sort by the position z // This will allow us to have entities layered visually. @@ -68,18 +70,19 @@ impl<'a> System<'a> for RenderingSystem<'a> { // and draw it at the specified position. for (position, renderable) in rendering_data.iter() { // Load the image - let image = Image::new(self.context, renderable.path.clone()).expect("expected image"); + let image = + Image::from_path(self.context, renderable.path.clone()).expect("expected image"); let x = position.x as f32 * TILE_WIDTH; let y = position.y as f32 * TILE_WIDTH; // draw let draw_params = DrawParam::new().dest(Vec2::new(x, y)); - graphics::draw(self.context, &image, draw_params).expect("expected render"); + canvas.draw(&image, draw_params); } // Finally, present the context, this will actually display everything // on the screen. - graphics::present(self.context).expect("expected to present"); + canvas.finish(self.context).expect("expected to present"); } } From b16e09aaf11afcbf08859cd2b2fb1f02967e19f3 Mon Sep 17 00:00:00 2001 From: Mateusz Kwiatkowski Date: Tue, 20 Dec 2022 17:33:32 +0100 Subject: [PATCH 04/15] update code and book for chapter 2.1 --- books/en_US/src/c02-01-map-loading.md | 11 ++++++----- code/rust-sokoban-c01-04/src/main.rs | 1 + code/rust-sokoban-c02-01/Cargo.toml | 8 ++++---- code/rust-sokoban-c02-01/src/main.rs | 21 +++++++++++++-------- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/books/en_US/src/c02-01-map-loading.md b/books/en_US/src/c02-01-map-loading.md index e831de6..718856c 100644 --- a/books/en_US/src/c02-01-map-loading.md +++ b/books/en_US/src/c02-01-map-loading.md @@ -3,10 +3,11 @@ Last chapter we left off at creating some entities to test our rendering system, but now it's time to render a proper map. In this section we will create a text based map configuration which we will load. ## Map config + First step, let's try to load a level based on a 2d map that looks like this. ``` -{{#include ../../../code/rust-sokoban-c02-01/src/main.rs:181:189}} +{{#include ../../../code/rust-sokoban-c02-01/src/main.rs:186:194}} where: . is an empty spot @@ -20,18 +21,18 @@ N is nothing: used for the outer edges of the map Let's make a string for this, eventually we can load from a file but for simplicity let's go with a constant in the code for now. ```rust -{{#include ../../../code/rust-sokoban-c02-01/src/main.rs:179:193}} +{{#include ../../../code/rust-sokoban-c02-01/src/main.rs:184:198}} ``` And here is the implementation of load map. ```rust -{{#include ../../../code/rust-sokoban-c02-01/src/main.rs:195:234}} +{{#include ../../../code/rust-sokoban-c02-01/src/main.rs:200:239}} ``` The most interesting Rust concept here is probably the `match`. We are using the basic feature of pattern matching here, we are simply matching on the values of each token found in the map config, but we could do a lot of more advanced conditions or types of patterns. -> **_MORE:_** Read more about pattern matching [here](https://doc.rust-lang.org/book/ch06-02-match.html). +> **_MORE:_** Read more about pattern matching [here](https://doc.rust-lang.org/book/ch06-02-match.html). Now let's run the game and see what our map looks like. @@ -43,4 +44,4 @@ Final code below. {{#include ../../../code/rust-sokoban-c02-01/src/main.rs}} ``` -> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c02-01). +> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c02-01). diff --git a/code/rust-sokoban-c01-04/src/main.rs b/code/rust-sokoban-c01-04/src/main.rs index ca3db9c..f78ca1c 100644 --- a/code/rust-sokoban-c01-04/src/main.rs +++ b/code/rust-sokoban-c01-04/src/main.rs @@ -1,5 +1,6 @@ // Rust sokoban // main.rs + use ggez::{ conf, event, graphics::{self, Canvas, DrawParam, Image}, diff --git a/code/rust-sokoban-c02-01/Cargo.toml b/code/rust-sokoban-c02-01/Cargo.toml index 92e5cac..2cf62a8 100644 --- a/code/rust-sokoban-c02-01/Cargo.toml +++ b/code/rust-sokoban-c02-01/Cargo.toml @@ -2,11 +2,11 @@ name = "rust-sokoban" version = "0.1.0" authors = ["Olivia "] -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ggez = "0.7" -glam = { version = "0.20.0", features = ["mint"] } -specs = { version = "0.17.0", features = ["specs-derive"] } \ No newline at end of file +ggez = "0.8.1" +glam = { version = "0.22.0", features = ["mint"] } +specs = { version = "0.18.0", features = ["specs-derive"] } diff --git a/code/rust-sokoban-c02-01/src/main.rs b/code/rust-sokoban-c02-01/src/main.rs index 561e0f2..dc58c7f 100644 --- a/code/rust-sokoban-c02-01/src/main.rs +++ b/code/rust-sokoban-c02-01/src/main.rs @@ -1,13 +1,15 @@ // Rust sokoban // main.rs +use ggez::{ + conf, event, + graphics::{self, DrawParam, Image}, + Context, GameResult, +}; use glam::Vec2; -use ggez::{conf, event, Context, GameResult, - graphics::{self, DrawParam, Image}}; use specs::{ join::Join, Builder, Component, ReadStorage, RunNow, System, VecStorage, World, WorldExt, }; - use std::path; const TILE_WIDTH: f32 = 32.0; @@ -56,8 +58,9 @@ impl<'a> System<'a> for RenderingSystem<'a> { fn run(&mut self, data: Self::SystemData) { let (positions, renderables) = data; - // Clearing the screen (this gives us the backround colour) - graphics::clear(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0)); + // Clearing the screen (this gives us the background colour) + let mut canvas = + Canvas::from_frame(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0)); // Get all the renderables with their positions and sort by the position z // This will allow us to have entities layered visually. @@ -68,18 +71,19 @@ impl<'a> System<'a> for RenderingSystem<'a> { // and draw it at the specified position. for (position, renderable) in rendering_data.iter() { // Load the image - let image = Image::new(self.context, renderable.path.clone()).expect("expected image"); + let image = + Image::from_path(self.context, renderable.path.clone()).expect("expected image"); let x = position.x as f32 * TILE_WIDTH; let y = position.y as f32 * TILE_WIDTH; // draw let draw_params = DrawParam::new().dest(Vec2::new(x, y)); - graphics::draw(self.context, &image, draw_params).expect("expected render"); + canvas.draw(&image, draw_params); } // Finally, present the context, this will actually display everything // on the screen. - graphics::present(self.context).expect("expected to present"); + canvas.finish(self.context).expect("expected to present"); } } @@ -232,6 +236,7 @@ pub fn load_map(world: &mut World, map_string: String) { } } } + pub fn main() -> GameResult { let mut world = World::new(); register_components(&mut world); From 703c70e85defd85a6ab572b111d34609886808c7 Mon Sep 17 00:00:00 2001 From: Mateusz Kwiatkowski Date: Tue, 20 Dec 2022 20:05:08 +0100 Subject: [PATCH 05/15] update code and book for chapter 2.2 --- books/en_US/src/c02-02-move-player.md | 30 +++++++++--------- code/rust-sokoban-c02-02/Cargo.toml | 8 ++--- code/rust-sokoban-c02-02/src/main.rs | 45 ++++++++++++++++----------- 3 files changed, 46 insertions(+), 37 deletions(-) diff --git a/books/en_US/src/c02-02-move-player.md b/books/en_US/src/c02-02-move-player.md index 9cd3663..89249a5 100644 --- a/books/en_US/src/c02-02-move-player.md +++ b/books/en_US/src/c02-02-move-player.md @@ -3,6 +3,7 @@ It wouldn't be a game if we couldn't move the player, would it? In this section we will figure out how to grab input events. ## Input events + The first step for making our player move is to start listening to input events. If we take a quick look at the [ggez input example](https://github.com/ggez/ggez/blob/master/examples/input_test.rs#L59) we can see we can subscribe to all sort of mouse and keyboard related events, for now we probably only want `key_down_event`. Let's start listening to key events. First we'll bring a few more modules into scope: @@ -14,16 +15,16 @@ Let's start listening to key events. First we'll bring a few more modules into s Then, we'll add this code inside the `event::EventHandler` implementation block for our Game: ```rust -{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:134}} +{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:136}} // ... -{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:155:162}} -{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:166}} +{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:157:158}} +{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:162}} // ... -{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:167}} +{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:163}} ``` If we run this we should see the print lines in the console. @@ -37,37 +38,38 @@ Key pressed: Down Key pressed: Left ``` -If you are not familiar with the `{:?}` notation used when printing, this is just a convenient way that Rust allows us to print objects for debugging. In this case we can print a KeyCode object (which is an enum) because the KeyCode type implements the Debug trait using the Debug macro (remember we discussed macros in [Chapter 1.3](./c01-03-entities-components.html), so head back there if you need a refresher). If KeyCode didn't implement Debug we would not be able to use this syntax and instead we would get a compiler error. This saves us writing some custom code to convert the key codes to strings, so we can rely on the built-in functionalily for that. +If you are not familiar with the `{:?}` notation used when printing, this is just a convenient way that Rust allows us to print objects for debugging. In this case we can print a KeyCode object (which is an enum) because the KeyCode type implements the Debug trait using the Debug macro (remember we discussed macros in [Chapter 1.3](./c01-03-entities-components.html), so head back there if you need a refresher). If KeyCode didn't implement Debug we would not be able to use this syntax and instead we would get a compiler error. This saves us writing some custom code to convert the key codes to strings, so we can rely on the built-in functionalily for that. ## Resources + Next up we'll add a resource, which is the specs way of sharing some state across systems which isn't part of your world. We'll use a resource for modelling the input queue of key presses, since that doesn't really fit into our existing components/entities model. ```rust -{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:48:52}} +{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:51:54}} ``` And then we'll push the new key presses into the queue when `key_down_event` is called. ```rust -{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:134}} +{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:136}} // ... -{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:155:166}} +{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:157:162}} // ... -{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:167}} +{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:163}} ``` Finally, we need to register the resources into specs like we did for components. ```rust // Registering resources -{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:179:181}} +{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:177:179}} // Registering resources in main -{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:295:312}} +{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:293:311}} ``` ## Input system @@ -75,13 +77,13 @@ Finally, we need to register the resources into specs like we did for components Using this code we have a resource that is a continuous queue of input key presses. Next up, we'll start processing these inputs in a system. ```rust -{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:94:121}} +{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:99:127}} ``` Finally we need to run the system in our update loop. ```rust -{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:135:143}} +{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:141:149}} ``` The input system is pretty simple, it grabs all the players and positions (we should only have one player but this code doesn't need to care about that, it could in theory work if we have multiple players that we want to control with the same input). And then for every player and position combination, it will grab the first key pressed and remove it from the input queue. It will then figure out what is the required transformation - for example if we press up we want to move one tile up and so on, and then applies this position update. @@ -90,4 +92,4 @@ Pretty cool! Here's how it should look like. Notice we can go through walls and ![Moving player](./images/input.gif) -> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c02-02). \ No newline at end of file +> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c02-02). diff --git a/code/rust-sokoban-c02-02/Cargo.toml b/code/rust-sokoban-c02-02/Cargo.toml index 92e5cac..2cf62a8 100644 --- a/code/rust-sokoban-c02-02/Cargo.toml +++ b/code/rust-sokoban-c02-02/Cargo.toml @@ -2,11 +2,11 @@ name = "rust-sokoban" version = "0.1.0" authors = ["Olivia "] -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ggez = "0.7" -glam = { version = "0.20.0", features = ["mint"] } -specs = { version = "0.17.0", features = ["specs-derive"] } \ No newline at end of file +ggez = "0.8.1" +glam = { version = "0.22.0", features = ["mint"] } +specs = { version = "0.18.0", features = ["specs-derive"] } diff --git a/code/rust-sokoban-c02-02/src/main.rs b/code/rust-sokoban-c02-02/src/main.rs index cf0dd0e..a53df2b 100644 --- a/code/rust-sokoban-c02-02/src/main.rs +++ b/code/rust-sokoban-c02-02/src/main.rs @@ -1,15 +1,18 @@ // Rust sokoban // main.rs +use ggez::{ + conf, event, + graphics::{self, Canvas, DrawParam, Image}, + input::keyboard::KeyInput, + winit::event::VirtualKeyCode, + Context, GameResult, +}; use glam::Vec2; -use ggez::{conf, Context, GameResult, - event::{self, KeyCode, KeyMods}, - graphics::{self, DrawParam, Image}}; use specs::{ join::Join, Builder, Component, ReadStorage, RunNow, System, VecStorage, World, WorldExt, Write, WriteStorage, }; - use std::path; const TILE_WIDTH: f32 = 32.0; @@ -48,7 +51,7 @@ pub struct BoxSpot {} // Resources #[derive(Default)] pub struct InputQueue { - pub keys_pressed: Vec, + pub keys_pressed: Vec, } // Systems @@ -64,8 +67,9 @@ impl<'a> System<'a> for RenderingSystem<'a> { fn run(&mut self, data: Self::SystemData) { let (positions, renderables) = data; - // Clearing the screen (this gives us the backround colour) - graphics::clear(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0)); + // Clearing the screen (this gives us the background colour) + let mut canvas = + Canvas::from_frame(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0)); // Get all the renderables with their positions and sort by the position z // This will allow us to have entities layered visually. @@ -76,18 +80,19 @@ impl<'a> System<'a> for RenderingSystem<'a> { // and draw it at the specified position. for (position, renderable) in rendering_data.iter() { // Load the image - let image = Image::new(self.context, renderable.path.clone()).expect("expected image"); + let image = + Image::from_path(self.context, renderable.path.clone()).expect("expected image"); let x = position.x as f32 * TILE_WIDTH; let y = position.y as f32 * TILE_WIDTH; // draw let draw_params = DrawParam::new().dest(Vec2::new(x, y)); - graphics::draw(self.context, &image, draw_params).expect("expected render"); + canvas.draw(&image, draw_params); } // Finally, present the context, this will actually display everything // on the screen. - graphics::present(self.context).expect("expected to present"); + canvas.finish(self.context).expect("expected to present"); } } @@ -109,10 +114,11 @@ impl<'a> System<'a> for InputSystem { if let Some(key) = input_queue.keys_pressed.pop() { // Apply the key to the position match key { - KeyCode::Up => position.y -= 1, - KeyCode::Down => position.y += 1, - KeyCode::Left => position.x -= 1, - KeyCode::Right => position.x += 1, + VirtualKeyCode::Up => position.y -= 1, + VirtualKeyCode::Down => position.y += 1, + VirtualKeyCode::Left => position.x -= 1, + VirtualKeyCode::Right => position.x += 1, + _ => (), } } @@ -155,14 +161,15 @@ impl event::EventHandler for Game { fn key_down_event( &mut self, _context: &mut Context, - keycode: KeyCode, - _keymod: KeyMods, + keyinput: KeyInput, _repeat: bool, - ) { - println!("Key pressed: {:?}", keycode); + ) -> GameResult { + println!("Key pressed: {:?}", keyinput.keycode.unwrap()); let mut input_queue = self.world.write_resource::(); - input_queue.keys_pressed.push(keycode); + input_queue.keys_pressed.push(keyinput.keycode.unwrap()); + + Ok(()) } } From 67b6afdbd327856282b47a5ec006e678e533e4c1 Mon Sep 17 00:00:00 2001 From: Mateusz Kwiatkowski Date: Wed, 21 Dec 2022 16:34:11 +0100 Subject: [PATCH 06/15] update code and book for chapter 2.3 --- books/en_US/src/c02-03-push-box.md | 42 +++++----- code/rust-sokoban-c02-03/Cargo.toml | 8 +- code/rust-sokoban-c02-03/src/main.rs | 112 +++++++++++++-------------- 3 files changed, 83 insertions(+), 79 deletions(-) diff --git a/books/en_US/src/c02-03-push-box.md b/books/en_US/src/c02-03-push-box.md index 75ed9ac..4f4df1a 100644 --- a/books/en_US/src/c02-03-push-box.md +++ b/books/en_US/src/c02-03-push-box.md @@ -3,33 +3,37 @@ In the previous chapter we got our player moving, but he is going through walls and boxes, not really interacting with the environment. In this section we'll add some logic for more intelligent player movement. ## Movement components + First, we need to make our code slightly more generic. If you remember the previous chapter we were operating on players to figure out where we should move them, but we'll also need to move boxes. Also in the future we might want to introduce another movable kind of object, so let's try to build something with that in mind. What we'll do in true ECS spirit we will use a marker component to tell us which entities are movable and which aren't. For example, players and boxes are movable, while walls are immovable. Box spots are kind of irrelevant here because they do not move, but they also shouldn't affect the movement of players or boxes, so box spots will not have either of these components. Here are our two new components, nothing too new apart from two minor things: -* we are using `NullStorage` which is slightly more efficient than using `VecStorage` since these two components will not have any fields, and are just used as markers -* we are implementing Default because that is a requirement for using NullStorage -* adding the two new components to our register_components function +- we are using `NullStorage` which is slightly more efficient than using `VecStorage` since these two components will not have any fields, and are just used as markers +- we are implementing Default because that is a requirement for using NullStorage +- adding the two new components to our register_components function ```rust -{{#include ../../../code/rust-sokoban-c02-03/src/main.rs:55:62}} +{{#include ../../../code/rust-sokoban-c02-03/src/main.rs:54:60}} -{{#include ../../../code/rust-sokoban-c02-03/src/main.rs:250:259}} +{{#include ../../../code/rust-sokoban-c02-03/src/main.rs:245:256}} ``` Next, we'll add: -* with(Movable) to players and boxes -* with(Immovable) to walls -* do nothing with floors and box spots (as mentioned before they should not be part of our movement/collision system since they are inconsequential to the movement) + +- with(Movable) to players and boxes +- with(Immovable) to walls +- do nothing with floors and box spots (as mentioned before they should not be part of our movement/collision system since they are inconsequential to the movement) ```rust -{{#include ../../../code/rust-sokoban-c02-03/src/main.rs:266:321}} +{{#include ../../../code/rust-sokoban-c02-03/src/main.rs:262:317}} ``` ## Movement requirements + Now let's think of a few examples that illustrate our requirements for movement. This will help us understand how we need to change the implementation of the input system to use `Movable` and `Immovable` correctly. Scenarios: + 1. `(player, floor)` and `RIGHT` pressed -> player should move to the right 1. `(player, wall)` and `RIGHT` pressed -> player should not move to the right 1. `(player, box, floor)` and `RIGHT` pressed -> player should move to the right, box should move to the right @@ -38,24 +42,26 @@ Scenarios: 1. `(player, box, box, wall)` and `RIGHT` pressed -> nothing should move A few observations we can make based on this: -* the collision/movement detection should happen all at once for all objects involved - for example, for scenario 6 if we processed one item at a time, we would move the player, we would move the first box, and when we get to the second box we realize we cannot move it, and we'd have to roll back all our movement actions, which will not work. So for every input, we must figure out all the objects involved and holistically decide if the action is possible or not. -* a chain of movables with an empty spot can move (empty spot in this case means something neither movable or immovable) -* a chain of movables with an immovable spot cannot move -* even though all examples were moving to the right, the rules should generalize for any movement and the key pressed should just influence how we find the chain + +- the collision/movement detection should happen all at once for all objects involved - for example, for scenario 6 if we processed one item at a time, we would move the player, we would move the first box, and when we get to the second box we realize we cannot move it, and we'd have to roll back all our movement actions, which will not work. So for every input, we must figure out all the objects involved and holistically decide if the action is possible or not. +- a chain of movables with an empty spot can move (empty spot in this case means something neither movable or immovable) +- a chain of movables with an immovable spot cannot move +- even though all examples were moving to the right, the rules should generalize for any movement and the key pressed should just influence how we find the chain So given this, let's start implementing this logic. Let's think about the logical pieces we need. Some initial ideas: + 1. **find all the movable and immovable entities** - this is so we can figure out if they are affected by the movement 2. **figure out which way to move based on a key** - we've kind of figured this out in the previous section already, basically a bunch of +1/-1 operations based on the key enum 3. **iterate through all positions between the player and the end of the map** on the correct axis based on the direction - for example, if we press right, we need to go from player.x to map_width, if we press up we need to go from 0 to player.y 4. **for every tile in this sequence** we need to: - * if the tile is movable, continue and remember this tile - * if the tile is not movable, stop and don't move anything - * if the tile is neither movable or immovable, move all the tiles we've remembered so far + - if the tile is movable, continue and remember this tile + - if the tile is not movable, stop and don't move anything + - if the tile is neither movable or immovable, move all the tiles we've remembered so far Here is the new implementation of the input systems, it's a bit long but hopefully it makes sense. ```rust -{{#include ../../../code/rust-sokoban-c02-03/src/main.rs:113:197}} +{{#include ../../../code/rust-sokoban-c02-03/src/main.rs:113:198}} ``` Now if we run the code, we'll see it actually works! We can't go through walls anymore and we can push the box and it stops when it gets to the wall. @@ -68,4 +74,4 @@ Full code below. {{#include ../../../code/rust-sokoban-c02-03/src/main.rs}} ``` -> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c02-03). \ No newline at end of file +> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c02-03). diff --git a/code/rust-sokoban-c02-03/Cargo.toml b/code/rust-sokoban-c02-03/Cargo.toml index 92e5cac..11bf251 100644 --- a/code/rust-sokoban-c02-03/Cargo.toml +++ b/code/rust-sokoban-c02-03/Cargo.toml @@ -2,11 +2,11 @@ name = "rust-sokoban" version = "0.1.0" authors = ["Olivia "] -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ggez = "0.7" -glam = { version = "0.20.0", features = ["mint"] } -specs = { version = "0.17.0", features = ["specs-derive"] } \ No newline at end of file +ggez = "0.8.1" +glam = { version = "0.22.0", features = ["mint"]} +specs = { version = "0.18.0", features = ["specs-derive"] } \ No newline at end of file diff --git a/code/rust-sokoban-c02-03/src/main.rs b/code/rust-sokoban-c02-03/src/main.rs index fda8fd8..dc40003 100644 --- a/code/rust-sokoban-c02-03/src/main.rs +++ b/code/rust-sokoban-c02-03/src/main.rs @@ -1,21 +1,19 @@ // Rust sokoban // main.rs - - -use glam::Vec2; use ggez::{ - conf, Context, GameResult, - event::{self, KeyCode, KeyMods}, - graphics::{self, DrawParam, Image}}; + conf, event, + graphics::{self, Canvas, DrawParam, Image}, + input::keyboard::KeyInput, + winit::event::VirtualKeyCode, + Context, GameResult, +}; +use glam::Vec2; use specs::{ - join::Join, Builder, Component, ReadStorage, RunNow, - System, VecStorage, World, WorldExt, - Write, WriteStorage, NullStorage, Entities, world::Index + join::Join, world::Index, Builder, Component, Entities, NullStorage, ReadStorage, RunNow, + System, VecStorage, World, WorldExt, Write, WriteStorage, }; - -use std::collections::HashMap; -use std::path; +use std::{collections::HashMap, path}; const TILE_WIDTH: f32 = 32.0; const MAP_WIDTH: u8 = 8; @@ -63,7 +61,7 @@ pub struct Immovable; // Resources #[derive(Default)] pub struct InputQueue { - pub keys_pressed: Vec, + pub keys_pressed: Vec, } // Systems @@ -79,8 +77,9 @@ impl<'a> System<'a> for RenderingSystem<'a> { fn run(&mut self, data: Self::SystemData) { let (positions, renderables) = data; - // Clearing the screen (this gives us the backround colour) - graphics::clear(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0)); + // Clearing the screen (this gives us the background colour) + let mut canvas = + Canvas::from_frame(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0)); // Get all the renderables with their positions and sort by the position z // This will allow us to have entities layered visually. @@ -91,24 +90,24 @@ impl<'a> System<'a> for RenderingSystem<'a> { // and draw it at the specified position. for (position, renderable) in rendering_data.iter() { // Load the image - let image = Image::new(self.context, renderable.path.clone()).expect("expected image"); - let x = position.x as f32 * TILE_WIDTH; - let y = position.y as f32 * TILE_WIDTH; + let image = + Image::from_path(self.context, renderable.path.clone()).expect("expected image"); + let x: f32 = position.x as f32 * TILE_WIDTH; + let y: f32 = position.y as f32 * TILE_WIDTH; // draw let draw_params = DrawParam::new().dest(Vec2::new(x, y)); - graphics::draw(self.context, &image, draw_params).expect("expected render"); + canvas.draw(&image, draw_params); } // Finally, present the context, this will actually display everything // on the screen. - graphics::present(self.context).expect("expected to present"); + canvas.finish(self.context).expect("expected to present"); } } pub struct InputSystem {} -// System implementation impl<'a> System<'a> for InputSystem { // Data type SystemData = ( @@ -141,10 +140,10 @@ impl<'a> System<'a> for InputSystem { // Now iterate through current position to the end of the map // on the correct axis and check what needs to move. let (start, end, is_x) = match key { - KeyCode::Up => (position.y, 0, false), - KeyCode::Down => (position.y, MAP_HEIGHT, false), - KeyCode::Left => (position.x, 0, true), - KeyCode::Right => (position.x, MAP_WIDTH, true), + VirtualKeyCode::Up => (position.y, 0, false), + VirtualKeyCode::Down => (position.y, MAP_HEIGHT, false), + VirtualKeyCode::Left => (position.x, 0, true), + VirtualKeyCode::Right => (position.x, MAP_WIDTH, true), _ => continue, }; @@ -185,10 +184,10 @@ impl<'a> System<'a> for InputSystem { let position = positions.get_mut(entities.entity(id)); if let Some(position) = position { match key { - KeyCode::Up => position.y -= 1, - KeyCode::Down => position.y += 1, - KeyCode::Left => position.x -= 1, - KeyCode::Right => position.x += 1, + VirtualKeyCode::Up => position.y -= 1, + VirtualKeyCode::Down => position.y += 1, + VirtualKeyCode::Left => position.x -= 1, + VirtualKeyCode::Right => position.x += 1, _ => (), } } @@ -231,17 +230,19 @@ impl event::EventHandler for Game { fn key_down_event( &mut self, _context: &mut Context, - keycode: KeyCode, - _keymod: KeyMods, + keyinput: KeyInput, _repeat: bool, - ) { - println!("Key pressed: {:?}", keycode); - + ) -> GameResult { let mut input_queue = self.world.write_resource::(); - input_queue.keys_pressed.push(keycode); + input_queue.keys_pressed.push(keyinput.keycode.unwrap()); + Ok(()) } } +pub fn register_resources(world: &mut World) { + world.insert(InputQueue::default()) +} + // Register components with the world pub fn register_components(world: &mut World) { world.register::(); @@ -250,12 +251,8 @@ pub fn register_components(world: &mut World) { world.register::(); world.register::(); world.register::(); - world.register::(); world.register::(); -} - -pub fn register_resources(world: &mut World) { - world.insert(InputQueue::default()) + world.register::(); } // Create a wall entity @@ -316,23 +313,6 @@ pub fn create_player(world: &mut World, position: Position) { .build(); } -// Initialize the level -pub fn initialize_level(world: &mut World) { - const MAP: &str = " - N N W W W W W W - W W W . . . . W - W . . . B . . W - W . . . . . . W - W . P . . . . W - W . . . . . . W - W . . S . . . W - W . . . . . . W - W W W W W W W W - "; - - load_map(world, MAP.to_string()); -} - pub fn load_map(world: &mut World, map_string: String) { // read all lines let rows: Vec<&str> = map_string.trim().split('\n').map(|x| x.trim()).collect(); @@ -373,6 +353,24 @@ pub fn load_map(world: &mut World, map_string: String) { } } } + +// Initialize the level +pub fn initialize_level(world: &mut World) { + const MAP: &str = " + N N W W W W W W + W W W . . . . W + W . . . B . . W + W . . . . . . W + W . P . . . . W + W . . . . . . W + W . . S . . . W + W . . . . . . W + W W W W W W W W + "; + + load_map(world, MAP.to_string()); +} + pub fn main() -> GameResult { let mut world = World::new(); register_components(&mut world); From 432586887deff35e2a4272054aeec31265cb4568 Mon Sep 17 00:00:00 2001 From: Mateusz Kwiatkowski Date: Wed, 21 Dec 2022 16:45:58 +0100 Subject: [PATCH 07/15] update code and book for chapter 2.4 --- books/en_US/src/c02-04-modules.md | 8 ++------ code/rust-sokoban-c02-04/Cargo.toml | 8 ++++---- code/rust-sokoban-c02-04/src/components.rs | 1 - code/rust-sokoban-c02-04/src/entities.rs | 1 - code/rust-sokoban-c02-04/src/main.rs | 12 +++++------ code/rust-sokoban-c02-04/src/resources.rs | 5 ++--- .../src/systems/input_system.rs | 20 +++++++++---------- .../src/systems/rendering_system.rs | 19 ++++++++++-------- 8 files changed, 34 insertions(+), 40 deletions(-) diff --git a/books/en_US/src/c02-04-modules.md b/books/en_US/src/c02-04-modules.md index 41636a0..9e18c51 100644 --- a/books/en_US/src/c02-04-modules.md +++ b/books/en_US/src/c02-04-modules.md @@ -26,7 +26,7 @@ For now, let's aim for this folder structure. Eventually as we get more componen └── Cargo.toml ``` -> **_MORE:_** Read more about modules and managing growing projects [here](https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html). +> **_MORE:_** Read more about modules and managing growing projects [here](https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html). Let's start by moving all the components into a file. There should be no changes apart from making some fields public. The reason why we need to make the fields public is because when everything was in the same file everything had access to everything else, which was convenient to start with, but now that we have split things out we need to pay more attention to visibilities. For now we'll make the fields public to get things working again, but there is a better way which we will discuss in a later section. We've also moved the components registration at the bottom of this file which is quite handy when we add components we only need to change this file. @@ -67,7 +67,6 @@ Finally, we'll move the systems code into their own files (RenderingSystem to re Now the interesting thing about systems is that it's a folder with multiple files inside. If we do nothing else and try to use `RenderingSystem` or `InputSystem` in main we will get some compilation failures. We will have to add a `mod.rs` file in the `systems` folder and tell Rust what we want to export out of this folder. All this bit is doing is it's telling Rust we want the outside world (the world out of this folder) to be able to access RenderingSystem and InputSystem types. - ```rust // systems/mod.rs {{#include ../../../code/rust-sokoban-c02-04/src/systems/mod.rs}} @@ -76,12 +75,9 @@ Now the interesting thing about systems is that it's a folder with multiple file Awesome, now that we've done that here is how our simplified main file looks like. Notice the mod and use declarations after the imports, those are again telling Rust that we want to use those modules. ```rust -// main.rs {{#include ../../../code/rust-sokoban-c02-04/src/main.rs}} ``` Feel free to run at this point, everything should work just the same, the only difference is now our code is much nicer and ready for more amazing Sokoban features. -> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c02-04). - - +> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c02-04). diff --git a/code/rust-sokoban-c02-04/Cargo.toml b/code/rust-sokoban-c02-04/Cargo.toml index 92e5cac..11bf251 100644 --- a/code/rust-sokoban-c02-04/Cargo.toml +++ b/code/rust-sokoban-c02-04/Cargo.toml @@ -2,11 +2,11 @@ name = "rust-sokoban" version = "0.1.0" authors = ["Olivia "] -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ggez = "0.7" -glam = { version = "0.20.0", features = ["mint"] } -specs = { version = "0.17.0", features = ["specs-derive"] } \ No newline at end of file +ggez = "0.8.1" +glam = { version = "0.22.0", features = ["mint"]} +specs = { version = "0.18.0", features = ["specs-derive"] } \ No newline at end of file diff --git a/code/rust-sokoban-c02-04/src/components.rs b/code/rust-sokoban-c02-04/src/components.rs index 84feb53..264299d 100644 --- a/code/rust-sokoban-c02-04/src/components.rs +++ b/code/rust-sokoban-c02-04/src/components.rs @@ -1,6 +1,5 @@ use specs::{Component, NullStorage, VecStorage, World, WorldExt}; -// Components #[derive(Debug, Component, Clone, Copy)] #[storage(VecStorage)] pub struct Position { diff --git a/code/rust-sokoban-c02-04/src/entities.rs b/code/rust-sokoban-c02-04/src/entities.rs index c3922fd..be09727 100644 --- a/code/rust-sokoban-c02-04/src/entities.rs +++ b/code/rust-sokoban-c02-04/src/entities.rs @@ -1,7 +1,6 @@ use crate::components::*; use specs::{Builder, World, WorldExt}; -// Create a wall entity pub fn create_wall(world: &mut World, position: Position) { world .create_entity() diff --git a/code/rust-sokoban-c02-04/src/main.rs b/code/rust-sokoban-c02-04/src/main.rs index 34f10b7..d622842 100644 --- a/code/rust-sokoban-c02-04/src/main.rs +++ b/code/rust-sokoban-c02-04/src/main.rs @@ -1,7 +1,7 @@ // Rust sokoban // main.rs -use ggez::{conf, event::{self, KeyCode, KeyMods}, Context, GameResult}; +use ggez::{conf, event, input::keyboard::KeyInput, Context, GameResult}; use specs::{RunNow, World, WorldExt}; use std::path; @@ -45,14 +45,12 @@ impl event::EventHandler for Game { fn key_down_event( &mut self, _context: &mut Context, - keycode: KeyCode, - _keymod: KeyMods, + keyinput: KeyInput, _repeat: bool, - ) { - println!("Key pressed: {:?}", keycode); - + ) -> GameResult { let mut input_queue = self.world.write_resource::(); - input_queue.keys_pressed.push(keycode); + input_queue.keys_pressed.push(keyinput.keycode.unwrap()); + Ok(()) } } diff --git a/code/rust-sokoban-c02-04/src/resources.rs b/code/rust-sokoban-c02-04/src/resources.rs index 10e8023..3e2ab70 100644 --- a/code/rust-sokoban-c02-04/src/resources.rs +++ b/code/rust-sokoban-c02-04/src/resources.rs @@ -1,10 +1,9 @@ -use ggez::event::KeyCode; +use ggez::winit::event::VirtualKeyCode; use specs::World; -// Resources #[derive(Default)] pub struct InputQueue { - pub keys_pressed: Vec, + pub keys_pressed: Vec, } pub fn register_resources(world: &mut World) { diff --git a/code/rust-sokoban-c02-04/src/systems/input_system.rs b/code/rust-sokoban-c02-04/src/systems/input_system.rs index 0b77146..d94f1a2 100644 --- a/code/rust-sokoban-c02-04/src/systems/input_system.rs +++ b/code/rust-sokoban-c02-04/src/systems/input_system.rs @@ -1,9 +1,9 @@ use crate::components::*; use crate::constants::*; use crate::resources::InputQueue; -use ggez::event::KeyCode; -use specs::{world::Index, Entities, Join, ReadStorage, System, Write, WriteStorage}; +use ggez::winit::event::VirtualKeyCode; +use specs::{world::Index, Entities, Join, ReadStorage, System, Write, WriteStorage}; use std::collections::HashMap; pub struct InputSystem {} @@ -41,10 +41,10 @@ impl<'a> System<'a> for InputSystem { // Now iterate through current position to the end of the map // on the correct axis and check what needs to move. let (start, end, is_x) = match key { - KeyCode::Up => (position.y, 0, false), - KeyCode::Down => (position.y, MAP_HEIGHT, false), - KeyCode::Left => (position.x, 0, true), - KeyCode::Right => (position.x, MAP_WIDTH, true), + VirtualKeyCode::Up => (position.y, 0, false), + VirtualKeyCode::Down => (position.y, MAP_HEIGHT, false), + VirtualKeyCode::Left => (position.x, 0, true), + VirtualKeyCode::Right => (position.x, MAP_WIDTH, true), _ => continue, }; @@ -85,10 +85,10 @@ impl<'a> System<'a> for InputSystem { let position = positions.get_mut(entities.entity(id)); if let Some(position) = position { match key { - KeyCode::Up => position.y -= 1, - KeyCode::Down => position.y += 1, - KeyCode::Left => position.x -= 1, - KeyCode::Right => position.x += 1, + VirtualKeyCode::Up => position.y -= 1, + VirtualKeyCode::Down => position.y += 1, + VirtualKeyCode::Left => position.x -= 1, + VirtualKeyCode::Right => position.x += 1, _ => (), } } diff --git a/code/rust-sokoban-c02-04/src/systems/rendering_system.rs b/code/rust-sokoban-c02-04/src/systems/rendering_system.rs index d0c5693..8343182 100644 --- a/code/rust-sokoban-c02-04/src/systems/rendering_system.rs +++ b/code/rust-sokoban-c02-04/src/systems/rendering_system.rs @@ -1,11 +1,12 @@ - - use crate::components::*; use crate::constants::TILE_WIDTH; -use ggez::{Context, graphics::{self, DrawParam, Image}}; -use specs::{Join, ReadStorage, System}; +use ggez::{ + graphics::{self, Canvas, DrawParam, Image}, + Context, +}; use glam::Vec2; +use specs::{Join, ReadStorage, System}; pub struct RenderingSystem<'a> { pub context: &'a mut Context, @@ -20,7 +21,8 @@ impl<'a> System<'a> for RenderingSystem<'a> { let (positions, renderables) = data; // Clearing the screen (this gives us the backround colour) - graphics::clear(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0)); + let mut canvas = + Canvas::from_frame(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0)); // Get all the renderables with their positions and sort by the position z // This will allow us to have entities layered visually. @@ -31,17 +33,18 @@ impl<'a> System<'a> for RenderingSystem<'a> { // and draw it at the specified position. for (position, renderable) in rendering_data.iter() { // Load the image - let image = Image::new(self.context, renderable.path.clone()).expect("expected image"); + let image = + Image::from_path(self.context, renderable.path.clone()).expect("expected image"); let x = position.x as f32 * TILE_WIDTH; let y = position.y as f32 * TILE_WIDTH; // draw let draw_params = DrawParam::new().dest(Vec2::new(x, y)); - graphics::draw(self.context, &image, draw_params).expect("expected render"); + canvas.draw(&image, draw_params); } // Finally, present the context, this will actually display everything // on the screen. - graphics::present(self.context).expect("expected to present"); + canvas.finish(self.context).expect("expected to present"); } } From 5ab3729f113385dcdc9582d3e749aaf927230ffe Mon Sep 17 00:00:00 2001 From: Mateusz Kwiatkowski Date: Wed, 21 Dec 2022 17:04:26 +0100 Subject: [PATCH 08/15] update code and book for chapter 2.5 --- books/en_US/src/c02-05-gameplay.md | 35 +++++++------- code/rust-sokoban-c02-05/Cargo.toml | 8 ++-- code/rust-sokoban-c02-05/src/components.rs | 1 - code/rust-sokoban-c02-05/src/entities.rs | 1 - code/rust-sokoban-c02-05/src/main.rs | 12 ++--- code/rust-sokoban-c02-05/src/resources.rs | 32 +++++-------- .../src/systems/input_system.rs | 25 +++++----- code/rust-sokoban-c02-05/src/systems/mod.rs | 4 +- .../src/systems/rendering_system.rs | 48 +++++++++---------- 9 files changed, 78 insertions(+), 88 deletions(-) diff --git a/books/en_US/src/c02-05-gameplay.md b/books/en_US/src/c02-05-gameplay.md index 6da5031..ab8635f 100644 --- a/books/en_US/src/c02-05-gameplay.md +++ b/books/en_US/src/c02-05-gameplay.md @@ -1,16 +1,16 @@ # Gameplay The player character is able to move and push boxes on the field. Many (but not all!) games have some kind of objective -for the player to achieve. The objective for Sokoban-style games is typically to push boxes onto a goal spot. There's -nothing stopping the player from doing this now, but the game also isn't checking for success. The player might achieve -the objective without realizing it! Let's update the game to check for the success state. +for the player to achieve. The objective for Sokoban-style games is typically to push boxes onto a goal spot. There's +nothing stopping the player from doing this now, but the game also isn't checking for success. The player might achieve +the objective without realizing it! Let's update the game to check for the success state. Let's think about what we'll need to add to this game to check for the success condition and to notify the user when they've beaten the level: - A `resource` for tracking the game state - - Is the game in progress or completed? - - How many move has the player made? + - Is the game in progress or completed? + - How many move has the player made? - A `system` for checking if the user has completed their objective - A `system` for updating the number of moves made - UI for reporting game state @@ -27,7 +27,7 @@ not associated with a specific entity. Let's start by defining a `Gameplay` reso `Gameplay` has two fields: `state` and `moves_count`. These are used to track the current state of the game (is the game still in play, or has the player won?) and -the number of moves made. `state` is described by an `enum`, defined like so: +the number of moves made. `state` is described by an `enum`, defined like so: ```rust // resources.rs @@ -84,12 +84,12 @@ counter. ```rust // input_system.rs ... -{{#include ../../../code/rust-sokoban-c02-05/src/systems/input_system.rs:83:105}} +{{#include ../../../code/rust-sokoban-c02-05/src/systems/input_system.rs:84:105}} ``` ## Gameplay System -Next, let's integrate this resource with a new `GamePlayStateSystem`. This +Next, let's integrate this resource with a new `GamePlayStateSystem`. This system will continuously check to see if all the boxes have the same position as all the box spots. Once all the boxes are on all the box spots, the game has been won! @@ -98,11 +98,11 @@ Aside from `Gameplay`, this system only needs read-only access to the `Position`, `Box`, and `BoxSpot` storages. The system uses `Join` to create a vector from the `Box` and `Position` -storages. This vector is mapped into a hashmap containing the location of +storages. This vector is mapped into a hashmap containing the location of each box on the board. Next, the system uses the `Join` method again to create an iterable from -entities that have both `BoxSpot` and `Position` components. The system walks through this iterable. +entities that have both `BoxSpot` and `Position` components. The system walks through this iterable. If all box spots have a corresponding box at the same position, the game is over and the player has won. Otherwise, the game is still in play. @@ -120,11 +120,10 @@ Finally, let's run the gameplay system in our main update loop. {{#include ../../../code/rust-sokoban-c02-05/src/main.rs:63}} ``` - ## Gameplay UI The last step is to provide feedback to the user letting them know what the -state of the game is. This requires a resource to track the state and a +state of the game is. This requires a resource to track the state and a system to update the state. We can adapt the `GameplayState` resource and `RenderingSystem` for this. @@ -134,7 +133,7 @@ to render "Playing" or "Won". ```rust // resources.rs -{{#include ../../../code/rust-sokoban-c02-05/src/resources.rs:21:30}} +{{#include ../../../code/rust-sokoban-c02-05/src/resources.rs:17:25}} ``` Next, we'll add a `draw_text` method to `RenderingSystem`, so it can print @@ -142,19 +141,20 @@ Next, we'll add a `draw_text` method to `RenderingSystem`, so it can print ```rust // rendering_systems.rs -{{#include ../../../code/rust-sokoban-c02-05/src/systems/rendering_system.rs:16:32}} +{{#include ../../../code/rust-sokoban-c02-05/src/systems/rendering_system.rs:15:26}} ``` ...and then we'll add the `Gameplay` resource to `RenderingSystem` so we can -call `draw_text`. `RenderingSystem` needs to be able to read the `Gameplay` +call `draw_text`. `RenderingSystem` needs to be able to read the `Gameplay` resource. ```rust // rendering_system.rs -{{#include ../../../code/rust-sokoban-c02-05/src/systems/rendering_system.rs:35:71}} +{{#include ../../../code/rust-sokoban-c02-05/src/systems/rendering_system.rs:29:71}} ``` At this point, the game will provide basic feedback to the user: + - Counts the number of steps - Tells the player when they have won @@ -162,7 +162,6 @@ Here's how it looks. ![Sokoban play](./images/moves.gif) - There are plenty of other enhancements that can be made! -> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c02-05). \ No newline at end of file +> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c02-05). diff --git a/code/rust-sokoban-c02-05/Cargo.toml b/code/rust-sokoban-c02-05/Cargo.toml index 92e5cac..11bf251 100644 --- a/code/rust-sokoban-c02-05/Cargo.toml +++ b/code/rust-sokoban-c02-05/Cargo.toml @@ -2,11 +2,11 @@ name = "rust-sokoban" version = "0.1.0" authors = ["Olivia "] -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ggez = "0.7" -glam = { version = "0.20.0", features = ["mint"] } -specs = { version = "0.17.0", features = ["specs-derive"] } \ No newline at end of file +ggez = "0.8.1" +glam = { version = "0.22.0", features = ["mint"]} +specs = { version = "0.18.0", features = ["specs-derive"] } \ No newline at end of file diff --git a/code/rust-sokoban-c02-05/src/components.rs b/code/rust-sokoban-c02-05/src/components.rs index 84feb53..264299d 100644 --- a/code/rust-sokoban-c02-05/src/components.rs +++ b/code/rust-sokoban-c02-05/src/components.rs @@ -1,6 +1,5 @@ use specs::{Component, NullStorage, VecStorage, World, WorldExt}; -// Components #[derive(Debug, Component, Clone, Copy)] #[storage(VecStorage)] pub struct Position { diff --git a/code/rust-sokoban-c02-05/src/entities.rs b/code/rust-sokoban-c02-05/src/entities.rs index c3922fd..be09727 100644 --- a/code/rust-sokoban-c02-05/src/entities.rs +++ b/code/rust-sokoban-c02-05/src/entities.rs @@ -1,7 +1,6 @@ use crate::components::*; use specs::{Builder, World, WorldExt}; -// Create a wall entity pub fn create_wall(world: &mut World, position: Position) { world .create_entity() diff --git a/code/rust-sokoban-c02-05/src/main.rs b/code/rust-sokoban-c02-05/src/main.rs index 327da35..bdf946c 100644 --- a/code/rust-sokoban-c02-05/src/main.rs +++ b/code/rust-sokoban-c02-05/src/main.rs @@ -1,7 +1,7 @@ // Rust sokoban // main.rs -use ggez::{conf, event::{self, KeyCode, KeyMods}, Context, GameResult}; +use ggez::{conf, event, input::keyboard::KeyInput, Context, GameResult}; use specs::{RunNow, World, WorldExt}; use std::path; @@ -51,14 +51,12 @@ impl event::EventHandler for Game { fn key_down_event( &mut self, _context: &mut Context, - keycode: KeyCode, - _keymod: KeyMods, + keyinput: KeyInput, _repeat: bool, - ) { - println!("Key pressed: {:?}", keycode); - + ) -> GameResult { let mut input_queue = self.world.write_resource::(); - input_queue.keys_pressed.push(keycode); + input_queue.keys_pressed.push(keyinput.keycode.unwrap()); + Ok(()) } } diff --git a/code/rust-sokoban-c02-05/src/resources.rs b/code/rust-sokoban-c02-05/src/resources.rs index b319be1..79b201d 100644 --- a/code/rust-sokoban-c02-05/src/resources.rs +++ b/code/rust-sokoban-c02-05/src/resources.rs @@ -1,42 +1,36 @@ -use ggez::event::KeyCode; +use ggez::winit::event::VirtualKeyCode; use specs::World; +use std::fmt::{Display, Formatter, Result}; -use std::fmt::{self, Display}; - -// Resources #[derive(Default)] pub struct InputQueue { - pub keys_pressed: Vec, -} - -pub fn register_resources(world: &mut World) { - world.insert(InputQueue::default()); - world.insert(Gameplay::default()); + pub keys_pressed: Vec, } +#[derive(Debug, Default, Clone, Copy)] pub enum GameplayState { + #[default] Playing, - Won + Won, } impl Display for GameplayState { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt.write_str(match self { GameplayState::Playing => "Playing", - GameplayState::Won => "Won" + GameplayState::Won => "Won", })?; Ok(()) } } -impl Default for GameplayState { - fn default() -> Self { - Self::Playing - } -} - #[derive(Default)] pub struct Gameplay { pub state: GameplayState, - pub moves_count: u32 + pub moves_count: u32, +} + +pub fn register_resources(world: &mut World) { + world.insert(InputQueue::default()); + world.insert(Gameplay::default()); } diff --git a/code/rust-sokoban-c02-05/src/systems/input_system.rs b/code/rust-sokoban-c02-05/src/systems/input_system.rs index 74ef39a..5c7117f 100644 --- a/code/rust-sokoban-c02-05/src/systems/input_system.rs +++ b/code/rust-sokoban-c02-05/src/systems/input_system.rs @@ -1,9 +1,9 @@ use crate::components::*; use crate::constants::*; -use crate::resources::{InputQueue, Gameplay}; -use ggez::event::KeyCode; -use specs::{world::Index, Entities, Join, ReadStorage, System, Write, WriteStorage}; +use crate::resources::{Gameplay, InputQueue}; +use ggez::winit::event::VirtualKeyCode; +use specs::{world::Index, Entities, Join, ReadStorage, System, Write, WriteStorage}; use std::collections::HashMap; pub struct InputSystem {} @@ -22,7 +22,8 @@ impl<'a> System<'a> for InputSystem { ); fn run(&mut self, data: Self::SystemData) { - let (mut input_queue, mut gameplay, entities, mut positions, players, movables, immovables) = data; + let (mut input_queue, mut gameplay, entities, mut positions, players, movables, immovables) = + data; let mut to_move = Vec::new(); @@ -42,10 +43,10 @@ impl<'a> System<'a> for InputSystem { // Now iterate through current position to the end of the map // on the correct axis and check what needs to move. let (start, end, is_x) = match key { - KeyCode::Up => (position.y, 0, false), - KeyCode::Down => (position.y, MAP_HEIGHT, false), - KeyCode::Left => (position.x, 0, true), - KeyCode::Right => (position.x, MAP_WIDTH, true), + VirtualKeyCode::Up => (position.y, 0, false), + VirtualKeyCode::Down => (position.y, MAP_HEIGHT, false), + VirtualKeyCode::Left => (position.x, 0, true), + VirtualKeyCode::Right => (position.x, MAP_WIDTH, true), _ => continue, }; @@ -91,10 +92,10 @@ impl<'a> System<'a> for InputSystem { let position = positions.get_mut(entities.entity(id)); if let Some(position) = position { match key { - KeyCode::Up => position.y -= 1, - KeyCode::Down => position.y += 1, - KeyCode::Left => position.x -= 1, - KeyCode::Right => position.x += 1, + VirtualKeyCode::Up => position.y -= 1, + VirtualKeyCode::Down => position.y += 1, + VirtualKeyCode::Left => position.x -= 1, + VirtualKeyCode::Right => position.x += 1, _ => (), } } diff --git a/code/rust-sokoban-c02-05/src/systems/mod.rs b/code/rust-sokoban-c02-05/src/systems/mod.rs index 36ecda1..5043403 100644 --- a/code/rust-sokoban-c02-05/src/systems/mod.rs +++ b/code/rust-sokoban-c02-05/src/systems/mod.rs @@ -1,7 +1,7 @@ +mod gameplay_state_system; mod input_system; mod rendering_system; -mod gameplay_state_system; +pub use self::gameplay_state_system::GameplayStateSystem; pub use self::input_system::InputSystem; pub use self::rendering_system::RenderingSystem; -pub use self::gameplay_state_system::GameplayStateSystem; diff --git a/code/rust-sokoban-c02-05/src/systems/rendering_system.rs b/code/rust-sokoban-c02-05/src/systems/rendering_system.rs index 2942a42..e24d648 100644 --- a/code/rust-sokoban-c02-05/src/systems/rendering_system.rs +++ b/code/rust-sokoban-c02-05/src/systems/rendering_system.rs @@ -1,46 +1,45 @@ - - - -use crate::components::*; -use crate::resources::*; use crate::constants::TILE_WIDTH; +use crate::{components::*, resources::Gameplay}; -use ggez::{Context, graphics::{self, DrawParam, Image, Color}}; -use specs::{Join, ReadStorage, System, Read}; +use ggez::{ + graphics::{self, Canvas, Color, DrawParam, Image}, + Context, +}; use glam::Vec2; +use specs::{Join, Read, ReadStorage, System}; pub struct RenderingSystem<'a> { pub context: &'a mut Context, } impl RenderingSystem<'_> { - pub fn draw_text(&mut self, text_string: &str, x: f32, y: f32) { + pub fn draw_text(&mut self, canvas: &mut Canvas, text_string: &str, x: f32, y: f32) { let text = graphics::Text::new(text_string); let destination = Vec2::new(x, y); - let color = Some(Color::new(0.0, 0.0, 0.0, 1.0)); - let dimensions = Vec2::new(0.0, 20.0); + let color = Color::new(0.0, 0.0, 0.0, 1.0); - graphics::queue_text(self.context, &text, dimensions, color); - graphics::draw_queued_text( - self.context, - graphics::DrawParam::new().dest(destination), - None, - graphics::FilterMode::Linear, + canvas.draw( + &text, + graphics::DrawParam::new().dest(destination).color(color), ) - .expect("expected drawing queued text"); } } // System implementation impl<'a> System<'a> for RenderingSystem<'a> { // Data - type SystemData = (Read<'a, Gameplay>, ReadStorage<'a, Position>, ReadStorage<'a, Renderable>); + type SystemData = ( + Read<'a, Gameplay>, + ReadStorage<'a, Position>, + ReadStorage<'a, Renderable>, + ); fn run(&mut self, data: Self::SystemData) { let (gameplay, positions, renderables) = data; // Clearing the screen (this gives us the backround colour) - graphics::clear(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0)); + let mut canvas = + Canvas::from_frame(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0)); // Get all the renderables with their positions and sort by the position z // This will allow us to have entities layered visually. @@ -51,21 +50,22 @@ impl<'a> System<'a> for RenderingSystem<'a> { // and draw it at the specified position. for (position, renderable) in rendering_data.iter() { // Load the image - let image = Image::new(self.context, renderable.path.clone()).expect("expected image"); + let image = + Image::from_path(self.context, renderable.path.clone()).expect("expected image"); let x = position.x as f32 * TILE_WIDTH; let y = position.y as f32 * TILE_WIDTH; // draw let draw_params = DrawParam::new().dest(Vec2::new(x, y)); - graphics::draw(self.context, &image, draw_params).expect("expected render"); + canvas.draw(&image, draw_params); } // Render any text - self.draw_text(&gameplay.state.to_string(), 525.0, 80.0); - self.draw_text(&gameplay.moves_count.to_string(), 525.0, 100.0); + self.draw_text(&mut canvas, &gameplay.state.to_string(), 525.0, 80.0); + self.draw_text(&mut canvas, &gameplay.moves_count.to_string(), 525.0, 120.0); // Finally, present the context, this will actually display everything // on the screen. - graphics::present(self.context).expect("expected to present"); + canvas.finish(self.context).expect("expected to present"); } } From 28b020478f2f5be5dafd7a7635f334c98ded5ad4 Mon Sep 17 00:00:00 2001 From: Mateusz Kwiatkowski Date: Wed, 21 Dec 2022 17:19:11 +0100 Subject: [PATCH 09/15] update code and book for chapter 3.1 --- books/en_US/src/SUMMARY.md | 27 ++++---- .../{c03-01-colours.md => c03-01-colors.md} | 59 ++++++++++-------- .../src/images/{colours.gif => colors.gif} | Bin code/rust-sokoban-c03-01/Cargo.toml | 8 +-- code/rust-sokoban-c03-01/src/components.rs | 35 +++++------ code/rust-sokoban-c03-01/src/entities.rs | 13 ++-- code/rust-sokoban-c03-01/src/main.rs | 13 ++-- code/rust-sokoban-c03-01/src/map.rs | 10 +-- code/rust-sokoban-c03-01/src/resources.rs | 42 +++++-------- .../src/systems/gameplay_state_system.rs | 8 +-- .../src/systems/input_system.rs | 24 +++---- code/rust-sokoban-c03-01/src/systems/mod.rs | 4 +- .../src/systems/rendering_system.rs | 48 +++++++------- code/rust-sokoban-c03-02/Cargo.toml | 8 +-- 14 files changed, 145 insertions(+), 154 deletions(-) rename books/en_US/src/{c03-01-colours.md => c03-01-colors.md} (56%) rename books/en_US/src/images/{colours.gif => colors.gif} (100%) diff --git a/books/en_US/src/SUMMARY.md b/books/en_US/src/SUMMARY.md index 6b6e818..7b9142f 100644 --- a/books/en_US/src/SUMMARY.md +++ b/books/en_US/src/SUMMARY.md @@ -1,19 +1,18 @@ # Summary - [Base game](./c01-00-intro.md) - - [Project setup](./c01-01-setup.md) - - [Entity Component System](./c01-02-ecs.md) - - [Components and entities](./c01-03-entities-components.md) - - [Rendering system](./c01-04-rendering.md) + - [Project setup](./c01-01-setup.md) + - [Entity Component System](./c01-02-ecs.md) + - [Components and entities](./c01-03-entities-components.md) + - [Rendering system](./c01-04-rendering.md) - [Gameplay](./c02-00-intro.md) - - [Map loading](./c02-01-map-loading.md) - - [Moving the player](./c02-02-move-player.md) - - [Pushing boxes](./c02-03-push-box.md) - - [Modules](./c02-04-modules.md) - - [Gameplay](./c02-05-gameplay.md) + - [Map loading](./c02-01-map-loading.md) + - [Moving the player](./c02-02-move-player.md) + - [Pushing boxes](./c02-03-push-box.md) + - [Modules](./c02-04-modules.md) + - [Gameplay](./c02-05-gameplay.md) - [Advanced gameplay](./c03-00-intro.md) - - [Coloured boxes](./c03-01-colours.md) - - [Animations](./c03-02-animations.md) - - [Sounds and events](./c03-03-sounds-events.md) - - [Batch rendering](./c03-04-batch-rendering.md) - + - [Colored boxes](./c03-01-colors.md) + - [Animations](./c03-02-animations.md) + - [Sounds and events](./c03-03-sounds-events.md) + - [Batch rendering](./c03-04-batch-rendering.md) diff --git a/books/en_US/src/c03-01-colours.md b/books/en_US/src/c03-01-colors.md similarity index 56% rename from books/en_US/src/c03-01-colours.md rename to books/en_US/src/c03-01-colors.md index f975052..4e5612a 100644 --- a/books/en_US/src/c03-01-colours.md +++ b/books/en_US/src/c03-01-colors.md @@ -1,7 +1,9 @@ -# Coloured boxes -It's time for a little more flair in our game! The gameplay so far is quite simple, put the box on the spot. Let's make it more exciting by adding different coloured boxes. We'll go with red and blue for now but feel free to adapt this to your preference, and create more colours! To win now you'll have to put the box on the same colour spot to win. +# Colored boxes + +It's time for a little more flair in our game! The gameplay so far is quite simple, put the box on the spot. Let's make it more exciting by adding different colored boxes. We'll go with red and blue for now but feel free to adapt this to your preference, and create more colors! To win now you'll have to put the box on the same color spot to win. ## Assets + First let's add the new assets, right click and download these as images, or create your own! ![Blue box](./images/box_blue.png) @@ -38,43 +40,47 @@ The directory structure should look like this (notice we've removed the old defa ``` ## Component changes -Now let's add an enum for the colour (if you chose to implement more than two colours you'll have to add them here). + +Now let's add an enum for the color (if you chose to implement more than two colors you'll have to add them here). ```rust // components.rs -{{#include ../../../code/rust-sokoban-c03-01/src/components.rs:29:32}} +{{#include ../../../code/rust-sokoban-c03-01/src/components.rs:35:39}} ``` -Now let's use this enum both for the box and the spot. +Now let's use this enum both for the box and the spot. ```rust // components.rs -{{#include ../../../code/rust-sokoban-c03-01/src/components.rs:44:54}} +{{#include ../../../code/rust-sokoban-c03-01/src/components.rs:51:61}} ``` ## Entity creation -Let's also add the colour as a parameter when we created boxes and spots and make sure we pass the correct asset path based on the colour enum. -In order to create the correct string for the asset path we basically want `"/images/box_{}.png"` where `{}` is the colour of the box we are trying to create. The challenge we have now is that we are using an enum for the colour, so the Rust compiler will not know how to convert `BoxColour::Red` into `"red"`. It would be really cool to be able to do `colour.to_string()` and get the right value. Fortunately, Rust has a nice way for us to do this, we need to implement the `Display` trait on the `BoxColour` enum. Here is how that looks like, we simply specify how to map each variant of the enum into a string. +Let's also add the color as a parameter when we created boxes and spots and make sure we pass the correct asset path based on the color enum. + +In order to create the correct string for the asset path we basically want `"/images/box_{}.png"` where `{}` is the color of the box we are trying to create. The challenge we have now is that we are using an enum for the color, so the Rust compiler will not know how to convert `BoxColor::Red` into `"red"`. It would be really cool to be able to do `color.to_string()` and get the right value. Fortunately, Rust has a nice way for us to do this, we need to implement the `Display` trait on the `BoxColor` enum. Here is how that looks like, we simply specify how to map each variant of the enum into a string. ```rust // components.rs -{{#include ../../../code/rust-sokoban-c03-01/src/components.rs:34:43}} +{{#include ../../../code/rust-sokoban-c03-01/src/components.rs:41:49}} ``` -Now let's include the colour in our entity creation code and use the fancy `colour.to_string()` we just made possible in the previous snippet. +Now let's include the color in our entity creation code and use the fancy `color.to_string()` we just made possible in the previous snippet. ```rust // entities.rs -{{#include ../../../code/rust-sokoban-c03-01/src/entities.rs:27:48}} +{{#include ../../../code/rust-sokoban-c03-01/src/entities.rs:26:47}} ``` ## Map -Now let's change our map code to allow new options for coloured boxes and spots: -* "BB" for blue box -* "RB" for red box -* "BS" for blue spot -* "RS" for red spot + +Now let's change our map code to allow new options for colored boxes and spots: + +- "BB" for blue box +- "RB" for red box +- "BS" for blue spot +- "RS" for red spot ```rust // map.rs @@ -85,15 +91,16 @@ And let's update our static map in the main. ```rust // main.rs -{{#include ../../../code/rust-sokoban-c03-01/src/main.rs:65:80}} +{{#include ../../../code/rust-sokoban-c03-01/src/main.rs:63:77}} ``` ## Gameplay -Now we've done the hard work, so we can go ahead and test this code out. You'll notice everything works, but there is a big gameplay bug. You can win by putting the red box on the blue spot and viceversa. Let's fix that. + +Now we've done the hard work, so we can go ahead and test this code out. You'll notice everything works, but there is a big gameplay bug. You can win by putting the red box on the blue spot and viceversa. Let's fix that. We've learnt before that data goes in components and behaviour goes in systems - as per ECS methodology. What we are discussing now is behaviour, so it must be in a system. Remember how we added a system for checking whether you've won or not? Well that is the exact place we are after. -Let's modify the run function and check the colour of the spot and the box match. +Let's modify the run function and check the color of the spot and the box match. ```rust // gameplay_state_system.rs @@ -104,12 +111,12 @@ Now if you compile the code at this point it should complain about the fact that ```rust // components.rs -{{#include ../../../code/rust-sokoban-c03-01/src/components.rs:28:32}} +{{#include ../../../code/rust-sokoban-c03-01/src/components.rs:35:39}} ``` -Now is a good time to discuss these unusual `derive` annotations. We've used them before, but never got too deep into what they do. Derive attributes can be applied to structs or enums and they allow us to add default trait implementations to our types. For example here we are telling Rust to add the `PartialEq` default trait implementations to our `BoxColour` enum. +Now is a good time to discuss these unusual `derive` annotations. We've used them before, but never got too deep into what they do. Derive attributes can be applied to structs or enums and they allow us to add default trait implementations to our types. For example here we are telling Rust to add the `PartialEq` default trait implementations to our `BoxColor` enum. -Here is how the `PartialEq` default implementation looks like, it just checks if something equals itself. If it does, the comparison succeeds and if it doesn't it fails. Don't worry too much about this if it doesn't make sense. +Here is how the `PartialEq` default implementation looks like, it just checks if something equals itself. If it does, the comparison succeeds and if it doesn't it fails. Don't worry too much about this if it doesn't make sense. ```rust pub trait PartialEq { @@ -118,12 +125,12 @@ pub trait PartialEq { } ``` -So by adding the `#[derive(PartialEq)]` on top of the enum we are telling Rust that `BoxColour` now implements the partial eq trait we saw before, which means if we try to do `box_colour_1 == box_colour_2` it will use this implementation which will just check if the colour_1 object is the same as the colour_2 object. This is not the most sophisticated partial equality implementation, but it should do just fine for our usecase. +So by adding the `#[derive(PartialEq)]` on top of the enum we are telling Rust that `BoxColor` now implements the partial eq trait we saw before, which means if we try to do `box_color_1 == box_color_2` it will use this implementation which will just check if the color_1 object is the same as the color_2 object. This is not the most sophisticated partial equality implementation, but it should do just fine for our usecase. -> **_MORE:_** Read more about PartialEq [here](https://doc.rust-lang.org/std/cmp/trait.PartialEq.html) and more about derivable traits [here](https://doc.rust-lang.org/book/appendix-03-derivable-traits.html). +> **_MORE:_** Read more about PartialEq [here](https://doc.rust-lang.org/std/cmp/trait.PartialEq.html) and more about derivable traits [here](https://doc.rust-lang.org/book/appendix-03-derivable-traits.html). Now we can compile the code the reap the rewards of our efforts by seeing the game run and telling us we've won only when we put the right box in the right spot! -![Sokoban play](./images/colours.gif) +![Sokoban play](./images/colors.gif) -> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c03-01). \ No newline at end of file +> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c03-01). diff --git a/books/en_US/src/images/colours.gif b/books/en_US/src/images/colors.gif similarity index 100% rename from books/en_US/src/images/colours.gif rename to books/en_US/src/images/colors.gif diff --git a/code/rust-sokoban-c03-01/Cargo.toml b/code/rust-sokoban-c03-01/Cargo.toml index 92e5cac..11bf251 100644 --- a/code/rust-sokoban-c03-01/Cargo.toml +++ b/code/rust-sokoban-c03-01/Cargo.toml @@ -2,11 +2,11 @@ name = "rust-sokoban" version = "0.1.0" authors = ["Olivia "] -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ggez = "0.7" -glam = { version = "0.20.0", features = ["mint"] } -specs = { version = "0.17.0", features = ["specs-derive"] } \ No newline at end of file +ggez = "0.8.1" +glam = { version = "0.22.0", features = ["mint"]} +specs = { version = "0.18.0", features = ["specs-derive"] } \ No newline at end of file diff --git a/code/rust-sokoban-c03-01/src/components.rs b/code/rust-sokoban-c03-01/src/components.rs index 6a1deea..140cd6b 100644 --- a/code/rust-sokoban-c03-01/src/components.rs +++ b/code/rust-sokoban-c03-01/src/components.rs @@ -1,8 +1,7 @@ -use specs::{Component, NullStorage, VecStorage, World, WorldExt}; - use std::fmt::{self, Display}; -// Components +use specs::{Component, NullStorage, VecStorage, World, WorldExt}; + #[derive(Debug, Component, Clone, Copy)] #[storage(VecStorage)] pub struct Position { @@ -25,17 +24,25 @@ pub struct Wall {} #[storage(VecStorage)] pub struct Player {} +#[derive(Component, Default)] +#[storage(NullStorage)] +pub struct Movable; + +#[derive(Component, Default)] +#[storage(NullStorage)] +pub struct Immovable; + #[derive(PartialEq)] -pub enum BoxColour { - Red, +pub enum BoxColor { Blue, + Red, } -impl Display for BoxColour { +impl Display for BoxColor { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt.write_str(match self { - BoxColour::Red => "red", - BoxColour::Blue => "blue", + BoxColor::Red => "red", + BoxColor::Blue => "blue", })?; Ok(()) } @@ -44,23 +51,15 @@ impl Display for BoxColour { #[derive(Component)] #[storage(VecStorage)] pub struct Box { - pub colour: BoxColour, + pub color: BoxColor, } #[derive(Component)] #[storage(VecStorage)] pub struct BoxSpot { - pub colour: BoxColour, + pub color: BoxColor, } -#[derive(Component, Default)] -#[storage(NullStorage)] -pub struct Movable; - -#[derive(Component, Default)] -#[storage(NullStorage)] -pub struct Immovable; - pub fn register_components(world: &mut World) { world.register::(); world.register::(); diff --git a/code/rust-sokoban-c03-01/src/entities.rs b/code/rust-sokoban-c03-01/src/entities.rs index 12a7823..e3a95b5 100644 --- a/code/rust-sokoban-c03-01/src/entities.rs +++ b/code/rust-sokoban-c03-01/src/entities.rs @@ -1,7 +1,6 @@ use crate::components::*; use specs::{Builder, World, WorldExt}; -// Create a wall entity pub fn create_wall(world: &mut World, position: Position) { world .create_entity() @@ -24,26 +23,26 @@ pub fn create_floor(world: &mut World, position: Position) { .build(); } -pub fn create_box(world: &mut World, position: Position, colour: BoxColour) { +pub fn create_box(world: &mut World, position: Position, color: BoxColor) { world .create_entity() .with(Position { z: 10, ..position }) .with(Renderable { - path: format!("/images/box_{}.png", colour), + path: format!("/images/box_{}.png", color), }) - .with(Box { colour }) + .with(Box { color }) .with(Movable) .build(); } -pub fn create_box_spot(world: &mut World, position: Position, colour: BoxColour) { +pub fn create_box_spot(world: &mut World, position: Position, color: BoxColor) { world .create_entity() .with(Position { z: 9, ..position }) .with(Renderable { - path: format!("/images/box_spot_{}.png", colour), + path: format!("/images/box_spot_{}.png", color), }) - .with(BoxSpot { colour }) + .with(BoxSpot { color }) .build(); } diff --git a/code/rust-sokoban-c03-01/src/main.rs b/code/rust-sokoban-c03-01/src/main.rs index 8336960..18e7f16 100644 --- a/code/rust-sokoban-c03-01/src/main.rs +++ b/code/rust-sokoban-c03-01/src/main.rs @@ -1,7 +1,7 @@ // Rust sokoban // main.rs -use ggez::{conf, event::{self, KeyCode, KeyMods}, Context, GameResult}; +use ggez::{conf, event, input::keyboard::KeyInput, Context, GameResult}; use specs::{RunNow, World, WorldExt}; use std::path; @@ -51,14 +51,12 @@ impl event::EventHandler for Game { fn key_down_event( &mut self, _context: &mut Context, - keycode: KeyCode, - _keymod: KeyMods, + keyinput: KeyInput, _repeat: bool, - ) { - println!("Key pressed: {:?}", keycode); - + ) -> GameResult { let mut input_queue = self.world.write_resource::(); - input_queue.keys_pressed.push(keycode); + input_queue.keys_pressed.push(keyinput.keycode.unwrap()); + Ok(()) } } @@ -75,7 +73,6 @@ pub fn initialize_level(world: &mut World) { W . . . . . . W W W W W W W W W "; - load_map(world, MAP.to_string()); } diff --git a/code/rust-sokoban-c03-01/src/map.rs b/code/rust-sokoban-c03-01/src/map.rs index 65caa3e..d923ad8 100644 --- a/code/rust-sokoban-c03-01/src/map.rs +++ b/code/rust-sokoban-c03-01/src/map.rs @@ -1,4 +1,4 @@ -use crate::components::{BoxColour, Position}; +use crate::components::{BoxColor, Position}; use crate::entities::*; use specs::World; @@ -30,19 +30,19 @@ pub fn load_map(world: &mut World, map_string: String) { } "BB" => { create_floor(world, position); - create_box(world, position, BoxColour::Blue); + create_box(world, position, BoxColor::Blue); } "RB" => { create_floor(world, position); - create_box(world, position, BoxColour::Red); + create_box(world, position, BoxColor::Red); } "BS" => { create_floor(world, position); - create_box_spot(world, position, BoxColour::Blue); + create_box_spot(world, position, BoxColor::Blue); } "RS" => { create_floor(world, position); - create_box_spot(world, position, BoxColour::Red); + create_box_spot(world, position, BoxColor::Red); } "N" => (), c => panic!("unrecognized map item {}", c), diff --git a/code/rust-sokoban-c03-01/src/resources.rs b/code/rust-sokoban-c03-01/src/resources.rs index b319be1..470e52e 100644 --- a/code/rust-sokoban-c03-01/src/resources.rs +++ b/code/rust-sokoban-c03-01/src/resources.rs @@ -1,42 +1,34 @@ -use ggez::event::KeyCode; +use ggez::winit::event::VirtualKeyCode; use specs::World; +use std::fmt::{Display, Formatter, Result}; -use std::fmt::{self, Display}; - -// Resources #[derive(Default)] pub struct InputQueue { - pub keys_pressed: Vec, -} - -pub fn register_resources(world: &mut World) { - world.insert(InputQueue::default()); - world.insert(Gameplay::default()); + pub keys_pressed: Vec, } +#[derive(Debug, Default, Clone, Copy)] pub enum GameplayState { + #[default] Playing, - Won + Won, } impl Display for GameplayState { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - fmt.write_str(match self { - GameplayState::Playing => "Playing", - GameplayState::Won => "Won" - })?; - Ok(()) + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + GameplayState::Playing => write!(f, "Playing"), + GameplayState::Won => write!(f, "Won"), + } } } - -impl Default for GameplayState { - fn default() -> Self { - Self::Playing - } -} - #[derive(Default)] pub struct Gameplay { pub state: GameplayState, - pub moves_count: u32 + pub moves_count: u32, +} + +pub fn register_resources(world: &mut World) { + world.insert(InputQueue::default()); + world.insert(Gameplay::default()); } diff --git a/code/rust-sokoban-c03-01/src/systems/gameplay_state_system.rs b/code/rust-sokoban-c03-01/src/systems/gameplay_state_system.rs index 9acf34f..e592118 100644 --- a/code/rust-sokoban-c03-01/src/systems/gameplay_state_system.rs +++ b/code/rust-sokoban-c03-01/src/systems/gameplay_state_system.rs @@ -27,15 +27,13 @@ impl<'a> System<'a> for GameplayStateSystem { .collect::>(); // loop through all box spots and check if there is a corresponding - // box at that position. since we now have different types of boxes - // we need to make sure the right type of box is on the right - // type of spot. + // box at that position for (box_spot, position) in (&box_spots, &positions).join() { if let Some(the_box) = boxes_by_position.get(&(position.x, position.y)) { - if the_box.colour == box_spot.colour { + if the_box.color == box_spot.color { // continue } else { - // return, haven't won yet + // haven't won yet return; } } else { diff --git a/code/rust-sokoban-c03-01/src/systems/input_system.rs b/code/rust-sokoban-c03-01/src/systems/input_system.rs index 74ef39a..318ac83 100644 --- a/code/rust-sokoban-c03-01/src/systems/input_system.rs +++ b/code/rust-sokoban-c03-01/src/systems/input_system.rs @@ -1,7 +1,7 @@ use crate::components::*; use crate::constants::*; -use crate::resources::{InputQueue, Gameplay}; -use ggez::event::KeyCode; +use crate::resources::{Gameplay, InputQueue}; +use ggez::winit::event::VirtualKeyCode; use specs::{world::Index, Entities, Join, ReadStorage, System, Write, WriteStorage}; use std::collections::HashMap; @@ -22,7 +22,8 @@ impl<'a> System<'a> for InputSystem { ); fn run(&mut self, data: Self::SystemData) { - let (mut input_queue, mut gameplay, entities, mut positions, players, movables, immovables) = data; + let (mut input_queue, mut gameplay, entities, mut positions, players, movables, immovables) = + data; let mut to_move = Vec::new(); @@ -42,10 +43,10 @@ impl<'a> System<'a> for InputSystem { // Now iterate through current position to the end of the map // on the correct axis and check what needs to move. let (start, end, is_x) = match key { - KeyCode::Up => (position.y, 0, false), - KeyCode::Down => (position.y, MAP_HEIGHT, false), - KeyCode::Left => (position.x, 0, true), - KeyCode::Right => (position.x, MAP_WIDTH, true), + VirtualKeyCode::Up => (position.y, 0, false), + VirtualKeyCode::Down => (position.y, MAP_HEIGHT, false), + VirtualKeyCode::Left => (position.x, 0, true), + VirtualKeyCode::Right => (position.x, MAP_WIDTH, true), _ => continue, }; @@ -81,7 +82,6 @@ impl<'a> System<'a> for InputSystem { } } - // We've just moved, so let's increase the number of moves if to_move.len() > 0 { gameplay.moves_count += 1; } @@ -91,10 +91,10 @@ impl<'a> System<'a> for InputSystem { let position = positions.get_mut(entities.entity(id)); if let Some(position) = position { match key { - KeyCode::Up => position.y -= 1, - KeyCode::Down => position.y += 1, - KeyCode::Left => position.x -= 1, - KeyCode::Right => position.x += 1, + VirtualKeyCode::Up => position.y -= 1, + VirtualKeyCode::Down => position.y += 1, + VirtualKeyCode::Left => position.x -= 1, + VirtualKeyCode::Right => position.x += 1, _ => (), } } diff --git a/code/rust-sokoban-c03-01/src/systems/mod.rs b/code/rust-sokoban-c03-01/src/systems/mod.rs index 36ecda1..5043403 100644 --- a/code/rust-sokoban-c03-01/src/systems/mod.rs +++ b/code/rust-sokoban-c03-01/src/systems/mod.rs @@ -1,7 +1,7 @@ +mod gameplay_state_system; mod input_system; mod rendering_system; -mod gameplay_state_system; +pub use self::gameplay_state_system::GameplayStateSystem; pub use self::input_system::InputSystem; pub use self::rendering_system::RenderingSystem; -pub use self::gameplay_state_system::GameplayStateSystem; diff --git a/code/rust-sokoban-c03-01/src/systems/rendering_system.rs b/code/rust-sokoban-c03-01/src/systems/rendering_system.rs index 2942a42..e24d648 100644 --- a/code/rust-sokoban-c03-01/src/systems/rendering_system.rs +++ b/code/rust-sokoban-c03-01/src/systems/rendering_system.rs @@ -1,46 +1,45 @@ - - - -use crate::components::*; -use crate::resources::*; use crate::constants::TILE_WIDTH; +use crate::{components::*, resources::Gameplay}; -use ggez::{Context, graphics::{self, DrawParam, Image, Color}}; -use specs::{Join, ReadStorage, System, Read}; +use ggez::{ + graphics::{self, Canvas, Color, DrawParam, Image}, + Context, +}; use glam::Vec2; +use specs::{Join, Read, ReadStorage, System}; pub struct RenderingSystem<'a> { pub context: &'a mut Context, } impl RenderingSystem<'_> { - pub fn draw_text(&mut self, text_string: &str, x: f32, y: f32) { + pub fn draw_text(&mut self, canvas: &mut Canvas, text_string: &str, x: f32, y: f32) { let text = graphics::Text::new(text_string); let destination = Vec2::new(x, y); - let color = Some(Color::new(0.0, 0.0, 0.0, 1.0)); - let dimensions = Vec2::new(0.0, 20.0); + let color = Color::new(0.0, 0.0, 0.0, 1.0); - graphics::queue_text(self.context, &text, dimensions, color); - graphics::draw_queued_text( - self.context, - graphics::DrawParam::new().dest(destination), - None, - graphics::FilterMode::Linear, + canvas.draw( + &text, + graphics::DrawParam::new().dest(destination).color(color), ) - .expect("expected drawing queued text"); } } // System implementation impl<'a> System<'a> for RenderingSystem<'a> { // Data - type SystemData = (Read<'a, Gameplay>, ReadStorage<'a, Position>, ReadStorage<'a, Renderable>); + type SystemData = ( + Read<'a, Gameplay>, + ReadStorage<'a, Position>, + ReadStorage<'a, Renderable>, + ); fn run(&mut self, data: Self::SystemData) { let (gameplay, positions, renderables) = data; // Clearing the screen (this gives us the backround colour) - graphics::clear(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0)); + let mut canvas = + Canvas::from_frame(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0)); // Get all the renderables with their positions and sort by the position z // This will allow us to have entities layered visually. @@ -51,21 +50,22 @@ impl<'a> System<'a> for RenderingSystem<'a> { // and draw it at the specified position. for (position, renderable) in rendering_data.iter() { // Load the image - let image = Image::new(self.context, renderable.path.clone()).expect("expected image"); + let image = + Image::from_path(self.context, renderable.path.clone()).expect("expected image"); let x = position.x as f32 * TILE_WIDTH; let y = position.y as f32 * TILE_WIDTH; // draw let draw_params = DrawParam::new().dest(Vec2::new(x, y)); - graphics::draw(self.context, &image, draw_params).expect("expected render"); + canvas.draw(&image, draw_params); } // Render any text - self.draw_text(&gameplay.state.to_string(), 525.0, 80.0); - self.draw_text(&gameplay.moves_count.to_string(), 525.0, 100.0); + self.draw_text(&mut canvas, &gameplay.state.to_string(), 525.0, 80.0); + self.draw_text(&mut canvas, &gameplay.moves_count.to_string(), 525.0, 120.0); // Finally, present the context, this will actually display everything // on the screen. - graphics::present(self.context).expect("expected to present"); + canvas.finish(self.context).expect("expected to present"); } } diff --git a/code/rust-sokoban-c03-02/Cargo.toml b/code/rust-sokoban-c03-02/Cargo.toml index 92e5cac..11bf251 100644 --- a/code/rust-sokoban-c03-02/Cargo.toml +++ b/code/rust-sokoban-c03-02/Cargo.toml @@ -2,11 +2,11 @@ name = "rust-sokoban" version = "0.1.0" authors = ["Olivia "] -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ggez = "0.7" -glam = { version = "0.20.0", features = ["mint"] } -specs = { version = "0.17.0", features = ["specs-derive"] } \ No newline at end of file +ggez = "0.8.1" +glam = { version = "0.22.0", features = ["mint"]} +specs = { version = "0.18.0", features = ["specs-derive"] } \ No newline at end of file From 9055d598a30fa45b51c3b8a23199b59b2843a7a8 Mon Sep 17 00:00:00 2001 From: Mateusz Kwiatkowski Date: Wed, 21 Dec 2022 18:53:16 +0100 Subject: [PATCH 10/15] update code and book for chapter 3.2 --- books/en_US/src/c03-02-animations.md | 74 ++++++++++--------- code/rust-sokoban-c03-02/src/components.rs | 40 +++++----- code/rust-sokoban-c03-02/src/entities.rs | 15 ++-- code/rust-sokoban-c03-02/src/main.rs | 17 ++--- code/rust-sokoban-c03-02/src/map.rs | 10 +-- code/rust-sokoban-c03-02/src/resources.rs | 44 +++++------ .../src/systems/gameplay_state_system.rs | 8 +- .../src/systems/input_system.rs | 23 +++--- code/rust-sokoban-c03-02/src/systems/mod.rs | 4 +- .../src/systems/rendering_system.rs | 44 +++++------ 10 files changed, 133 insertions(+), 146 deletions(-) diff --git a/books/en_US/src/c03-02-animations.md b/books/en_US/src/c03-02-animations.md index d2ec577..525957e 100644 --- a/books/en_US/src/c03-02-animations.md +++ b/books/en_US/src/c03-02-animations.md @@ -1,29 +1,36 @@ # Animations -In this section we are going to look at adding animations to our game, we'll start with some basic ones but feel free to add more complex ones given the ideas in this tutorial. We'll add two animations: making the player blink and making the boxes jiggle slightly in place. + +In this section we are going to look at adding animations to our game, we'll start with some basic ones but feel free to add more complex ones given the ideas in this tutorial. We'll add two animations: making the player blink and making the boxes jiggle slightly in place. ## What is an animation? -An animation is simply a set of frames played at a specific time interval that gives the illusion of movement. Think of it like a video (a video is just a set of images played in sequence), but much lower framerate. -For example, to get our player blinking we'll have three animation frames: +An animation is simply a set of frames played at a specific time interval that gives the illusion of movement. Think of it like a video (a video is just a set of images played in sequence), but much lower framerate. + +For example, to get our player blinking we'll have three animation frames: + 1. our current player with the eyes open 1. player with eyes a little bit closed 1. player with eyes completely closed -If we play these three frames in sequence you'll notice it looks like the player is blinking. You can try this out by opening the images and shifting between them quickly on the image preview. +If we play these three frames in sequence you'll notice it looks like the player is blinking. You can try this out by opening the images and shifting between them quickly on the image preview. -There are a few gotchas on this: -* the assets need to be done with a specific framerate in mind - for us we will go with 250 milliseconds, meaning we will play a new animation frame every 250ms, so we will have 4 frames per second -* the assets need to be consistent with each other - imagine we had two types of players which had different assets and different looking eyes, we would have to make sure that when we create the three frames mentioned above they would be consistent, otherwise the two players would blink at different rates -* designing assets for a lot of frames is a lot of work, so we'll try to keep our animations quite simple and stick to the key frames +There are a few gotchas on this: + +- the assets need to be done with a specific framerate in mind - for us we will go with 250 milliseconds, meaning we will play a new animation frame every 250ms, so we will have 4 frames per second +- the assets need to be consistent with each other - imagine we had two types of players which had different assets and different looking eyes, we would have to make sure that when we create the three frames mentioned above they would be consistent, otherwise the two players would blink at different rates +- designing assets for a lot of frames is a lot of work, so we'll try to keep our animations quite simple and stick to the key frames ## How will it work? + So how is this going to work in our existing Sokoban game? We'll have to: + 1. Change our renderable component to allow multiple frames - we could also create a new renderable component that handles animated renderables and keep the one we have for static renderables, but it feels a bit cleaner to keep them together for now 1. Modify the player entity construction to take multiple frames 1. Keep track of time in our rendering loop - we'll discuss this one in more detail so don't worry if it's not obvious why we need to do this 1. Change the rendering system taking into account the number of frames, the time and the frame that is supposed to be rendered at a given time ## Assets + Let's add the new assets for the player, it should then look like this. Notice we created a convention to name the frames sequentially, this is not strictly necessary, but it will help us keep track of the order easily. ![Player 1](./images/player_1.png) @@ -45,11 +52,12 @@ Let's add the new assets for the player, it should then look like this. Notice w ``` ## Renderable -Now let's update our renderable component to receive multiple frames, instead of having a single path, we'll have a list of paths, this should be pretty straightforward. + +Now let's update our renderable component to receive multiple frames, instead of having a single path, we'll have a list of paths, this should be pretty straightforward. Let's also add two new functions to construct the two types of renderables, either with a single path or with multiple paths. These two functions are associated functions, because they are associated with the struct `Renderable`, but they are the equivalent of static functions in other languages since they don't operate on instances (notice they don't receive `&self` or `&mut self` as the first argument, which means we can call them in the context of the struct not an instance of the struct). They are also similar to factory functions, since they encapsulate the logic and validation required before actually constructing an object. -> **_MORE:_** Read more about associated functions [here](https://doc.rust-lang.org/book/ch05-03-method-syntax.html#associated-functions). +> **_MORE:_** Read more about associated functions [here](https://doc.rust-lang.org/book/ch05-03-method-syntax.html#associated-functions). ```rust // components.rs @@ -61,7 +69,7 @@ Next, we need a way of telling if a renderable is animated or static, which we w ```rust // components.rs -{{#include ../../../code/rust-sokoban-c03-02/src/components.rs:14:18}} +{{#include ../../../code/rust-sokoban-c03-02/src/components.rs:14:17}} ``` Now let's add a function to tell us the kind of a renderable based on the internal paths. @@ -72,7 +80,7 @@ Now let's add a function to tell us the kind of a renderable based on the intern {{#include ../../../code/rust-sokoban-c03-02/src/components.rs:48}} ``` -And finally, because we made paths private, we need to allow users of renderable to get a specific path from our list. For static renderables, this will be the 0th path (the only one) and for animated paths we'll let the rendering system decide which path should be rendered based on the time. The only tricky bit here is if we get asked for a frame bigger than what we have, we will wrap that around by modding with the length. +And finally, because we made paths private, we need to allow users of renderable to get a specific path from our list. For static renderables, this will be the 0th path (the only one) and for animated paths we'll let the rendering system decide which path should be rendered based on the time. The only tricky bit here is if we get asked for a frame bigger than what we have, we will wrap that around by modding with the length. ```rust // components.rs @@ -84,39 +92,41 @@ And finally, because we made paths private, we need to allow users of renderable ``` ## Entity creation + Next up, let's update our player entity creation to account for multiple paths. Notice now we are using the `new_animated` function to construct the renderable. ```rust // entities.rs -{{#include ../../../code/rust-sokoban-c03-02/src/entities.rs:48:60}} +{{#include ../../../code/rust-sokoban-c03-02/src/entities.rs:47:60}} ``` And let's update everything else to use the `new_static` function - here is how we are doing it for the wall entity creation, feel free to go ahead and apply this to the other static entities. ```rust // entities.rs -{{#include ../../../code/rust-sokoban-c03-02/src/entities.rs:5:14}} +{{#include ../../../code/rust-sokoban-c03-02/src/entities.rs:4:12}} ``` ## Time -Another component we will need for this is keeping track of time. What does time have to do with this and how does this connect with frame rate? The basic idea is this: ggez controls how often the rendering system gets called, and this depends on the frame rate which in turn depends on how much work we are doing on every iteration of the game loop. Because we don't control this, in the span of a second we could get called 60 times or 57 times or maybe even 30 times. This means we cannot base our animation system on the framerate, and instead we need to keep it based on time. + +Another component we will need for this is keeping track of time. What does time have to do with this and how does this connect with frame rate? The basic idea is this: ggez controls how often the rendering system gets called, and this depends on the frame rate which in turn depends on how much work we are doing on every iteration of the game loop. Because we don't control this, in the span of a second we could get called 60 times or 57 times or maybe even 30 times. This means we cannot base our animation system on the framerate, and instead we need to keep it based on time. Because of this we need to keep track of the delta time - or how much time passes between the previous loop and the current loop. And because the delta time is much smaller than our animation frame interval (which we have decided on 250ms), we need to keep the cumulative delta - or how much time has passed since the beginning of the game being launched. -> **_MORE:_** Read more about delta time, frame rate and game loops [here](https://medium.com/@dr3wc/understanding-delta-time-b53bf4781a03#:~:text=Delta%20time%20describes%20the%20time,drawn%20and%20the%20current%20frame.&text=If%20you%20read%20my%20article,until%20the%20game%20is%20stopped.), [here](https://www.reddit.com/r/pcmasterrace/comments/29qcqr/an_explanation_of_game_loops_fps_and_delta_time/) or [here](https://www.youtube.com/watch?v=pctGOMDW-HQ&list=PLlrATfBNZ98dC-V-N3m0Go4deliWHPFwT&index=37) . +> **_MORE:_** Read more about delta time, frame rate and game loops [here](https://medium.com/@dr3wc/understanding-delta-time-b53bf4781a03#:~:text=Delta%20time%20describes%20the%20time,drawn%20and%20the%20current%20frame.&text=If%20you%20read%20my%20article,until%20the%20game%20is%20stopped.), [here](https://www.reddit.com/r/pcmasterrace/comments/29qcqr/an_explanation_of_game_loops_fps_and_delta_time/) or [here](https://www.youtube.com/watch?v=pctGOMDW-HQ&list=PLlrATfBNZ98dC-V-N3m0Go4deliWHPFwT&index=37) . Let's now add a resource for time, this doesn't fit into our component model since time is just some global state that needs to be kept. ```rust // resources.rs -{{#include ../../../code/rust-sokoban-c03-02/src/resources.rs:45:48}} +{{#include ../../../code/rust-sokoban-c03-02/src/resources.rs:33:36}} ``` And don't forget to register the new resource. ```rust // resources.rs -{{#include ../../../code/rust-sokoban-c03-02/src/resources.rs:12:16}} +{{#include ../../../code/rust-sokoban-c03-02/src/resources.rs:38:42}} ``` And now let's update this time in our main loop. Luckily ggez provides a function to get the delta, so all we have to do is accumulate it. @@ -126,37 +136,36 @@ And now let's update this time in our main loop. Luckily ggez provides a functio {{#include ../../../code/rust-sokoban-c03-02/src/main.rs:24:45}} ``` - ## Rendering system + Now let's update our rendering system. We will get the kind from the renderable, if it's static we simply use the first frame, otherwise we figure out which frame to get based on the delta time. Let's first add a function to enapsulate this logic of getting the correct image. ```rust // rendering_system.rs -{{#include ../../../code/rust-sokoban-c03-02/src/systems/rendering_system.rs:17}} +{{#include ../../../code/rust-sokoban-c03-02/src/systems/rendering_system.rs:18}} //... -{{#include ../../../code/rust-sokoban-c03-02/src/systems/rendering_system.rs:34:54}} +{{#include ../../../code/rust-sokoban-c03-02/src/systems/rendering_system.rs:30:49}} ``` And finally, let's use the new `get_image` function inside the run function (we will also have to add time to the `SystemData` definition and a couple of imports, but that should be pretty much it). ```rust // rendering_system.rs -{{#include ../../../code/rust-sokoban-c03-02/src/systems/rendering_system.rs:57:81}} +{{#include ../../../code/rust-sokoban-c03-02/src/systems/rendering_system.rs:52:77}} //... - -{{#include ../../../code/rust-sokoban-c03-02/src/systems/rendering_system.rs:88}} - //... +{{#include ../../../code/rust-sokoban-c03-02/src/systems/rendering_system.rs:84}} -{{#include ../../../code/rust-sokoban-c03-02/src/systems/rendering_system.rs:97}} -{{#include ../../../code/rust-sokoban-c03-02/src/systems/rendering_system.rs:98}} + //... +{{#include ../../../code/rust-sokoban-c03-02/src/systems/rendering_system.rs:93:94}} ``` ## Box animations + Now that we've learned how to do this, let's extend this to make the boxes animate as well. All we have to do is add new assets and fix the entity creation and everything should just work. Here are the assets I used, feel free to re-use them or create new ones! ![Box red 1](./images/box_red_1.png) @@ -165,16 +174,9 @@ Now that we've learned how to do this, let's extend this to make the boxes anima ![Box blue 2](./images/box_blue_2.png) ## Wrap up + That was a long section, but I hope you enjoyed it! Here is how the game should look now. ![Sokoban animations](./images/animations.gif) -> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c03-02). - - - - - - - - +> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c03-02). diff --git a/code/rust-sokoban-c03-02/src/components.rs b/code/rust-sokoban-c03-02/src/components.rs index 9f4f26d..ad68acd 100644 --- a/code/rust-sokoban-c03-02/src/components.rs +++ b/code/rust-sokoban-c03-02/src/components.rs @@ -1,8 +1,7 @@ -use specs::{Component, NullStorage, VecStorage, World, WorldExt}; - use std::fmt::{self, Display}; -// Components +use specs::{Component, NullStorage, VecStorage, World, WorldExt}; + #[derive(Debug, Component, Clone, Copy)] #[storage(VecStorage)] pub struct Position { @@ -11,6 +10,7 @@ pub struct Position { pub z: u8, } +#[derive(Component)] pub enum RenderableKind { Static, Animated, @@ -19,7 +19,7 @@ pub enum RenderableKind { #[derive(Component)] #[storage(VecStorage)] pub struct Renderable { - paths: Vec, + pub paths: Vec, } impl Renderable { @@ -33,7 +33,7 @@ impl Renderable { pub fn kind(&self) -> RenderableKind { match self.paths.len() { - 0 => panic!("invalid renderable"), + 0 => panic!("invalid renderable!"), 1 => RenderableKind::Static, _ => RenderableKind::Animated, } @@ -55,17 +55,25 @@ pub struct Wall {} #[storage(VecStorage)] pub struct Player {} +#[derive(Component, Default)] +#[storage(NullStorage)] +pub struct Movable; + +#[derive(Component, Default)] +#[storage(NullStorage)] +pub struct Immovable; + #[derive(PartialEq)] -pub enum BoxColour { - Red, +pub enum BoxColor { Blue, + Red, } -impl Display for BoxColour { +impl Display for BoxColor { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt.write_str(match self { - BoxColour::Red => "red", - BoxColour::Blue => "blue", + BoxColor::Red => "red", + BoxColor::Blue => "blue", })?; Ok(()) } @@ -74,23 +82,15 @@ impl Display for BoxColour { #[derive(Component)] #[storage(VecStorage)] pub struct Box { - pub colour: BoxColour, + pub color: BoxColor, } #[derive(Component)] #[storage(VecStorage)] pub struct BoxSpot { - pub colour: BoxColour, + pub color: BoxColor, } -#[derive(Component, Default)] -#[storage(NullStorage)] -pub struct Movable; - -#[derive(Component, Default)] -#[storage(NullStorage)] -pub struct Immovable; - pub fn register_components(world: &mut World) { world.register::(); world.register::(); diff --git a/code/rust-sokoban-c03-02/src/entities.rs b/code/rust-sokoban-c03-02/src/entities.rs index 0457c63..6f5641c 100644 --- a/code/rust-sokoban-c03-02/src/entities.rs +++ b/code/rust-sokoban-c03-02/src/entities.rs @@ -1,7 +1,6 @@ use crate::components::*; use specs::{Builder, World, WorldExt}; -// Create a wall entity pub fn create_wall(world: &mut World, position: Position) { world .create_entity() @@ -20,28 +19,28 @@ pub fn create_floor(world: &mut World, position: Position) { .build(); } -pub fn create_box(world: &mut World, position: Position, colour: BoxColour) { +pub fn create_box(world: &mut World, position: Position, color: BoxColor) { world .create_entity() .with(Position { z: 10, ..position }) .with(Renderable::new_animated(vec![ - format!("/images/box_{}_1.png", colour), - format!("/images/box_{}_2.png", colour), + format!("/images/box_{}_1.png", color), + format!("/images/box_{}_2.png", color), ])) - .with(Box { colour }) + .with(Box { color }) .with(Movable) .build(); } -pub fn create_box_spot(world: &mut World, position: Position, colour: BoxColour) { +pub fn create_box_spot(world: &mut World, position: Position, color: BoxColor) { world .create_entity() .with(Position { z: 9, ..position }) .with(Renderable::new_static(format!( "/images/box_spot_{}.png", - colour + color ))) - .with(BoxSpot { colour }) + .with(BoxSpot { color }) .build(); } diff --git a/code/rust-sokoban-c03-02/src/main.rs b/code/rust-sokoban-c03-02/src/main.rs index 6fda229..6257dff 100644 --- a/code/rust-sokoban-c03-02/src/main.rs +++ b/code/rust-sokoban-c03-02/src/main.rs @@ -1,8 +1,8 @@ // Rust sokoban // main.rs -use ggez::{conf, event::{self, KeyCode, KeyMods}, timer, Context, GameResult}; -use specs::{RunNow, World, WorldExt}; +use ggez::{conf, event, input::keyboard::KeyInput, Context, GameResult}; +use specs::{RunNow, World, WorldExt}; use std::path; mod components; @@ -38,7 +38,7 @@ impl event::EventHandler for Game { // Get and update time resource { let mut time = self.world.write_resource::