diff --git a/books/en_US/src/SUMMARY.md b/books/en_US/src/SUMMARY.md
index 6b6e818..3bf0c8f 100644
--- a/books/en_US/src/SUMMARY.md
+++ b/books/en_US/src/SUMMARY.md
@@ -1,19 +1,19 @@
# 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)
-
+ - [Coloured boxes](./c03-01-colours.md)
+ - [Animations](./c03-02-animations.md)
+ - [Events](./c03-03-events.md)
+ - [Sounds](./c03-04-sounds.md)
+ - [Batch rendering](./c03-05-batch-rendering.md)
diff --git a/books/en_US/src/c01-02-ecs.md b/books/en_US/src/c01-02-ecs.md
index 48df7f0..1d0d12f 100644
--- a/books/en_US/src/c01-02-ecs.md
+++ b/books/en_US/src/c01-02-ecs.md
@@ -3,25 +3,29 @@
In this section we will discuss Sokoban in more detail and how we will architect our game.
## Sokoban
+
Here is how a Sokoban game looks like, if you are not familiar with the gameplay already. We have walls and boxes, and the goal is for the player to move the boxes onto their spots by pushing them around.
![Sokoban play](./images/sokoban.gif)
## ECS
+
ECS (Entity Component System) is an architectural pattern for writing games which follows the composition over inheritance principle. We will be using ECS heavily in this project, much like most Rust games, so let's spend a bit of time familiarizing ourselves with the key concepts:
+
* **Components** - data-only structs which hold different characteristics of entities: some examples of components: Position, Renderable, Movement, etc. The key part here is that this is pure data, no behaviour.
* **Entities** - entities are made up of multiple components, for example a player might be made up by Position, Renderable & Movement, while the floor might just be Position & Renderable since it doesn't move. Entities are pretty much just dummy containers of one or more components with a unique identifier.
* **Systems** - systems use entities and components and contain behaviour and logic based on that data. For example, you could have a rendering system which just iterates through all entities which contain renderable components and draws all of them. The key here is that the components themselves don't contain any behaviour, instead they use a system to interpret the data and act.
If that doesn't make sense yet don't worry, we will discuss some practical examples in the next section applied to our Sokoban game.
-
## Sokoban architecture
+
Based on what we know now about how a Sokoban game should work, we will need a few different types of "things": walls, a player, floors, boxes and box spots. These will be our *entities*.
Now we have to identify what these entities will be made of, or what *components* we need. Well first of all we will need to keep track of where everything is on the map, so we need some sort of position component. Secondly, some (but not all) entities can move. The player can move around, but boxes can also move if they get pushed by the player. Finally, we need a way to render each entity, so we need some renderable component.
Here is how our first idea of entities and components looks like:
+
1. **Player entity**: Position, Renderable, Movable
1. **Wall entity**: Position, Renderable
1. **Floor entity**: Position, Renderable
@@ -30,8 +34,9 @@ Here is how our first idea of entities and components looks like:
At first thinking in ECS might be difficult, so don't worry if you don't understand everything yet or if this doesn't seem familiar with the way you've done things in other languages.
-## Specs
-Finally, let's bring in an ECS crate. There are a ton of them out there, but we will use [specs](https://specs.amethyst.rs/docs/tutorials/) for this book.
+## Hecs
+
+Finally, let's bring in an ECS crate. There are a ton of them out there, but we will use [hecs](https://crates.io/crates/hecs) for this book.
```
{{#include ../../../code/rust-sokoban-c01-03/Cargo.toml:9:11}}
@@ -39,4 +44,4 @@ Finally, let's bring in an ECS crate. There are a ton of them out there, but we
Next up, we'll start implementing entities and components!
-> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c01-03).
+> ***CODELINK:*** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c01-03).
diff --git a/books/en_US/src/c01-03-entities-components.md b/books/en_US/src/c01-03-entities-components.md
index 4b4ba4a..172b1d0 100644
--- a/books/en_US/src/c01-03-entities-components.md
+++ b/books/en_US/src/c01-03-entities-components.md
@@ -1,34 +1,29 @@
# Components and entities
+
In this section we will create our components, we'll see how to create entities and register everything to keep specs happy.
## Defining components
+
Let's start by defining components. We previously discussed Position, Renderable and Movement - we'll skip movement for now. We will also need some components to identify each entity - for example we will need a Wall component so we can identify an entity as a wall by the fact that it has a wall component.
This should hopefully be straight-forward, the position components stores the x, y and z coordinates which will tell us where something is on the map, and the renderable component will receive a string path pointing to an image which we can render. All other components are [marker components](https://specs.amethyst.rs/docs/tutorials/11_advanced_component.html?highlight=marker#marker-components), with no data (yet).
-
```rust
-{{#include ../../../code/rust-sokoban-c01-03/src/main.rs:13:42}}
+{{#include ../../../code/rust-sokoban-c01-03/src/main.rs:components}}
```
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.
> **_MORE:_** Read more about procedural macros [here](https://doc.rust-lang.org/book/ch19-06-macros.html).
-## Registering components
-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}}
-```
-
## Creating entities
+
An entity is simply a numeric identifier tied to a set of components. So the way we'll create entities is by simply specifying which components they contain.
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:entities}}
```
## Assets
@@ -57,10 +52,11 @@ Let's add the images to our project. We'll add a `resources` folder which will h
```
## World creation
+
Finally, let's tie everything together. We'll need to create a specs::World object, we'll add that to our Game struct and we will initialize it first thing in our main. Here is the full code, running now should render the same blank window, but we've made tremendous progress in actually setting up our game components and entities! Next up, we'll get to rendering so we'll finally see something on screen!
```rust
-{{#include ../../../code/rust-sokoban-c01-03/src/main.rs}}
+{{#include ../../../code/rust-sokoban-c01-03/src/main.rs:main}}
```
Note that running now will report some warnings in the console about unused import(s) and/or fields, don't worry about these just yet as we'll fix them in the coming chapters.
diff --git a/books/en_US/src/c01-04-rendering.md b/books/en_US/src/c01-04-rendering.md
index 859c06f..d49fa70 100644
--- a/books/en_US/src/c01-04-rendering.md
+++ b/books/en_US/src/c01-04-rendering.md
@@ -3,28 +3,19 @@
It's time for our first system, the rendering system. This system will be responsible for drawing all our entities on the screen.
## Rendering system setup
-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}}
-```
-
-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.
-
-> **_MORE:_** Read more about lifetimes [here](https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html).
-
-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.
+First we'll start with a blank implementation, something like this:
```rust
-{{#include ../../../code/rust-sokoban-c01-04/src/main.rs:51:57}}
- // implementation here
-{{#include ../../../code/rust-sokoban-c01-04/src/main.rs:83:84}}
+pub fn run_rendering(world: &World, context: &mut Context) {
+ // TODO add implementation
+}
```
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:handler}}
```
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.
@@ -38,13 +29,14 @@ Running the game now should compile, but it will probably not do anything yet, s
```
Here is the implementation of the rendering system. It does a few things:
+
* clear the screen (ensuring we don't keep any of the state rendered on the previous frame)
* get all entities with a renderable component and sort them by z (we do this in order to ensure we can render things on top of each other, for example the player should be above the floor, otherwise we wouldn't be able to see them)
* iterate through sorted entities and render each of them as an image
* 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:rendering_system}}
```
## Add some test entities
@@ -52,7 +44,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:init}}
```
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!
@@ -63,7 +55,6 @@ Final code below.
> **_NOTE:_** Note that this is a very basic implementation of rendering and as the number of entities grow the performance will not be good enough. A more advanced implementation of rendering which uses batch rendering can be found in [Chapter 3 - Batch Rendering](/c03-04-batch-rendering.html).
-
```rust
{{#include ../../../code/rust-sokoban-c01-04/src/main.rs}}
```
diff --git a/books/en_US/src/c02-01-map-loading.md b/books/en_US/src/c02-01-map-loading.md
index e831de6..8c61267 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:map}}
where:
. is an empty spot
@@ -17,16 +18,12 @@ S is a box spot
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}}
-```
+Eventually we can load from a file but for simplicity let's go with a constant in the code for now.
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:init}}
```
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.
diff --git a/books/en_US/src/c02-02-move-player.md b/books/en_US/src/c02-02-move-player.md
index 9cd3663..d6f8840 100644
--- a/books/en_US/src/c02-02-move-player.md
+++ b/books/en_US/src/c02-02-move-player.md
@@ -3,91 +3,65 @@
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:
+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 check if a given key is pressed using `is_key_pressed`.
+
+Let's start with a very basic implementation of the input system where we simply check if a key is pressed and print to the console.
```rust
-{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:1:11}}
+{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:input_system_print}}
```
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:155:162}}
-{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:166}}
-
- // ...
-
-{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:167}}
+{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:handler}}
```
If we run this we should see the print lines in the console.
-```
-Key pressed: Left
-Key pressed: Left
-Key pressed: Right
-Key pressed: Up
-Key pressed: Down
-Key pressed: Left
+```sh
+LEFT
+LEFT
+RIGHT
+UP
+DOWN
+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.
+## Input system
-## 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.
+Let's start by implementing the final input system now.
-```rust
-{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:48:52}}
-```
+We already have a way to check if a key was pressed, now we need to implement the logic that will move the player. The logic we are aiming to implement:
-And then we'll push the new key presses into the queue when `key_down_event` is called.
+* if UP is pressed, we move the player one position up on the y axis
+* if DOWN is pressed, we move the player one position down on the y axis
+* if LEFT is pressed, we move the player one position left on the x axis
+* if RIGHT is pressed, we move the player one position right on the x axis
```rust
-{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:134}}
-
- // ...
-
-{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:155:166}}
-
- // ...
-
-{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:167}}
+{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:input_system_duplicate}}
```
-Finally, we need to register the resources into specs like we did for components.
+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.
-```rust
-// Registering resources
-{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:179:181}}
+Pretty cool! Here's how it should look like. Notice we can go through walls and boxes. We'll fix that up in the next section when we add the movable component.
-// Registering resources in main
-{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:295:312}}
-```
+![Moving player](./images/input.gif)
-## Input system
+But you might have noticed there's a problem, a single key press triggers multiple moves. Let's fix that in the next section.
-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.
+## Handling multiple key presses
-```rust
-{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:94:121}}
-```
+The problem is we are calling the input system multiple times during a given second, which means holding a key pressed for one second will trigger multiple moves for the same key. As you might have noticed this is not a great experience as a player because you don't have good control over the movements and you can easily get into a situation where a box gets stuck next to a wall and there's no way to bring it back.
-Finally we need to run the system in our update loop.
+What options do we have to fix this? We could remember if the key was pressed the last frame, and if it was, we skip it. It would require storing state of the previous frame, and comparing against it in the current frame before deciding to move or not, which is very doable. Fortunately, ggez added this functionality to their keyboard api where you can call `is_key_just_pressed` and it will automatically check against the current state. Let's try this, it looks something like this.
```rust
-{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:135:143}}
+{{#include ../../../code/rust-sokoban-c02-02/src/main.rs:input_system}}
```
-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.
-
-Pretty cool! Here's how it should look like. Notice we can go through walls and boxes. We'll fix that up in the next section when we add the movable component.
-
-![Moving player](./images/input.gif)
+And now everything works as expected!
-> **_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/books/en_US/src/c02-03-push-box.md b/books/en_US/src/c02-03-push-box.md
index 75ed9ac..9398930 100644
--- a/books/en_US/src/c02-03-push-box.md
+++ b/books/en_US/src/c02-03-push-box.md
@@ -3,33 +3,31 @@
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
+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.
```rust
-{{#include ../../../code/rust-sokoban-c02-03/src/main.rs:55:62}}
-
-{{#include ../../../code/rust-sokoban-c02-03/src/main.rs:250:259}}
+{{#include ../../../code/rust-sokoban-c02-03/src/main.rs:components_movement}}
```
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)
```rust
-{{#include ../../../code/rust-sokoban-c02-03/src/main.rs:266:321}}
+{{#include ../../../code/rust-sokoban-c02-03/src/main.rs:entities}}
```
## 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,12 +36,14 @@ 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
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
@@ -55,7 +55,7 @@ So given this, let's start implementing this logic. Let's think about the logica
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:input_system}}
```
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 +68,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/books/en_US/src/c02-04-modules.md b/books/en_US/src/c02-04-modules.md
index 41636a0..ac1b54e 100644
--- a/books/en_US/src/c02-04-modules.md
+++ b/books/en_US/src/c02-04-modules.md
@@ -14,9 +14,9 @@ For now, let's aim for this folder structure. Eventually as we get more componen
│ └── wall.png
├── src
│ ├── systems
-│ │ ├── input_system.rs
-│ │ ├── mod.rs
-│ │ └── rendering_system.rs
+│ │ ├── input.rs
+│ │ ├── rendering.rs
+│ │ └── mod.rs
│ ├── components.rs
│ ├── constants.rs
│ ├── entities.rs
@@ -35,13 +35,6 @@ Let's start by moving all the components into a file. There should be no changes
{{#include ../../../code/rust-sokoban-c02-04/src/components.rs:}}
```
-Now for the resources.
-
-```rust
-// resources.rs
-{{#include ../../../code/rust-sokoban-c02-04/src/resources.rs:}}
-```
-
Next up, let's move the constants into their own file. For now we are hardcoding the map dimensions, we need them for the movement to know when we've reached the edge of the map, but as an improvement would could later store the dimensions of the map and make them dynamic based on the map loading.
```rust
@@ -63,10 +56,9 @@ Now for the map loading.
{{#include ../../../code/rust-sokoban-c02-04/src/map.rs}}
```
-Finally, we'll move the systems code into their own files (RenderingSystem to rendering_system.rs and InputSystem to input_system.rs). It should just be a copy paste from main with some import removals, so go ahead and do that.
-
-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.
+Finally, we'll move the systems code into their own files (RenderingSystem to `rendering.rs` and InputSystem to `input.rs`). It should just be a copy paste from main with some import removals, so go ahead and do that.
+We have to update the `mod.rs` to tell Rust we want to export all the systems to the outside world (in this case the main module).
```rust
// systems/mod.rs
@@ -83,5 +75,3 @@ Awesome, now that we've done that here is how our simplified main file looks lik
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).
-
-
diff --git a/books/en_US/src/c02-05-gameplay.md b/books/en_US/src/c02-05-gameplay.md
index 6da5031..f672e22 100644
--- a/books/en_US/src/c02-05-gameplay.md
+++ b/books/en_US/src/c02-05-gameplay.md
@@ -9,8 +9,8 @@ Let's think about what we'll need to add to this game to check for the success c
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
@@ -22,17 +22,12 @@ not associated with a specific entity. Let's start by defining a `Gameplay` reso
```rust
// resources.rs
-{{#include ../../../code/rust-sokoban-c02-05/src/resources.rs:38:43}}
+{{#include ../../../code/rust-sokoban-c02-05/src/components.rs:gameplay_state}}
```
`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:
-
-```rust
-// resources.rs
-{{#include ../../../code/rust-sokoban-c02-05/src/resources.rs:17:20}}
-```
+the number of moves made. `state` is described by an `enum`.
The eagle-eyed reader will note that we used a macro to derive the `Default` trait
for `Gameplay`, but not for the `GameplayState` enum. If we want to use `Gameplay`
@@ -43,14 +38,7 @@ automatically, we must implement `Default` for `Gameplay` ourselves.
```rust
// resources.rs
-{{#include ../../../code/rust-sokoban-c02-05/src/resources.rs:32:36}}
-```
-
-Having defined the resource, let's register it with the world:
-
-```rust
-// resources.rs
-{{#include ../../../code/rust-sokoban-c02-05/src/resources.rs:12:15}}
+{{#include ../../../code/rust-sokoban-c02-05/src/components.rs:gameplay_state_impl_default}}
```
Now, when the game is started, the `Gameplay` resource will look like this:
@@ -65,15 +53,11 @@ Gameplay {
## Step Counter System
We can increment `Gameplay`'s `moves_count` field to track the number of turns taken.
-We already have a system dealing with user input in `InputSystem`, so let's adapt that for this purpose.
-
-Since we need to mutate the `Gameplay` resource, we need to register it with
-`InputSystem` by adding `Write<'a, Gameplay>` to the `SystemData` type
-definition.
+We already have a system dealing with user input in the input system, so let's adapt that for this purpose.
```rust
// input_system.rs
-{{#include ../../../code/rust-sokoban-c02-05/src/systems/input_system.rs:0:25}}
+{{#include ../../../code/rust-sokoban-c02-05/src/systems/input_system.rs}}
...
```
@@ -81,52 +65,37 @@ Since we've already done the work to check if a player character will move in
response to a keypress, we can use that to determine when to increment the step
counter.
-```rust
-// input_system.rs
- ...
-{{#include ../../../code/rust-sokoban-c02-05/src/systems/input_system.rs:83:105}}
-```
-
## Gameplay System
-Next, let's integrate this resource with a new `GamePlayStateSystem`. This
+Next, let's integrate this resource with a new gameplay state system. 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!
Aside from `Gameplay`, this system only needs read-only access to the
-`Position`, `Box`, and `BoxSpot` storages.
+`Position`, `Box`, and `BoxSpot` components.
-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
-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.
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.
```rust
// gameplay_state_system.rs
-{{#include ../../../code/rust-sokoban-c02-05/src/systems/gameplay_state_system.rs::}}
+{{#include ../../../code/rust-sokoban-c02-05/src/systems/gameplay.rs}}
```
Finally, let's run the gameplay system in our main update loop.
```rust
// main.rs
-{{#include ../../../code/rust-sokoban-c02-05/src/main.rs:24:39}}
- // ...
-{{#include ../../../code/rust-sokoban-c02-05/src/main.rs:63}}
+{{#include ../../../code/rust-sokoban-c02-05/src/main.rs}}
```
-
## 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
system to update the state. We can adapt the `GameplayState` resource and
-`RenderingSystem` for this.
+rendering system for this.
First, we'll implement `Display` for `GameplayState` so we can render the
state of the game as text. We'll use a match expression to allow `GameplayState`
@@ -134,27 +103,27 @@ 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/components.rs:gameplay_state_impl_display}}
```
-Next, we'll add a `draw_text` method to `RenderingSystem`, so it can print
+Next, we'll add a `draw_text` method to rendering system, so it can print
`GameplayState` to the screen...
```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:draw_text}}
```
...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`
-resource.
+call `draw_text`, and use it all to render the state and number of moves.
```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:draw_gameplay_state}}
```
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 +131,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/books/en_US/src/c03-01-colours.md b/books/en_US/src/c03-01-colours.md
index f975052..5e06749 100644
--- a/books/en_US/src/c03-01-colours.md
+++ b/books/en_US/src/c03-01-colours.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.
## 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,78 +40,83 @@ 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).
```rust
// components.rs
-{{#include ../../../code/rust-sokoban-c03-01/src/components.rs:29:32}}
+{{#include ../../../code/rust-sokoban-c03-01/src/components.rs:box_colour}}
```
-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:box}}
```
## 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.
+
+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.
```rust
// components.rs
-{{#include ../../../code/rust-sokoban-c03-01/src/components.rs:34:43}}
+{{#include ../../../code/rust-sokoban-c03-01/src/components.rs:box_colour_display}}
```
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.
```rust
// entities.rs
-{{#include ../../../code/rust-sokoban-c03-01/src/entities.rs:27:48}}
+{{#include ../../../code/rust-sokoban-c03-01/src/entities.rs:create_box}}
```
## 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
+* "BS" for blue spot
* "RS" for red spot
```rust
// map.rs
-{{#include ../../../code/rust-sokoban-c03-01/src/map.rs}}
+{{#include ../../../code/rust-sokoban-c03-01/src/map.rs:map_match}}
```
-And let's update our static map in the main.
+And let's update our static map when initialising the level.
```rust
-// main.rs
-{{#include ../../../code/rust-sokoban-c03-01/src/main.rs:65:80}}
+// map.rs
+{{#include ../../../code/rust-sokoban-c03-01/src/map.rs:initialize_level}}
```
## 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.
```rust
-// gameplay_state_system.rs
-{{#include ../../../code/rust-sokoban-c03-01/src/systems/gameplay_state_system.rs:20:52}}
+// gameplay.rs
+{{#include ../../../code/rust-sokoban-c03-01/src/systems/gameplay.rs}}
```
Now if you compile the code at this point it should complain about the fact that we are trying to compare two enums with `==`. Rust doesn't know by default how to handle this, so we must tell it. The best way we can do that is add an implementation for the `PartialEq` trait.
```rust
// components.rs
-{{#include ../../../code/rust-sokoban-c03-01/src/components.rs:28:32}}
+{{#include ../../../code/rust-sokoban-c03-01/src/components.rs:box_colour_eq}}
```
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.
-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,7 +125,7 @@ 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 `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.
> **_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).
@@ -126,4 +133,4 @@ Now we can compile the code the reap the rewards of our efforts by seeing the ga
![Sokoban play](./images/colours.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/c03-02-animations.md b/books/en_US/src/c03-02-animations.md
index d2ec577..f93627d 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:
-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,7 +52,8 @@ 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.
@@ -53,53 +61,56 @@ Let's also add two new functions to construct the two types of renderables, eith
```rust
// components.rs
-{{#include ../../../code/rust-sokoban-c03-02/src/components.rs:19:32}}
-{{#include ../../../code/rust-sokoban-c03-02/src/components.rs:48}}
+{{#include ../../../code/rust-sokoban-c03-02/src/components.rs:renderable}}
```
Next, we need a way of telling if a renderable is animated or static, which we will use in the rendering system. We could leave the paths member variable public and allow the rendering system to get the length of the paths and infer based on the length, but there is a more idiomatic way. We can add an enum for the kind of renderable, and add a method on the renderable to get that kind, in this way we encapsulate the logic of the kind within the renderable, and we can keep paths private. You can put the kind declaration anywhere in the components.rs, but ideally next to the renderable declaration.
```rust
// components.rs
-{{#include ../../../code/rust-sokoban-c03-02/src/components.rs:14:18}}
+{{#include ../../../code/rust-sokoban-c03-02/src/components.rs:renderable_kind}}
```
Now let's add a function to tell us the kind of a renderable based on the internal paths.
```rust
// components.rs
-{{#include ../../../code/rust-sokoban-c03-02/src/components.rs:25:40}}
-{{#include ../../../code/rust-sokoban-c03-02/src/components.rs:48}}
+{{#include ../../../code/rust-sokoban-c03-02/src/components.rs:renderable_kind_fn}}
```
-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
-{{#include ../../../code/rust-sokoban-c03-02/src/components.rs:25}}
+{{#include ../../../code/rust-sokoban-c03-02/src/components.rs:renderable_path_fn}}
+```
- //...
+Finally, we add a way to construct renderables based on one or more paths.
-{{#include ../../../code/rust-sokoban-c03-02/src/components.rs:42:48}}
+```rust
+// components.rs
+{{#include ../../../code/rust-sokoban-c03-02/src/components.rs:renderable_new_fn}}
```
## 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:create_box}}
```
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}}
```
## 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.
@@ -108,55 +119,37 @@ Because of this we need to keep track of the delta time - or how much time passe
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}}
-```
-
-And don't forget to register the new resource.
-
-```rust
-// resources.rs
-{{#include ../../../code/rust-sokoban-c03-02/src/resources.rs:12:16}}
+// components.rs
+{{#include ../../../code/rust-sokoban-c03-02/src/components.rs:create_time}}
```
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.
```rust
// main.rs
-{{#include ../../../code/rust-sokoban-c03-02/src/main.rs:24:45}}
+{{#include ../../../code/rust-sokoban-c03-02/src/main.rs:update}}
```
-
## 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:34:54}}
+{{#include ../../../code/rust-sokoban-c03-02/src/systems/rendering.rs:get_image}}
```
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:88}}
-
- //...
-
-{{#include ../../../code/rust-sokoban-c03-02/src/systems/rendering_system.rs:97}}
-{{#include ../../../code/rust-sokoban-c03-02/src/systems/rendering_system.rs:98}}
-
+// rendering.rs
+{{#include ../../../code/rust-sokoban-c03-02/src/systems/rendering.rs:run_rendering}}
```
## 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 +158,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).
-
-
-
-
-
-
-
-
diff --git a/books/en_US/src/c03-03-sounds-events.md b/books/en_US/src/c03-03-events.md
similarity index 67%
rename from books/en_US/src/c03-03-sounds-events.md
rename to books/en_US/src/c03-03-events.md
index 543bff4..b00193b 100644
--- a/books/en_US/src/c03-03-sounds-events.md
+++ b/books/en_US/src/c03-03-events.md
@@ -1,24 +1,29 @@
# Sounds and events
-In this section we will work on adding sound effects. In short, we want to play sounds in these circumstances:
+In this section we will work on adding events which will be later used for adding sound effects. In short, we want to play sounds in these circumstances:
+
1. when the player hits a wall or an obstacle - to let them know they cannot get through
1. when the player places a box on the correct spot - as an indication of "you've done it correctly"
1. when the player places a box on the incorrect spot - as an indication that the move was wrong
-Actually playing audio will not be too difficult as ggez provides this ability, but the biggest issue we have right now is that we need to determine *when* to play the sounds.
+Actually playing audio will not be too difficult as ggez provides this ability, but the biggest issue we have right now is that we need to determine *when* to play the sounds.
Let's take the box on correct spot example. We could probably use our game state system and loop through the boxes and the spots and determine when we are in this state and play the sound. But that is not going to work because we will be interating many times per second, and we will always be in this state as long as the box doesn't move, so we will attempt to play many times per second, which is not what we want. We could try to keep some state to know what we are currently playing, but that doesn't feel right. The problem is we cannot do this by iteratively checking state, we instead need a reactive model where we can be told something has just happenned, and we need to take an action. What I've described here is an event model, we need to fire an event when a box gets placed on a spot, and then when we receive this event on the other end we need to play the sound. The really good thing about this is that we will be able to re-use this event system for many other purposes.
## Events infrastructure: How
+
Let's start by discussing how we will implement events. We will not use components or entities (although we could), instead we will use a resource very similar to the input queue. The parts of the code that need to enqueue events will need to get access to this resource, and we will then have a system which processes these events and take the appropriate actions.
## What events
+
Let's discuss in more detail what events we will need:
+
1. player hit obstacle - this can be an event in itself which we fire from the input system when we try to move but can't
1. box placed on correct/incorrect spot - we can model this as a single event with a property inside it that tells us if the box/spot combination is correct - thinking a bit deeper about how we can achieve this, we can have an entity moved event, and when we receive that event we can check the entity id of that entity that just moved to see if it's a box and if it's on the right/wrong/any spot (this is an example of creating an event chain - an event from an event)
## Events types
-Now let's go into the implementation of events. We'll use an enum to define various event types. Now, we've used enums before (for the rendering type and the box colours) but this time we will take full advantage of the power of Rust enums. One of the most interesting things about them is that we actually attach properties to each enum variant.
+
+Now let's go into the implementation of events. We'll use an enum to define various event types. Now, we've used enums before (for the rendering type and the box colours) but this time we will take full advantage of the power of Rust enums. One of the most interesting things about them is that we actually attach properties to each enum variant.
Let's look at our events enum.
@@ -35,7 +40,8 @@ Note the second `EntityMoved` and the second `BoxPlacedOnSpot`. Those are actual
```
## Event queue resource
-Now let's add a resource for the event queue. We will have various systems writing to this queue and one system (the event system) consuming this queue. It's basically a multiple producer single consumer model.
+
+Now let's add a resource for the event queue. We will have various systems writing to this queue and one system (the event system) consuming this queue. It's basically a multiple producer single consumer model.
```rust
// resources.rs
@@ -51,6 +57,7 @@ And as always let's register this resource.
```
## Sending events
+
Now that we have a way to enqueue events, let's add the two events we need in the input_system: EntityMoved and PlayerHitObstacle.
```rust
@@ -61,14 +68,16 @@ Now that we have a way to enqueue events, let's add the two events we need in th
{{#include ../../../code/rust-sokoban-c03-03/src/systems/input_system.rs:83:124}}
```
-I've omitted some of the code in the original file for readability, but we are really just adding two lines in the right places.
+I've omitted some of the code in the original file for readability, but we are really just adding two lines in the right places.
## Consuming events - event system
+
Now it's time to add a way to consume the events, which will be the events system. This system will contain the logic for what should happen when a specific event is received.
Let discuss how we will handle each event:
+
* Event::PlayerHitObstacle -> this is where the sound playing will go, but we'll come back to this when we add the audio bits
-* Event::EntityMoved(EntityMoved { id }) -> this is where we will add the logic for checking if the entity that just moved is a box and whether it's on a spot or not
+* Event::EntityMoved(EntityMoved { id }) -> this is where we will add the logic for checking if the entity that just moved is a box and whether it's on a spot or not
* Event::BoxPlacedOnSpot(BoxPlacedOnSpot { is_correct_spot }) -> this is where the sound playing will go, but we'll come back to this when we add the audio bits
```rust
@@ -79,85 +88,4 @@ Let discuss how we will handle each event:
```
-## Audio assets
-Now that we have the event bits in place, let's add audio assets. I've selected 3 sounds from this [asset pack](https://opengameart.org/content/512-sound-effects-8-bit-style), but feel free to select your own.
-
-Correct sound [here](./sounds/correct.wav)
-
-Incorrect sound [here](./sounds/incorrect.wav)
-
-Wall sound [here](./sounds/wall.wav)
-
-Let's add these sounds to a new folder under resources.
-
-```
-.
-├── resources
-│ ├── images
-│ │ ├── box_blue_1.png
-│ │ ├── box_blue_2.png
-│ │ ├── box_red_1.png
-│ │ ├── box_red_2.png
-│ │ ├── box_spot_blue.png
-│ │ ├── box_spot_red.png
-│ │ ├── floor.png
-│ │ ├── player_1.png
-│ │ ├── player_2.png
-│ │ ├── player_3.png
-│ │ └── wall.png
-│ └── sounds
-│ ├── correct.wav
-│ ├── incorrect.wav
-│ └── wall.wav
-├── Cargo.lock
-└── Cargo.toml
-```
-
-## Audio store
-Now in order to play the sound the wav files need to be loaded. To avoid loading them on the fly every time before we play the sound we'll create an audio store and load them up at the beginning of the game.
-
-We'll use a resource for the audio store.
-
-```rust
-// audio.rs
-{{#include ../../../code/rust-sokoban-c03-03/src/audio.rs:6:9}}
-```
-
-And as always let's register this resource.
-
-```rust
-// resources.rs
-{{#include ../../../code/rust-sokoban-c03-03/src/resources.rs:14:20}}
-```
-
-And let's add the code for initializing the store.
-
-```rust
-// audio.rs
-{{#include ../../../code/rust-sokoban-c03-03/src/audio.rs:21:32}}
-```
-
-## Playing audio
-Finally, let's add the ability to play the sound in the store.
-
-```rust
-// audio.rs
-{{#include ../../../code/rust-sokoban-c03-03/src/audio.rs:11:19}}
-```
-
-And now let's play in the event system.
-
-```rust
- // event_system.rs
-{{#include ../../../code/rust-sokoban-c03-03/src/systems/event_system.rs:24:37}}
- // ...
-{{#include ../../../code/rust-sokoban-c03-03/src/systems/event_system.rs:61:73}}
-```
-
-Now let's run the game and enjoy those sound effects!
-
-
-
-> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c03-03).
+> ***CODELINK:*** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c03-03).
diff --git a/books/en_US/src/c03-04-sounds.md b/books/en_US/src/c03-04-sounds.md
new file mode 100644
index 0000000..668001c
--- /dev/null
+++ b/books/en_US/src/c03-04-sounds.md
@@ -0,0 +1,58 @@
+# Sound effects
+
+In this section we will work on adding sound effects. In short, we want to play sounds in these circumstances:
+
+1. when the player hits a wall or an obstacle - to let them know they cannot get through
+1. when the player places a box on the correct spot - as an indication of "you've done it correctly"
+1. when the player places a box on the incorrect spot - as an indication that the move was wrong
+
+## Audio store
+
+Now in order to play the sound the wav files need to be loaded. To avoid loading them on the fly every time before we play the sound we'll create an audio store and load them up at the beginning of the game.
+
+We'll use a resource for the audio store.
+
+```rust
+// audio.rs
+{{#include ../../../code/rust-sokoban-c03-03/src/audio.rs:6:9}}
+```
+
+And as always let's register this resource.
+
+```rust
+// resources.rs
+{{#include ../../../code/rust-sokoban-c03-03/src/resources.rs:14:20}}
+```
+
+And let's add the code for initializing the store.
+
+```rust
+// audio.rs
+{{#include ../../../code/rust-sokoban-c03-03/src/audio.rs:21:32}}
+```
+
+## Playing audio
+
+Finally, let's add the ability to play the sound in the store.
+
+```rust
+// audio.rs
+{{#include ../../../code/rust-sokoban-c03-03/src/audio.rs:11:19}}
+```
+
+And now let's play in the event system.
+
+```rust
+ // event_system.rs
+{{#include ../../../code/rust-sokoban-c03-03/src/systems/event_system.rs:24:37}}
+ // ...
+{{#include ../../../code/rust-sokoban-c03-03/src/systems/event_system.rs:61:73}}
+```
+
+Now let's run the game and enjoy those sound effects!
+
+
+
+> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c03-04).
diff --git a/books/en_US/src/c03-04-batch-rendering.md b/books/en_US/src/c03-05-batch-rendering.md
similarity index 96%
rename from books/en_US/src/c03-04-batch-rendering.md
rename to books/en_US/src/c03-05-batch-rendering.md
index e06408c..216df82 100644
--- a/books/en_US/src/c03-04-batch-rendering.md
+++ b/books/en_US/src/c03-05-batch-rendering.md
@@ -3,7 +3,9 @@
You might have noticed while playing the game that the input feels a bit slow. Let's add an FPS counter to see how fast we are rendering. If you are not familiar with the term FPS, it stands for Frames Per Second, and we are basically aiming for 60FPS.
## FPS counter
+
Let's start by adding an FPS counter, there are two parts to this:
+
1. getting or calculating the FPS value
1. rendering the value on the screen
@@ -20,19 +22,22 @@ For 1 luckily ggez provides a way to get the fps - see [here](https://docs.rs/gg
{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:123}}
```
-Run the game and move around with the keys a bit and you will see the FPS drops quite significantly from the expected 60. For me it looks to be in the range of 20-30 but depending on your machine it might be more or less.
+Run the game and move around with the keys a bit and you will see the FPS drops quite significantly from the expected 60. For me it looks to be in the range of 20-30 but depending on your machine it might be more or less.
![low fps](./images/low_fps.png)
## What is causing the FPS drop?
-Now you might be asking yourself, what have we done to make this so low? We have a fairly simple game and our logic for input and movement is not actually that complex, we also don't have that many entities or components to warrant such a big FPS drop. Well, to understand this we need to go a bit deeper into how our current rendering system works.
-Currently, for every renderable entity, we figure out which image to render and we render it. This means that if we have 20 floor tiles we will load the floor image 20 times and issue 20 separate rendering calls. This is too expensive and it's the cause for our massive FPS drop.
+Now you might be asking yourself, what have we done to make this so low? We have a fairly simple game and our logic for input and movement is not actually that complex, we also don't have that many entities or components to warrant such a big FPS drop. Well, to understand this we need to go a bit deeper into how our current rendering system works.
-How can we fix this? Well, we can use a technique called batch rendering. With this technique, what we have to do is only load the image once, and tell ggez to render it in all the 20 positions where it needs to be rendered. This way we not only load the image once, but we also only call render once per image, which will speed things up significantly. As a side note, some engines will do this render batching under the hood for you, but ggez doesn't, hence why we need to care.
+Currently, for every renderable entity, we figure out which image to render and we render it. This means that if we have 20 floor tiles we will load the floor image 20 times and issue 20 separate rendering calls. This is too expensive and it's the cause for our massive FPS drop.
+
+How can we fix this? Well, we can use a technique called batch rendering. With this technique, what we have to do is only load the image once, and tell ggez to render it in all the 20 positions where it needs to be rendered. This way we not only load the image once, but we also only call render once per image, which will speed things up significantly. As a side note, some engines will do this render batching under the hood for you, but ggez doesn't, hence why we need to care.
## Batch rendering
+
Here is what we'll have to do to implement batch rendering:
+
* for every renderable entity, figure out which image we need to render and at which DrawParams (this is what we currently give ggez as an indication of where to render)
* save all the (image, DrawParams) into a convenient format
* iterate through (image, DrawParams) on a per images basis sorted by z and make a single render call per image
@@ -59,6 +64,7 @@ Now, remember that get_image function we wrote in the Animations chapter to figu
```
Now let's figure out the format we want our batched data to be in. We will use a `HashMap>>` where:
+
* the first key (`u8`) is the z position - remember we need to respect the z positions and draw from highest to smallest z to ensure the right order (for example floors should be below player, etc)
* the value is another `HashMap`, where the second key (`String`) is the path to the image
* finally, the last value is a `Vec` which are all the params at which we must render that particular image
@@ -76,7 +82,7 @@ Let's now write the code to populate the rendering_batches hash map.
{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:123}}
```
-Finally, let's actually render the batches. We will not be able to use the draw(image) function we used before but luckily ggez has a batching API - [SpriteBatch](https://docs.rs/ggez/0.7.0/ggez/graphics/spritebatch/struct.SpriteBatch.html). Also note the `sorted_by` here, that is provided to us to itertools.
+Finally, let's actually render the batches. We will not be able to use the draw(image) function we used before but luckily ggez has a batching API - [SpriteBatch](https://docs.rs/ggez/0.7.0/ggez/graphics/spritebatch/struct.SpriteBatch.html). Also note the `sorted_by` here, that is provided to us to itertools.
```rust
// rendering_system.rs
@@ -93,6 +99,4 @@ And that's it! Run the game again and you should see a shiny 60FPS and everythin
![low fps](./images/high_fps.png)
-> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c03-04).
-
-
+> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c03-05).
diff --git a/code/.DS_Store b/code/.DS_Store
index 61bb58d..8147e4a 100644
Binary files a/code/.DS_Store and b/code/.DS_Store differ
diff --git a/code/rust-sokoban-c01-01/Cargo.toml b/code/rust-sokoban-c01-01/Cargo.toml
index 2ceb2e6..7ae47cd 100644
--- a/code/rust-sokoban-c01-01/Cargo.toml
+++ b/code/rust-sokoban-c01-01/Cargo.toml
@@ -7,4 +7,4 @@ edition = "2018"
# 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.9.3"
\ No newline at end of file
diff --git a/code/rust-sokoban-c01-03/Cargo.toml b/code/rust-sokoban-c01-03/Cargo.toml
index 28f9106..ece8abf 100644
--- a/code/rust-sokoban-c01-03/Cargo.toml
+++ b/code/rust-sokoban-c01-03/Cargo.toml
@@ -7,5 +7,5 @@ edition = "2018"
# 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.9.3"
+hecs = "0.10.5"
\ No newline at end of file
diff --git a/code/rust-sokoban-c01-03/src/main.rs b/code/rust-sokoban-c01-03/src/main.rs
index 68c1530..c1a3767 100644
--- a/code/rust-sokoban-c01-03/src/main.rs
+++ b/code/rust-sokoban-c01-03/src/main.rs
@@ -1,52 +1,43 @@
+/* ANCHOR: all */
// Rust sokoban
// main.rs
-
-
use ggez::{conf, event, Context, GameResult};
-use specs::{Builder, Component, VecStorage, World, WorldExt};
-
+use hecs::{Entity, World};
use std::path;
-// Components
-#[derive(Debug, Component, Clone, Copy)]
-#[storage(VecStorage)]
+// ANCHOR: components
pub struct Position {
x: u8,
y: u8,
z: u8,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Renderable {
path: String,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Wall {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Player {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Box {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct BoxSpot {}
+// ANCHOR_END: components
+
+// ANCHOR: game
// This struct will hold all our game state
// For now there is nothing to be held, but we'll add
// things shortly.
struct Game {
world: World,
}
+// ANCHOR_END: game
+// ANCHOR: handler
impl event::EventHandler for Game {
fn update(&mut self, _context: &mut Context) -> GameResult {
Ok(())
@@ -56,75 +47,61 @@ impl event::EventHandler for Game {
Ok(())
}
}
+// ANCHOR_END: handler
-// Register components with the world
-pub fn register_components(world: &mut World) {
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
-}
-
-// Create a wall entity
-pub fn create_wall(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
+// ANCHOR: entities
+pub fn create_wall(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
path: "/images/wall.png".to_string(),
- })
- .with(Wall {})
- .build();
+ },
+ Wall {},
+ ))
}
-
-pub fn create_floor(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 5, ..position })
- .with(Renderable {
+pub fn create_floor(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 5, ..position },
+ Renderable {
path: "/images/floor.png".to_string(),
- })
- .build();
+ },
+ ))
}
-pub fn create_box(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
+pub fn create_box(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
path: "/images/box.png".to_string(),
- })
- .with(Box {})
- .build();
+ },
+ Box {},
+ ))
}
-pub fn create_box_spot(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 9, ..position })
- .with(Renderable {
+pub fn create_box_spot(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 9, ..position },
+ Renderable {
path: "/images/box_spot.png".to_string(),
- })
- .with(BoxSpot {})
- .build();
+ },
+ BoxSpot {},
+ ))
}
-pub fn create_player(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
+pub fn create_player(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
path: "/images/player.png".to_string(),
- })
- .with(Player {})
- .build();
+ },
+ Player {},
+ ))
}
+// ANCHOR_END: entities
+// ANCHOR: main
pub fn main() -> GameResult {
let mut world = World::new();
- register_components(&mut world);
// Create a game context and event loop
let context_builder = ggez::ContextBuilder::new("rust_sokoban", "sokoban")
@@ -139,3 +116,6 @@ pub fn main() -> GameResult {
// Run the main event loop
event::run(context, event_loop, game)
}
+// ANCHOR_END: main
+
+/* ANCHOR_END: all */
diff --git a/code/rust-sokoban-c01-04/Cargo.toml b/code/rust-sokoban-c01-04/Cargo.toml
index 92e5cac..d39b812 100644
--- a/code/rust-sokoban-c01-04/Cargo.toml
+++ b/code/rust-sokoban-c01-04/Cargo.toml
@@ -7,6 +7,6 @@ edition = "2018"
# 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.9.3"
+glam = { version = "0.24", features = ["mint"] }
+hecs = "0.10.5"
\ No newline at end of file
diff --git a/code/rust-sokoban-c01-04/src/main.rs b/code/rust-sokoban-c01-04/src/main.rs
index 7f82725..6a6856b 100644
--- a/code/rust-sokoban-c01-04/src/main.rs
+++ b/code/rust-sokoban-c01-04/src/main.rs
@@ -1,99 +1,80 @@
+/* ANCHOR: all */
// Rust sokoban
// main.rs
-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 ggez::{
+ conf, event,
+ graphics::{self, DrawParam, Image},
+ Context, GameResult,
};
+use glam::Vec2;
+use hecs::{Entity, World};
use std::path;
const TILE_WIDTH: f32 = 32.0;
-// Components
-#[derive(Debug, Component, Clone, Copy)]
-#[storage(VecStorage)]
+// ANCHOR: components
pub struct Position {
x: u8,
y: u8,
z: u8,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Renderable {
path: String,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Wall {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Player {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Box {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct BoxSpot {}
-// Systems
-pub struct RenderingSystem<'a> {
- context: &'a mut Context,
-}
-
-// System implementation
-impl<'a> System<'a> for RenderingSystem<'a> {
- // Data
- type SystemData = (ReadStorage<'a, Position>, ReadStorage<'a, Renderable>);
-
- fn run(&mut self, data: Self::SystemData) {
- 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));
-
- // Get all the renderables with their positions and sort by the position z
- // This will allow us to have entities layered visually.
- let mut rendering_data = (&positions, &renderables).join().collect::>();
- rendering_data.sort_by_key(|&k| k.0.z);
-
- // Iterate through all pairs of positions & renderables, load the image
- // 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;
-
- // draw
- let draw_params = DrawParam::new().dest(Vec2::new(x, y));
- graphics::draw(self.context, &image, draw_params).expect("expected render");
- }
-
- // Finally, present the context, this will actually display everything
- // on the screen.
- graphics::present(self.context).expect("expected to present");
- }
-}
+// ANCHOR_END: components
+// ANCHOR: game
// This struct will hold all our game state
// For now there is nothing to be held, but we'll add
// things shortly.
struct Game {
world: World,
}
+// ANCHOR_END: game
+
+// ANCHOR: init
+// Initialize the level
+pub fn initialize_level(world: &mut World) {
+ create_player(
+ world,
+ Position {
+ x: 0,
+ y: 0,
+ z: 0, // we will get the z from the factory functions
+ },
+ );
+ create_wall(
+ world,
+ Position {
+ x: 1,
+ y: 0,
+ z: 0, // we will get the z from the factory functions
+ },
+ );
+ create_box(
+ world,
+ Position {
+ x: 2,
+ y: 0,
+ z: 0, // we will get the z from the factory functions
+ },
+ );
+}
+// ANCHOR_END: init
-// This is the main event loop. ggez tells us to implement
-// two things:
-// - updating
-// - rendering
+// ANCHOR: handler
impl event::EventHandler for Game {
fn update(&mut self, _context: &mut Context) -> GameResult {
Ok(())
@@ -102,111 +83,98 @@ impl event::EventHandler for Game {
fn draw(&mut self, context: &mut Context) -> GameResult {
// Render game entities
{
- let mut rs = RenderingSystem { context };
- rs.run_now(&self.world);
+ run_rendering(&self.world, context);
}
Ok(())
}
}
+// ANCHOR_END: handler
-// Register components with the world
-pub fn register_components(world: &mut World) {
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
-}
-
-// Create a wall entity
-pub fn create_wall(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
+// ANCHOR: entities
+pub fn create_wall(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
path: "/images/wall.png".to_string(),
- })
- .with(Wall {})
- .build();
+ },
+ Wall {},
+ ))
}
-
-pub fn create_floor(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 5, ..position })
- .with(Renderable {
+pub fn create_floor(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 5, ..position },
+ Renderable {
path: "/images/floor.png".to_string(),
- })
- .build();
+ },
+ ))
}
-pub fn create_box(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
+pub fn create_box(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
path: "/images/box.png".to_string(),
- })
- .with(Box {})
- .build();
+ },
+ Box {},
+ ))
}
-pub fn create_box_spot(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 9, ..position })
- .with(Renderable {
+pub fn create_box_spot(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 9, ..position },
+ Renderable {
path: "/images/box_spot.png".to_string(),
- })
- .with(BoxSpot {})
- .build();
+ },
+ BoxSpot {},
+ ))
}
-pub fn create_player(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
+pub fn create_player(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
path: "/images/player.png".to_string(),
- })
- .with(Player {})
- .build();
+ },
+ Player {},
+ ))
}
+// ANCHOR_END: entities
+
+// ANCHOR: rendering_system
+fn run_rendering(world: &World, context: &mut Context) {
+ // Clearing the screen (this gives us the background colour)
+ let mut canvas =
+ graphics::Canvas::from_frame(context, graphics::Color::from([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.
+ let mut query = world.query::<(&Position, &Renderable)>();
+ let mut rendering_data: Vec<(Entity, (&Position, &Renderable))> = query.into_iter().collect();
+ rendering_data.sort_by_key(|&k| k.1 .0.z);
+
+ // Iterate through all pairs of positions & renderables, load the image
+ // and draw it at the specified position.
+ for (_, (position, renderable)) in rendering_data.iter() {
+ // Load the image
+ let image = Image::from_path(context, renderable.path.clone()).unwrap();
+ 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));
+ canvas.draw(&image, draw_params);
+ }
-// Initialize the level
-pub fn initialize_level(world: &mut World) {
- create_player(
- world,
- Position {
- x: 0,
- y: 0,
- z: 0, // we will get the z from the factory functions
- },
- );
- create_wall(
- world,
- Position {
- x: 1,
- y: 0,
- z: 0, // we will get the z from the factory functions
- },
- );
- create_box(
- world,
- Position {
- x: 2,
- y: 0,
- z: 0, // we will get the z from the factory functions
- },
- );
+ // Finally, present the canvas, this will actually display everything
+ // on the screen.
+ canvas.finish(context).expect("expected to present");
}
+// ANCHOR_END: rendering_system
+// ANCHOR: main
pub fn main() -> GameResult {
- let mut world = World::new();
- register_components(&mut world);
- initialize_level(&mut world);
+ let world = World::new();
// Create a game context and event loop
let context_builder = ggez::ContextBuilder::new("rust_sokoban", "sokoban")
@@ -221,3 +189,6 @@ pub fn main() -> GameResult {
// Run the main event loop
event::run(context, event_loop, game)
}
+// ANCHOR_END: main
+
+/* ANCHOR_END: all */
diff --git a/code/rust-sokoban-c02-01/Cargo.toml b/code/rust-sokoban-c02-01/Cargo.toml
index 92e5cac..d39b812 100644
--- a/code/rust-sokoban-c02-01/Cargo.toml
+++ b/code/rust-sokoban-c02-01/Cargo.toml
@@ -7,6 +7,6 @@ edition = "2018"
# 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.9.3"
+glam = { version = "0.24", features = ["mint"] }
+hecs = "0.10.5"
\ No newline at end of file
diff --git a/code/rust-sokoban-c02-01/src/main.rs b/code/rust-sokoban-c02-01/src/main.rs
index 561e0f2..5b065ed 100644
--- a/code/rust-sokoban-c02-01/src/main.rs
+++ b/code/rust-sokoban-c02-01/src/main.rs
@@ -1,182 +1,54 @@
+/* ANCHOR: all */
// Rust sokoban
// main.rs
-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 ggez::{
+ conf, event,
+ graphics::{self, DrawParam, Image},
+ Context, GameResult,
};
+use glam::Vec2;
+use hecs::{Entity, World};
use std::path;
const TILE_WIDTH: f32 = 32.0;
-// Components
-#[derive(Debug, Component, Clone, Copy)]
-#[storage(VecStorage)]
+// ANCHOR: components
+#[derive(Clone, Copy)]
pub struct Position {
x: u8,
y: u8,
z: u8,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Renderable {
path: String,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Wall {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Player {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Box {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct BoxSpot {}
-// Systems
-pub struct RenderingSystem<'a> {
- context: &'a mut Context,
-}
-
-// System implementation
-impl<'a> System<'a> for RenderingSystem<'a> {
- // Data
- type SystemData = (ReadStorage<'a, Position>, ReadStorage<'a, Renderable>);
-
- 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));
-
- // Get all the renderables with their positions and sort by the position z
- // This will allow us to have entities layered visually.
- let mut rendering_data = (&positions, &renderables).join().collect::>();
- rendering_data.sort_by_key(|&k| k.0.z);
-
- // Iterate through all pairs of positions & renderables, load the image
- // 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;
-
- // draw
- let draw_params = DrawParam::new().dest(Vec2::new(x, y));
- graphics::draw(self.context, &image, draw_params).expect("expected render");
- }
-
- // Finally, present the context, this will actually display everything
- // on the screen.
- graphics::present(self.context).expect("expected to present");
- }
-}
+// ANCHOR_END: components
+// ANCHOR: game
// This struct will hold all our game state
// For now there is nothing to be held, but we'll add
// things shortly.
struct Game {
world: World,
}
+// ANCHOR_END: game
-// This is the main event loop. ggez tells us to implement
-// two things:
-// - updating
-// - rendering
-impl event::EventHandler for Game {
- fn update(&mut self, _context: &mut Context) -> GameResult {
- Ok(())
- }
-
- fn draw(&mut self, context: &mut Context) -> GameResult {
- // Render game entities
- {
- let mut rs = RenderingSystem { context };
- rs.run_now(&self.world);
- }
-
- Ok(())
- }
-}
-
-// Register components with the world
-pub fn register_components(world: &mut World) {
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
-}
-
-// Create a wall entity
-pub fn create_wall(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
- path: "/images/wall.png".to_string(),
- })
- .with(Wall {})
- .build();
-}
-
-pub fn create_floor(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 5, ..position })
- .with(Renderable {
- path: "/images/floor.png".to_string(),
- })
- .build();
-}
-
-pub fn create_box(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
- path: "/images/box.png".to_string(),
- })
- .with(Box {})
- .build();
-}
-
-pub fn create_box_spot(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 9, ..position })
- .with(Renderable {
- path: "/images/box_spot.png".to_string(),
- })
- .with(BoxSpot {})
- .build();
-}
-
-pub fn create_player(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
- path: "/images/player.png".to_string(),
- })
- .with(Player {})
- .build();
-}
-
-// Initialize the level
+// ANCHOR: init
+// Initialize the level// Initialize the level
pub fn initialize_level(world: &mut World) {
+ // ANCHOR: map
const MAP: &str = "
N N W W W W W W
W W W . . . . W
@@ -188,6 +60,7 @@ pub fn initialize_level(world: &mut World) {
W . . . . . . W
W W W W W W W W
";
+ // ANCHOR_END: map
load_map(world, MAP.to_string());
}
@@ -209,7 +82,9 @@ pub fn load_map(world: &mut World, map_string: String) {
// Figure out what object we should create
match *column {
- "." => create_floor(world, position),
+ "." => {
+ create_floor(world, position);
+ }
"W" => {
create_floor(world, position);
create_wall(world, position);
@@ -232,9 +107,109 @@ pub fn load_map(world: &mut World, map_string: String) {
}
}
}
+// ANCHOR_END: init
+
+// ANCHOR: handler
+impl event::EventHandler for Game {
+ fn update(&mut self, _context: &mut Context) -> GameResult {
+ Ok(())
+ }
+
+ fn draw(&mut self, context: &mut Context) -> GameResult {
+ // Render game entities
+ {
+ run_rendering(&self.world, context);
+ }
+
+ Ok(())
+ }
+}
+// ANCHOR_END: handler
+
+// ANCHOR: entities
+pub fn create_wall(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
+ path: "/images/wall.png".to_string(),
+ },
+ Wall {},
+ ))
+}
+pub fn create_floor(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 5, ..position },
+ Renderable {
+ path: "/images/floor.png".to_string(),
+ },
+ ))
+}
+
+pub fn create_box(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
+ path: "/images/box.png".to_string(),
+ },
+ Box {},
+ ))
+}
+
+pub fn create_box_spot(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 9, ..position },
+ Renderable {
+ path: "/images/box_spot.png".to_string(),
+ },
+ BoxSpot {},
+ ))
+}
+
+pub fn create_player(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
+ path: "/images/player.png".to_string(),
+ },
+ Player {},
+ ))
+}
+// ANCHOR_END: entities
+
+// ANCHOR: rendering_system
+fn run_rendering(world: &World, context: &mut Context) {
+ // Clearing the screen (this gives us the background colour)
+ let mut canvas =
+ graphics::Canvas::from_frame(context, graphics::Color::from([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.
+ let mut query = world.query::<(&Position, &Renderable)>();
+ let mut rendering_data: Vec<(Entity, (&Position, &Renderable))> = query.into_iter().collect();
+ rendering_data.sort_by_key(|&k| k.1 .0.z);
+
+ // Iterate through all pairs of positions & renderables, load the image
+ // and draw it at the specified position.
+ for (_, (position, renderable)) in rendering_data.iter() {
+ // Load the image
+ let image = Image::from_path(context, renderable.path.clone()).unwrap();
+ 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));
+ canvas.draw(&image, draw_params);
+ }
+
+ // Finally, present the canvas, this will actually display everything
+ // on the screen.
+ canvas.finish(context).expect("expected to present");
+}
+// ANCHOR_END: rendering_system
+
+// ANCHOR: main
pub fn main() -> GameResult {
let mut world = World::new();
- register_components(&mut world);
initialize_level(&mut world);
// Create a game context and event loop
@@ -250,3 +225,6 @@ pub fn main() -> GameResult {
// Run the main event loop
event::run(context, event_loop, game)
}
+// ANCHOR_END: main
+
+/* ANCHOR_END: all */
diff --git a/code/rust-sokoban-c02-02/Cargo.toml b/code/rust-sokoban-c02-02/Cargo.toml
index 92e5cac..d39b812 100644
--- a/code/rust-sokoban-c02-02/Cargo.toml
+++ b/code/rust-sokoban-c02-02/Cargo.toml
@@ -7,6 +7,6 @@ edition = "2018"
# 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.9.3"
+glam = { version = "0.24", features = ["mint"] }
+hecs = "0.10.5"
\ No newline at end of file
diff --git a/code/rust-sokoban-c02-02/src/main.rs b/code/rust-sokoban-c02-02/src/main.rs
index cf0dd0e..2ecd6b3 100644
--- a/code/rust-sokoban-c02-02/src/main.rs
+++ b/code/rust-sokoban-c02-02/src/main.rs
@@ -1,241 +1,54 @@
+/* ANCHOR: all */
// Rust sokoban
// main.rs
-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 ggez::{
+ conf, event,
+ graphics::{self, DrawParam, Image},
+ input::keyboard,
+ input::keyboard::{KeyCode, KeyInput},
+ Context, GameResult,
};
+use glam::Vec2;
+use hecs::{Entity, World};
use std::path;
const TILE_WIDTH: f32 = 32.0;
-// Components
-#[derive(Debug, Component, Clone, Copy)]
-#[storage(VecStorage)]
+// ANCHOR: components
+#[derive(Clone, Copy)]
pub struct Position {
x: u8,
y: u8,
z: u8,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Renderable {
path: String,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Wall {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Player {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Box {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct BoxSpot {}
-// Resources
-#[derive(Default)]
-pub struct InputQueue {
- pub keys_pressed: Vec,
-}
-
-// Systems
-pub struct RenderingSystem<'a> {
- context: &'a mut Context,
-}
-
-// System implementation
-impl<'a> System<'a> for RenderingSystem<'a> {
- // Data
- type SystemData = (ReadStorage<'a, Position>, ReadStorage<'a, Renderable>);
-
- 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));
-
- // Get all the renderables with their positions and sort by the position z
- // This will allow us to have entities layered visually.
- let mut rendering_data = (&positions, &renderables).join().collect::>();
- rendering_data.sort_by_key(|&k| k.0.z);
-
- // Iterate through all pairs of positions & renderables, load the image
- // 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;
-
- // draw
- let draw_params = DrawParam::new().dest(Vec2::new(x, y));
- graphics::draw(self.context, &image, draw_params).expect("expected render");
- }
-
- // Finally, present the context, this will actually display everything
- // on the screen.
- graphics::present(self.context).expect("expected to present");
- }
-}
-
-pub struct InputSystem {}
-
-impl<'a> System<'a> for InputSystem {
- // Data
- type SystemData = (
- Write<'a, InputQueue>,
- WriteStorage<'a, Position>,
- ReadStorage<'a, Player>,
- );
-
- fn run(&mut self, data: Self::SystemData) {
- let (mut input_queue, mut positions, players) = data;
-
- for (position, _player) in (&mut positions, &players).join() {
- // Get the first key pressed
- 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,
- _ => (),
- }
- }
- }
- }
-}
+// ANCHOR_END: components
+// ANCHOR: game
// This struct will hold all our game state
// For now there is nothing to be held, but we'll add
// things shortly.
struct Game {
world: World,
}
+// ANCHOR_END: game
-// This is the main event loop. ggez tells us to implement
-// two things:
-// - updating
-// - rendering
-impl event::EventHandler for Game {
- fn update(&mut self, _context: &mut Context) -> GameResult {
- // Run input system
- {
- let mut is = InputSystem {};
- is.run_now(&self.world);
- }
-
- Ok(())
- }
-
- fn draw(&mut self, context: &mut Context) -> GameResult {
- // Render game entities
- {
- let mut rs = RenderingSystem { context };
- rs.run_now(&self.world);
- }
-
- Ok(())
- }
-
- fn key_down_event(
- &mut self,
- _context: &mut Context,
- keycode: KeyCode,
- _keymod: KeyMods,
- _repeat: bool,
- ) {
- println!("Key pressed: {:?}", keycode);
-
- let mut input_queue = self.world.write_resource::();
- input_queue.keys_pressed.push(keycode);
- }
-}
-
-// Register components with the world
-pub fn register_components(world: &mut World) {
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
-}
-
-pub fn register_resources(world: &mut World) {
- world.insert(InputQueue::default())
-}
-
-// Create a wall entity
-pub fn create_wall(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
- path: "/images/wall.png".to_string(),
- })
- .with(Wall {})
- .build();
-}
-
-pub fn create_floor(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 5, ..position })
- .with(Renderable {
- path: "/images/floor.png".to_string(),
- })
- .build();
-}
-
-pub fn create_box(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
- path: "/images/box.png".to_string(),
- })
- .with(Box {})
- .build();
-}
-
-pub fn create_box_spot(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 9, ..position })
- .with(Renderable {
- path: "/images/box_spot.png".to_string(),
- })
- .with(BoxSpot {})
- .build();
-}
-
-pub fn create_player(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
- path: "/images/player.png".to_string(),
- })
- .with(Player {})
- .build();
-}
-
-// Initialize the level
+// ANCHOR: init
+// Initialize the level// Initialize the level
pub fn initialize_level(world: &mut World) {
const MAP: &str = "
N N W W W W W W
@@ -269,7 +82,9 @@ pub fn load_map(world: &mut World, map_string: String) {
// Figure out what object we should create
match *column {
- "." => create_floor(world, position),
+ "." => {
+ create_floor(world, position);
+ }
"W" => {
create_floor(world, position);
create_wall(world, position);
@@ -292,10 +107,169 @@ pub fn load_map(world: &mut World, map_string: String) {
}
}
}
+// ANCHOR_END: init
+
+// ANCHOR: handler
+impl event::EventHandler for Game {
+ fn update(&mut self, context: &mut Context) -> GameResult {
+ // Run input system
+ {
+ run_input(&self.world, context);
+ }
+
+ Ok(())
+ }
+
+ fn draw(&mut self, context: &mut Context) -> GameResult {
+ // Render game entities
+ {
+ run_rendering(&self.world, context);
+ }
+
+ Ok(())
+ }
+}
+// ANCHOR_END: handler
+
+// ANCHOR: entities
+pub fn create_wall(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
+ path: "/images/wall.png".to_string(),
+ },
+ Wall {},
+ ))
+}
+pub fn create_floor(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 5, ..position },
+ Renderable {
+ path: "/images/floor.png".to_string(),
+ },
+ ))
+}
+
+pub fn create_box(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
+ path: "/images/box.png".to_string(),
+ },
+ Box {},
+ ))
+}
+
+pub fn create_box_spot(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 9, ..position },
+ Renderable {
+ path: "/images/box_spot.png".to_string(),
+ },
+ BoxSpot {},
+ ))
+}
+
+pub fn create_player(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
+ path: "/images/player.png".to_string(),
+ },
+ Player {},
+ ))
+}
+// ANCHOR_END: entities
+
+// ANCHOR: rendering_system
+fn run_rendering(world: &World, context: &mut Context) {
+ // Create a canvas to draw on
+ let mut canvas =
+ graphics::Canvas::from_frame(context, graphics::Color::from([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.
+ let mut query = world.query::<(&Position, &Renderable)>();
+ let mut rendering_data: Vec<(Entity, (&Position, &Renderable))> = query.into_iter().collect();
+ rendering_data.sort_by_key(|&k| k.1 .0.z);
+
+ // Iterate through all pairs of positions & renderables, load the image
+ // and draw it at the specified position.
+ for (_, (position, renderable)) in rendering_data.iter() {
+ // Load the image
+ let image = Image::from_path(context, renderable.path.clone()).unwrap();
+ 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));
+ canvas.draw(&image, draw_params);
+ }
+
+ // Finally, present the canvas, this will actually display everything
+ // on the screen.
+ canvas.finish(context).expect("expected to present");
+}
+// ANCHOR_END: rendering_system
+
+// ANCHOR: input_system_print
+fn run_input_print(world: &World, context: &mut Context) {
+ if keyboard::is_key_pressed(context, KeyCode::Up) {
+ println!("UP");
+ }
+ if keyboard::is_key_pressed(context, KeyCode::Down) {
+ println!("DOWN");
+ }
+ if keyboard::is_key_pressed(context, KeyCode::Left) {
+ println!("LEFT");
+ }
+ if keyboard::is_key_pressed(context, KeyCode::Right) {
+ println!("RIGHT");
+ }
+}
+// ANCHOR_END: input_system_print
+
+// ANCHOR: input_system_duplicate
+fn input_system_duplicate(world: &World, context: &mut Context) {
+ for (_, (position, _player)) in world.query::<(&mut Position, &Player)>().iter() {
+ if keyboard::is_key_pressed(context, KeyCode::Up) {
+ position.y -= 1;
+ }
+ if keyboard::is_key_pressed(context, KeyCode::Down) {
+ position.y += 1;
+ }
+ if keyboard::is_key_pressed(context, KeyCode::Left) {
+ position.x -= 1;
+ }
+ if keyboard::is_key_pressed(context, KeyCode::Right) {
+ position.x += 1;
+ }
+ }
+}
+// ANCHOR_END: input_system_duplicate
+
+// ANCHOR: input_system
+fn run_input(world: &World, context: &mut Context) {
+ for (_, (position, _player)) in world.query::<(&mut Position, &Player)>().iter() {
+ if keyboard::is_key_just_pressed(context, KeyCode::Up) {
+ position.y -= 1;
+ }
+ if keyboard::is_key_just_pressed(context, KeyCode::Down) {
+ position.y += 1;
+ }
+ if keyboard::is_key_just_pressed(context, KeyCode::Left) {
+ position.x -= 1;
+ }
+ if keyboard::is_key_just_pressed(context, KeyCode::Right) {
+ position.x += 1;
+ }
+ }
+}
+// ANCHOR_END: input_system
+
+// ANCHOR: main
pub fn main() -> GameResult {
let mut world = World::new();
- register_components(&mut world);
- register_resources(&mut world);
initialize_level(&mut world);
// Create a game context and event loop
@@ -311,3 +285,6 @@ pub fn main() -> GameResult {
// Run the main event loop
event::run(context, event_loop, game)
}
+// ANCHOR_END: main
+
+/* ANCHOR_END: all */
diff --git a/code/rust-sokoban-c02-03/Cargo.toml b/code/rust-sokoban-c02-03/Cargo.toml
index 92e5cac..d39b812 100644
--- a/code/rust-sokoban-c02-03/Cargo.toml
+++ b/code/rust-sokoban-c02-03/Cargo.toml
@@ -7,6 +7,6 @@ edition = "2018"
# 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.9.3"
+glam = { version = "0.24", features = ["mint"] }
+hecs = "0.10.5"
\ 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..c76b33d 100644
--- a/code/rust-sokoban-c02-03/src/main.rs
+++ b/code/rust-sokoban-c02-03/src/main.rs
@@ -1,18 +1,16 @@
+/* ANCHOR: all */
// Rust sokoban
// main.rs
-
-
-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, NullStorage, Entities, world::Index
+ conf, event,
+ graphics::{self, DrawParam, Image},
+ input::keyboard,
+ input::keyboard::{KeyCode, KeyInput},
+ Context, GameResult,
};
+use glam::Vec2;
+use hecs::{Entity, World};
use std::collections::HashMap;
use std::path;
@@ -21,302 +19,45 @@ const TILE_WIDTH: f32 = 32.0;
const MAP_WIDTH: u8 = 8;
const MAP_HEIGHT: u8 = 9;
-// Components
-#[derive(Debug, Component, Clone, Copy)]
-#[storage(VecStorage)]
+// ANCHOR: components
+#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Position {
x: u8,
y: u8,
z: u8,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Renderable {
path: String,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Wall {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Player {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Box {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct BoxSpot {}
-#[derive(Component, Default)]
-#[storage(NullStorage)]
+// ANCHOR: components_movement
pub struct Movable;
-#[derive(Component, Default)]
-#[storage(NullStorage)]
pub struct Immovable;
+// ANCHOR_END: components_movement
-// Resources
-#[derive(Default)]
-pub struct InputQueue {
- pub keys_pressed: Vec,
-}
-
-// Systems
-pub struct RenderingSystem<'a> {
- context: &'a mut Context,
-}
-
-// System implementation
-impl<'a> System<'a> for RenderingSystem<'a> {
- // Data
- type SystemData = (ReadStorage<'a, Position>, ReadStorage<'a, Renderable>);
-
- 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));
-
- // Get all the renderables with their positions and sort by the position z
- // This will allow us to have entities layered visually.
- let mut rendering_data = (&positions, &renderables).join().collect::>();
- rendering_data.sort_by_key(|&k| k.0.z);
-
- // Iterate through all pairs of positions & renderables, load the image
- // 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;
-
- // draw
- let draw_params = DrawParam::new().dest(Vec2::new(x, y));
- graphics::draw(self.context, &image, draw_params).expect("expected render");
- }
-
- // Finally, present the context, this will actually display everything
- // on the screen.
- graphics::present(self.context).expect("expected to present");
- }
-}
-
-pub struct InputSystem {}
-
-// System implementation
-impl<'a> System<'a> for InputSystem {
- // Data
- type SystemData = (
- Write<'a, InputQueue>,
- Entities<'a>,
- WriteStorage<'a, Position>,
- ReadStorage<'a, Player>,
- ReadStorage<'a, Movable>,
- ReadStorage<'a, Immovable>,
- );
-
- fn run(&mut self, data: Self::SystemData) {
- let (mut input_queue, entities, mut positions, players, movables, immovables) = data;
-
- let mut to_move = Vec::new();
-
- for (position, _player) in (&positions, &players).join() {
- // Get the first key pressed
- if let Some(key) = input_queue.keys_pressed.pop() {
- // get all the movables and immovables
- let mov: HashMap<(u8, u8), Index> = (&entities, &movables, &positions)
- .join()
- .map(|t| ((t.2.x, t.2.y), t.0.id()))
- .collect::>();
- let immov: HashMap<(u8, u8), Index> = (&entities, &immovables, &positions)
- .join()
- .map(|t| ((t.2.x, t.2.y), t.0.id()))
- .collect::>();
-
- // 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),
- _ => continue,
- };
-
- let range = if start < end {
- (start..=end).collect::>()
- } else {
- (end..=start).rev().collect::>()
- };
-
- for x_or_y in range {
- let pos = if is_x {
- (x_or_y, position.y)
- } else {
- (position.x, x_or_y)
- };
-
- // find a movable
- // if it exists, we try to move it and continue
- // if it doesn't exist, we continue and try to find an immovable instead
- match mov.get(&pos) {
- Some(id) => to_move.push((key, id.clone())),
- None => {
- // find an immovable
- // if it exists, we need to stop and not move anything
- // if it doesn't exist, we stop because we found a gap
- match immov.get(&pos) {
- Some(_id) => to_move.clear(),
- None => break,
- }
- }
- }
- }
- }
- }
-
- // Now actually move what needs to be moved
- for (key, id) in to_move {
- 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,
- _ => (),
- }
- }
- }
- }
-}
+// ANCHOR_END: components
+// ANCHOR: game
// This struct will hold all our game state
// For now there is nothing to be held, but we'll add
// things shortly.
struct Game {
world: World,
}
+// ANCHOR_END: game
-// This is the main event loop. ggez tells us to implement
-// two things:
-// - updating
-// - rendering
-impl event::EventHandler for Game {
- fn update(&mut self, _context: &mut Context) -> GameResult {
- // Run input system
- {
- let mut is = InputSystem {};
- is.run_now(&self.world);
- }
-
- Ok(())
- }
-
- fn draw(&mut self, context: &mut Context) -> GameResult {
- // Render game entities
- {
- let mut rs = RenderingSystem { context };
- rs.run_now(&self.world);
- }
-
- Ok(())
- }
-
- fn key_down_event(
- &mut self,
- _context: &mut Context,
- keycode: KeyCode,
- _keymod: KeyMods,
- _repeat: bool,
- ) {
- println!("Key pressed: {:?}", keycode);
-
- let mut input_queue = self.world.write_resource::();
- input_queue.keys_pressed.push(keycode);
- }
-}
-
-// Register components with the world
-pub fn register_components(world: &mut World) {
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
-}
-
-pub fn register_resources(world: &mut World) {
- world.insert(InputQueue::default())
-}
-
-// Create a wall entity
-pub fn create_wall(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
- path: "/images/wall.png".to_string(),
- })
- .with(Wall {})
- .with(Immovable)
- .build();
-}
-
-pub fn create_floor(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 5, ..position })
- .with(Renderable {
- path: "/images/floor.png".to_string(),
- })
- .build();
-}
-
-pub fn create_box(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
- path: "/images/box.png".to_string(),
- })
- .with(Box {})
- .with(Movable)
- .build();
-}
-
-pub fn create_box_spot(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 9, ..position })
- .with(Renderable {
- path: "/images/box_spot.png".to_string(),
- })
- .with(BoxSpot {})
- .build();
-}
-
-pub fn create_player(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
- path: "/images/player.png".to_string(),
- })
- .with(Player {})
- .with(Movable)
- .build();
-}
-
-// Initialize the level
+// ANCHOR: init
+// Initialize the level// Initialize the level
pub fn initialize_level(world: &mut World) {
const MAP: &str = "
N N W W W W W W
@@ -350,7 +91,9 @@ pub fn load_map(world: &mut World, map_string: String) {
// Figure out what object we should create
match *column {
- "." => create_floor(world, position),
+ "." => {
+ create_floor(world, position);
+ }
"W" => {
create_floor(world, position);
create_wall(world, position);
@@ -373,10 +116,206 @@ pub fn load_map(world: &mut World, map_string: String) {
}
}
}
+// ANCHOR_END: init
+
+// ANCHOR: handler
+impl event::EventHandler for Game {
+ fn update(&mut self, context: &mut Context) -> GameResult {
+ // Run input system
+ {
+ run_input(&self.world, context);
+ }
+
+ Ok(())
+ }
+
+ fn draw(&mut self, context: &mut Context) -> GameResult {
+ // Render game entities
+ {
+ run_rendering(&self.world, context);
+ }
+
+ Ok(())
+ }
+}
+// ANCHOR_END: handler
+
+// ANCHOR: entities
+pub fn create_wall(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
+ path: "/images/wall.png".to_string(),
+ },
+ Wall {},
+ Immovable {},
+ ))
+}
+pub fn create_floor(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 5, ..position },
+ Renderable {
+ path: "/images/floor.png".to_string(),
+ },
+ ))
+}
+
+pub fn create_box(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
+ path: "/images/box.png".to_string(),
+ },
+ Box {},
+ Movable {},
+ ))
+}
+
+pub fn create_box_spot(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 9, ..position },
+ Renderable {
+ path: "/images/box_spot.png".to_string(),
+ },
+ BoxSpot {},
+ ))
+}
+
+pub fn create_player(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
+ path: "/images/player.png".to_string(),
+ },
+ Player {},
+ Movable {},
+ ))
+}
+// ANCHOR_END: entities
+
+// ANCHOR: rendering_system
+fn run_rendering(world: &World, context: &mut Context) {
+ // Clearing the screen (this gives us the background colour)
+ let mut canvas =
+ graphics::Canvas::from_frame(context, graphics::Color::from([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.
+ let mut query = world.query::<(&Position, &Renderable)>();
+ let mut rendering_data: Vec<(Entity, (&Position, &Renderable))> = query.into_iter().collect();
+ rendering_data.sort_by_key(|&k| k.1 .0.z);
+
+ // Iterate through all pairs of positions & renderables, load the image
+ // and draw it at the specified position.
+ for (_, (position, renderable)) in rendering_data.iter() {
+ // Load the image
+ let image = Image::from_path(context, renderable.path.clone()).unwrap();
+ 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));
+ canvas.draw(&image, draw_params);
+ }
+
+ // Finally, present the canvas, this will actually display everything
+ // on the screen.
+ canvas.finish(context).expect("expected to present");
+}
+// ANCHOR_END: rendering_system
+
+// ANCHOR: input_system
+fn run_input(world: &World, context: &mut Context) {
+ let mut to_move: Vec<(Entity, KeyCode)> = Vec::new();
+
+ // get all the movables and immovables
+ let mov: HashMap<(u8, u8), Entity> = world
+ .query::<(&Position, &Movable)>()
+ .iter()
+ .map(|t| ((t.1 .0.x, t.1 .0.y), t.0.clone()))
+ .collect::>();
+ let immov: HashMap<(u8, u8), Entity> = world
+ .query::<(&Position, &Immovable)>()
+ .iter()
+ .map(|t| ((t.1 .0.x, t.1 .0.y), t.0.clone()))
+ .collect::>();
+
+ for (_, (position, _player)) in world.query::<(&mut Position, &Player)>().iter() {
+ if keyboard::is_key_repeated(context) {
+ continue;
+ }
+
+ // Now iterate through current position to the end of the map
+ // on the correct axis and check what needs to move.
+ let key = if keyboard::is_key_pressed(context, KeyCode::Up) {
+ KeyCode::Up
+ } else if keyboard::is_key_pressed(context, KeyCode::Down) {
+ KeyCode::Down
+ } else if keyboard::is_key_pressed(context, KeyCode::Left) {
+ KeyCode::Left
+ } else if keyboard::is_key_pressed(context, KeyCode::Right) {
+ KeyCode::Right
+ } else {
+ continue;
+ };
+
+ let (start, end, is_x) = match key {
+ KeyCode::Up => (position.y, 0, false),
+ KeyCode::Down => (position.y, MAP_HEIGHT - 1, false),
+ KeyCode::Left => (position.x, 0, true),
+ KeyCode::Right => (position.x, MAP_WIDTH - 1, true),
+ _ => continue,
+ };
+
+ let range = if start < end {
+ (start..=end).collect::>()
+ } else {
+ (end..=start).rev().collect::>()
+ };
+
+ for x_or_y in range {
+ let pos = if is_x {
+ (x_or_y, position.y)
+ } else {
+ (position.x, x_or_y)
+ };
+
+ // find a movable
+ // if it exists, we try to move it and continue
+ // if it doesn't exist, we continue and try to find an immovable instead
+ match mov.get(&pos) {
+ Some(entity) => to_move.push((*entity, key)),
+ None => {
+ // find an immovable
+ // if it exists, we need to stop and not move anything
+ // if it doesn't exist, we stop because we found a gap
+ match immov.get(&pos) {
+ Some(_id) => to_move.clear(),
+ None => break,
+ }
+ }
+ }
+ }
+ }
+
+ // Now actually move what needs to be moved
+ for (entity, key) in to_move {
+ let mut position = world.get::<&mut Position>(entity).unwrap();
+
+ match key {
+ KeyCode::Up => position.y -= 1,
+ KeyCode::Down => position.y += 1,
+ KeyCode::Left => position.x -= 1,
+ KeyCode::Right => position.x += 1,
+ _ => (),
+ }
+ }
+}
+// ANCHOR_END: input_system
+
+// ANCHOR: main
pub fn main() -> GameResult {
let mut world = World::new();
- register_components(&mut world);
- register_resources(&mut world);
initialize_level(&mut world);
// Create a game context and event loop
@@ -392,3 +331,6 @@ pub fn main() -> GameResult {
// Run the main event loop
event::run(context, event_loop, game)
}
+// ANCHOR_END: main
+
+/* ANCHOR_END: all */
diff --git a/code/rust-sokoban-c02-04/Cargo.toml b/code/rust-sokoban-c02-04/Cargo.toml
index 92e5cac..d39b812 100644
--- a/code/rust-sokoban-c02-04/Cargo.toml
+++ b/code/rust-sokoban-c02-04/Cargo.toml
@@ -7,6 +7,6 @@ edition = "2018"
# 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.9.3"
+glam = { version = "0.24", features = ["mint"] }
+hecs = "0.10.5"
\ 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..68a29eb 100644
--- a/code/rust-sokoban-c02-04/src/components.rs
+++ b/code/rust-sokoban-c02-04/src/components.rs
@@ -1,51 +1,22 @@
-use specs::{Component, NullStorage, VecStorage, World, WorldExt};
-
-// Components
-#[derive(Debug, Component, Clone, Copy)]
-#[storage(VecStorage)]
+#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Position {
pub x: u8,
pub y: u8,
pub z: u8,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Renderable {
pub path: String,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Wall {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Player {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Box {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct BoxSpot {}
-#[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::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
-}
diff --git a/code/rust-sokoban-c02-04/src/entities.rs b/code/rust-sokoban-c02-04/src/entities.rs
index c3922fd..5a274e1 100644
--- a/code/rust-sokoban-c02-04/src/entities.rs
+++ b/code/rust-sokoban-c02-04/src/entities.rs
@@ -1,60 +1,53 @@
use crate::components::*;
-use specs::{Builder, World, WorldExt};
+use hecs::{Entity, World};
-// Create a wall entity
-pub fn create_wall(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
+pub fn create_wall(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
path: "/images/wall.png".to_string(),
- })
- .with(Wall {})
- .with(Immovable)
- .build();
+ },
+ Wall {},
+ Immovable {},
+ ))
}
-
-pub fn create_floor(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 5, ..position })
- .with(Renderable {
+pub fn create_floor(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 5, ..position },
+ Renderable {
path: "/images/floor.png".to_string(),
- })
- .build();
+ },
+ ))
}
-pub fn create_box(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
+pub fn create_box(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
path: "/images/box.png".to_string(),
- })
- .with(Box {})
- .with(Movable)
- .build();
+ },
+ Box {},
+ Movable {},
+ ))
}
-pub fn create_box_spot(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 9, ..position })
- .with(Renderable {
+pub fn create_box_spot(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 9, ..position },
+ Renderable {
path: "/images/box_spot.png".to_string(),
- })
- .with(BoxSpot {})
- .build();
+ },
+ BoxSpot {},
+ ))
}
-pub fn create_player(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
+pub fn create_player(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
path: "/images/player.png".to_string(),
- })
- .with(Player {})
- .with(Movable)
- .build();
+ },
+ Player {},
+ Movable {},
+ ))
}
diff --git a/code/rust-sokoban-c02-04/src/main.rs b/code/rust-sokoban-c02-04/src/main.rs
index 34f10b7..0094eac 100644
--- a/code/rust-sokoban-c02-04/src/main.rs
+++ b/code/rust-sokoban-c02-04/src/main.rs
@@ -1,32 +1,41 @@
+/* ANCHOR: all */
// Rust sokoban
// main.rs
-use ggez::{conf, event::{self, KeyCode, KeyMods}, Context, GameResult};
-use specs::{RunNow, World, WorldExt};
+use ggez::{
+ conf, event,
+ graphics::{self, DrawParam, Image},
+ input::keyboard,
+ input::keyboard::{KeyCode, KeyInput},
+ Context, GameResult,
+};
+use glam::Vec2;
+use hecs::{Entity, World};
+
+use std::collections::HashMap;
use std::path;
mod components;
mod constants;
mod entities;
mod map;
-mod resources;
mod systems;
-use crate::components::*;
-use crate::map::*;
-use crate::resources::*;
-use crate::systems::*;
-
+// ANCHOR: game
+// This struct will hold all our game state
+// For now there is nothing to be held, but we'll add
+// things shortly.
struct Game {
world: World,
}
+// ANCHOR_END: game
+// ANCHOR: handler
impl event::EventHandler for Game {
- fn update(&mut self, _context: &mut Context) -> GameResult {
+ fn update(&mut self, context: &mut Context) -> GameResult {
// Run input system
{
- let mut is = InputSystem {};
- is.run_now(&self.world);
+ systems::input::run_input(&self.world, context);
}
Ok(())
@@ -35,49 +44,18 @@ impl event::EventHandler for Game {
fn draw(&mut self, context: &mut Context) -> GameResult {
// Render game entities
{
- let mut rs = RenderingSystem { context };
- rs.run_now(&self.world);
+ systems::rendering::run_rendering(&self.world, context);
}
Ok(())
}
-
- fn key_down_event(
- &mut self,
- _context: &mut Context,
- keycode: KeyCode,
- _keymod: KeyMods,
- _repeat: bool,
- ) {
- println!("Key pressed: {:?}", keycode);
-
- let mut input_queue = self.world.write_resource::();
- input_queue.keys_pressed.push(keycode);
- }
-}
-
-// 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());
}
+// ANCHOR_END: handler
+// ANCHOR: main
pub fn main() -> GameResult {
let mut world = World::new();
- register_components(&mut world);
- register_resources(&mut world);
- initialize_level(&mut world);
+ map::initialize_level(&mut world);
// Create a game context and event loop
let context_builder = ggez::ContextBuilder::new("rust_sokoban", "sokoban")
@@ -92,3 +70,6 @@ pub fn main() -> GameResult {
// Run the main event loop
event::run(context, event_loop, game)
}
+// ANCHOR_END: main
+
+/* ANCHOR_END: all */
diff --git a/code/rust-sokoban-c02-04/src/map.rs b/code/rust-sokoban-c02-04/src/map.rs
index 2e3222f..162d440 100644
--- a/code/rust-sokoban-c02-04/src/map.rs
+++ b/code/rust-sokoban-c02-04/src/map.rs
@@ -1,6 +1,22 @@
use crate::components::Position;
use crate::entities::*;
-use specs::World;
+use hecs::World;
+
+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
@@ -19,7 +35,9 @@ pub fn load_map(world: &mut World, map_string: String) {
// Figure out what object we should create
match *column {
- "." => create_floor(world, position),
+ "." => {
+ create_floor(world, position);
+ }
"W" => {
create_floor(world, position);
create_wall(world, position);
diff --git a/code/rust-sokoban-c02-04/src/resources.rs b/code/rust-sokoban-c02-04/src/resources.rs
deleted file mode 100644
index 10e8023..0000000
--- a/code/rust-sokoban-c02-04/src/resources.rs
+++ /dev/null
@@ -1,12 +0,0 @@
-use ggez::event::KeyCode;
-use specs::World;
-
-// Resources
-#[derive(Default)]
-pub struct InputQueue {
- pub keys_pressed: Vec,
-}
-
-pub fn register_resources(world: &mut World) {
- world.insert(InputQueue::default())
-}
diff --git a/code/rust-sokoban-c02-04/src/systems/input.rs b/code/rust-sokoban-c02-04/src/systems/input.rs
new file mode 100644
index 0000000..037a3b3
--- /dev/null
+++ b/code/rust-sokoban-c02-04/src/systems/input.rs
@@ -0,0 +1,102 @@
+use ggez::{
+ conf, event,
+ graphics::{self, DrawParam, Image},
+ input::keyboard,
+ input::keyboard::{KeyCode, KeyInput},
+ Context, GameResult,
+};
+use glam::Vec2;
+use hecs::{Entity, World};
+
+use std::collections::HashMap;
+use std::path;
+
+use crate::components::*;
+use crate::constants::*;
+
+pub fn run_input(world: &World, context: &mut Context) {
+ let mut to_move: Vec<(Entity, KeyCode)> = Vec::new();
+
+ // get all the movables and immovables
+ let mov: HashMap<(u8, u8), Entity> = world
+ .query::<(&Position, &Movable)>()
+ .iter()
+ .map(|t| ((t.1 .0.x, t.1 .0.y), t.0.clone()))
+ .collect::>();
+ let immov: HashMap<(u8, u8), Entity> = world
+ .query::<(&Position, &Immovable)>()
+ .iter()
+ .map(|t| ((t.1 .0.x, t.1 .0.y), t.0.clone()))
+ .collect::>();
+
+ for (_, (position, _player)) in world.query::<(&mut Position, &Player)>().iter() {
+ if keyboard::is_key_repeated(context) {
+ continue;
+ }
+
+ // Now iterate through current position to the end of the map
+ // on the correct axis and check what needs to move.
+ let key = if keyboard::is_key_just_pressed(context, KeyCode::Up) {
+ KeyCode::Up
+ } else if keyboard::is_key_just_pressed(context, KeyCode::Down) {
+ KeyCode::Down
+ } else if keyboard::is_key_just_pressed(context, KeyCode::Left) {
+ KeyCode::Left
+ } else if keyboard::is_key_just_pressed(context, KeyCode::Right) {
+ KeyCode::Right
+ } else {
+ continue;
+ };
+
+ let (start, end, is_x) = match key {
+ KeyCode::Up => (position.y, 0, false),
+ KeyCode::Down => (position.y, MAP_HEIGHT - 1, false),
+ KeyCode::Left => (position.x, 0, true),
+ KeyCode::Right => (position.x, MAP_WIDTH - 1, true),
+ _ => continue,
+ };
+
+ let range = if start < end {
+ (start..=end).collect::>()
+ } else {
+ (end..=start).rev().collect::>()
+ };
+
+ for x_or_y in range {
+ let pos = if is_x {
+ (x_or_y, position.y)
+ } else {
+ (position.x, x_or_y)
+ };
+
+ // find a movable
+ // if it exists, we try to move it and continue
+ // if it doesn't exist, we continue and try to find an immovable instead
+ match mov.get(&pos) {
+ Some(entity) => to_move.push((*entity, key)),
+ None => {
+ // find an immovable
+ // if it exists, we need to stop and not move anything
+ // if it doesn't exist, we stop because we found a gap
+ match immov.get(&pos) {
+ Some(_id) => to_move.clear(),
+ None => break,
+ }
+ }
+ }
+ }
+ }
+
+ // Now actually move what needs to be moved
+ for (entity, key) in to_move {
+ let mut position = world.get::<&mut Position>(entity).unwrap();
+
+ match key {
+ KeyCode::Up => position.y -= 1,
+ KeyCode::Down => position.y += 1,
+ KeyCode::Left => position.x -= 1,
+ KeyCode::Right => position.x += 1,
+ _ => (),
+ }
+ }
+}
diff --git a/code/rust-sokoban-c02-04/src/systems/input_system.rs b/code/rust-sokoban-c02-04/src/systems/input_system.rs
deleted file mode 100644
index 0b77146..0000000
--- a/code/rust-sokoban-c02-04/src/systems/input_system.rs
+++ /dev/null
@@ -1,97 +0,0 @@
-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 std::collections::HashMap;
-
-pub struct InputSystem {}
-
-// System implementation
-impl<'a> System<'a> for InputSystem {
- // Data
- type SystemData = (
- Write<'a, InputQueue>,
- Entities<'a>,
- WriteStorage<'a, Position>,
- ReadStorage<'a, Player>,
- ReadStorage<'a, Movable>,
- ReadStorage<'a, Immovable>,
- );
-
- fn run(&mut self, data: Self::SystemData) {
- let (mut input_queue, entities, mut positions, players, movables, immovables) = data;
-
- let mut to_move = Vec::new();
-
- for (position, _player) in (&positions, &players).join() {
- // Get the first key pressed
- if let Some(key) = input_queue.keys_pressed.pop() {
- // get all the movables and immovables
- let mov: HashMap<(u8, u8), Index> = (&entities, &movables, &positions)
- .join()
- .map(|t| ((t.2.x, t.2.y), t.0.id()))
- .collect::>();
- let immov: HashMap<(u8, u8), Index> = (&entities, &immovables, &positions)
- .join()
- .map(|t| ((t.2.x, t.2.y), t.0.id()))
- .collect::>();
-
- // 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),
- _ => continue,
- };
-
- let range = if start < end {
- (start..=end).collect::>()
- } else {
- (end..=start).rev().collect::>()
- };
-
- for x_or_y in range {
- let pos = if is_x {
- (x_or_y, position.y)
- } else {
- (position.x, x_or_y)
- };
-
- // find a movable
- // if it exists, we try to move it and continue
- // if it doesn't exist, we continue and try to find an immovable instead
- match mov.get(&pos) {
- Some(id) => to_move.push((key, id.clone())),
- None => {
- // find an immovable
- // if it exists, we need to stop and not move anything
- // if it doesn't exist, we stop because we found a gap
- match immov.get(&pos) {
- Some(_id) => to_move.clear(),
- None => break,
- }
- }
- }
- }
- }
- }
-
- // Now actually move what needs to be moved
- for (key, id) in to_move {
- 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,
- _ => (),
- }
- }
- }
- }
-}
diff --git a/code/rust-sokoban-c02-04/src/systems/mod.rs b/code/rust-sokoban-c02-04/src/systems/mod.rs
index e574ced..77c4d48 100644
--- a/code/rust-sokoban-c02-04/src/systems/mod.rs
+++ b/code/rust-sokoban-c02-04/src/systems/mod.rs
@@ -1,5 +1,2 @@
-mod input_system;
-mod rendering_system;
-
-pub use self::input_system::InputSystem;
-pub use self::rendering_system::RenderingSystem;
+pub mod input;
+pub mod rendering;
diff --git a/code/rust-sokoban-c02-04/src/systems/rendering.rs b/code/rust-sokoban-c02-04/src/systems/rendering.rs
new file mode 100644
index 0000000..b55a4ad
--- /dev/null
+++ b/code/rust-sokoban-c02-04/src/systems/rendering.rs
@@ -0,0 +1,44 @@
+use ggez::{
+ conf, event,
+ graphics::{self, DrawParam, Image},
+ input::keyboard,
+ input::keyboard::{KeyCode, KeyInput},
+ Context, GameResult,
+};
+use glam::Vec2;
+use hecs::{Entity, World};
+
+use std::collections::HashMap;
+use std::path;
+
+use crate::components::*;
+use crate::constants::*;
+
+pub fn run_rendering(world: &World, context: &mut Context) {
+ // Clearing the screen (this gives us the background colour)
+ let mut canvas =
+ graphics::Canvas::from_frame(context, graphics::Color::from([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.
+ let mut query = world.query::<(&Position, &Renderable)>();
+ let mut rendering_data: Vec<(Entity, (&Position, &Renderable))> = query.into_iter().collect();
+ rendering_data.sort_by_key(|&k| k.1 .0.z);
+
+ // Iterate through all pairs of positions & renderables, load the image
+ // and draw it at the specified position.
+ for (_, (position, renderable)) in rendering_data.iter() {
+ // Load the image
+ let image = Image::from_path(context, renderable.path.clone()).unwrap();
+ 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));
+ canvas.draw(&image, draw_params);
+ }
+
+ // Finally, present the canvas, this will actually display everything
+ // on the screen.
+ canvas.finish(context).expect("expected to present");
+}
diff --git a/code/rust-sokoban-c02-04/src/systems/rendering_system.rs b/code/rust-sokoban-c02-04/src/systems/rendering_system.rs
deleted file mode 100644
index d0c5693..0000000
--- a/code/rust-sokoban-c02-04/src/systems/rendering_system.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-use crate::components::*;
-use crate::constants::TILE_WIDTH;
-
-use ggez::{Context, graphics::{self, DrawParam, Image}};
-use specs::{Join, ReadStorage, System};
-use glam::Vec2;
-
-pub struct RenderingSystem<'a> {
- pub context: &'a mut Context,
-}
-
-// System implementation
-impl<'a> System<'a> for RenderingSystem<'a> {
- // Data
- type SystemData = (ReadStorage<'a, Position>, ReadStorage<'a, Renderable>);
-
- 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));
-
- // Get all the renderables with their positions and sort by the position z
- // This will allow us to have entities layered visually.
- let mut rendering_data = (&positions, &renderables).join().collect::>();
- rendering_data.sort_by_key(|&k| k.0.z);
-
- // Iterate through all pairs of positions & renderables, load the image
- // 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;
-
- // draw
- let draw_params = DrawParam::new().dest(Vec2::new(x, y));
- graphics::draw(self.context, &image, draw_params).expect("expected render");
- }
-
- // Finally, present the context, this will actually display everything
- // on the screen.
- graphics::present(self.context).expect("expected to present");
- }
-}
diff --git a/code/rust-sokoban-c02-05/Cargo.toml b/code/rust-sokoban-c02-05/Cargo.toml
index 92e5cac..d39b812 100644
--- a/code/rust-sokoban-c02-05/Cargo.toml
+++ b/code/rust-sokoban-c02-05/Cargo.toml
@@ -7,6 +7,6 @@ edition = "2018"
# 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.9.3"
+glam = { version = "0.24", features = ["mint"] }
+hecs = "0.10.5"
\ 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..09a3ad3 100644
--- a/code/rust-sokoban-c02-05/src/components.rs
+++ b/code/rust-sokoban-c02-05/src/components.rs
@@ -1,51 +1,58 @@
-use specs::{Component, NullStorage, VecStorage, World, WorldExt};
+use std::fmt;
+use std::fmt::Display;
-// Components
-#[derive(Debug, Component, Clone, Copy)]
-#[storage(VecStorage)]
+#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Position {
pub x: u8,
pub y: u8,
pub z: u8,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Renderable {
pub path: String,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Wall {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Player {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Box {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct BoxSpot {}
-#[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::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
+// ANCHOR: gameplay_state
+pub enum GameplayState {
+ Playing,
+ Won,
}
+
+#[derive(Default)]
+pub struct Gameplay {
+ pub state: GameplayState,
+ pub moves_count: u32,
+}
+// ANCHOR_END: gameplay_state
+
+// ANCHOR: gameplay_state_impl_default
+impl Default for GameplayState {
+ fn default() -> Self {
+ GameplayState::Playing
+ }
+}
+// ANCHOR_END: gameplay_state_impl_default
+
+// ANCHOR: gameplay_state_impl_display
+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(())
+ }
+}
+// ANCHOR_END: gameplay_state_impl_display
diff --git a/code/rust-sokoban-c02-05/src/entities.rs b/code/rust-sokoban-c02-05/src/entities.rs
index c3922fd..4cbcc61 100644
--- a/code/rust-sokoban-c02-05/src/entities.rs
+++ b/code/rust-sokoban-c02-05/src/entities.rs
@@ -1,60 +1,58 @@
use crate::components::*;
-use specs::{Builder, World, WorldExt};
+use hecs::{Entity, World};
-// Create a wall entity
-pub fn create_wall(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
+pub fn create_wall(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
path: "/images/wall.png".to_string(),
- })
- .with(Wall {})
- .with(Immovable)
- .build();
+ },
+ Wall {},
+ Immovable {},
+ ))
}
-pub fn create_floor(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 5, ..position })
- .with(Renderable {
+pub fn create_floor(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 5, ..position },
+ Renderable {
path: "/images/floor.png".to_string(),
- })
- .build();
+ },
+ ))
}
-pub fn create_box(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
+pub fn create_box(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
path: "/images/box.png".to_string(),
- })
- .with(Box {})
- .with(Movable)
- .build();
+ },
+ Box {},
+ Movable {},
+ ))
}
-pub fn create_box_spot(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 9, ..position })
- .with(Renderable {
+pub fn create_box_spot(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 9, ..position },
+ Renderable {
path: "/images/box_spot.png".to_string(),
- })
- .with(BoxSpot {})
- .build();
+ },
+ BoxSpot {},
+ ))
}
-pub fn create_player(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
+pub fn create_player(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
path: "/images/player.png".to_string(),
- })
- .with(Player {})
- .with(Movable)
- .build();
+ },
+ Player {},
+ Movable {},
+ ))
+}
+
+pub fn create_gameplay(world: &mut World) -> Entity {
+ world.spawn((Gameplay::default(),))
}
diff --git a/code/rust-sokoban-c02-05/src/main.rs b/code/rust-sokoban-c02-05/src/main.rs
index 327da35..691235a 100644
--- a/code/rust-sokoban-c02-05/src/main.rs
+++ b/code/rust-sokoban-c02-05/src/main.rs
@@ -1,38 +1,46 @@
+/* ANCHOR: all */
// Rust sokoban
// main.rs
-use ggez::{conf, event::{self, KeyCode, KeyMods}, Context, GameResult};
-use specs::{RunNow, World, WorldExt};
+use ggez::{
+ conf, event,
+ graphics::{self, DrawParam, Image},
+ input::keyboard,
+ input::keyboard::{KeyCode, KeyInput},
+ Context, GameResult,
+};
+use glam::Vec2;
+use hecs::{Entity, World};
+
+use std::collections::HashMap;
use std::path;
mod components;
mod constants;
mod entities;
mod map;
-mod resources;
mod systems;
-use crate::components::*;
-use crate::map::*;
-use crate::resources::*;
-use crate::systems::*;
-
+// ANCHOR: game
+// This struct will hold all our game state
+// For now there is nothing to be held, but we'll add
+// things shortly.
struct Game {
world: World,
}
+// ANCHOR_END: game
+// ANCHOR: handler
impl event::EventHandler for Game {
- fn update(&mut self, _context: &mut Context) -> GameResult {
+ fn update(&mut self, context: &mut Context) -> GameResult {
// Run input system
{
- let mut is = InputSystem {};
- is.run_now(&self.world);
+ systems::input::run_input(&self.world, context);
}
- // Run gameplay state system
+ // Run gameplay state
{
- let mut gss = GameplayStateSystem {};
- gss.run_now(&self.world);
+ systems::gameplay::run_gameplay_state(&mut self.world);
}
Ok(())
@@ -41,49 +49,19 @@ impl event::EventHandler for Game {
fn draw(&mut self, context: &mut Context) -> GameResult {
// Render game entities
{
- let mut rs = RenderingSystem { context };
- rs.run_now(&self.world);
+ systems::rendering::run_rendering(&self.world, context);
}
Ok(())
}
-
- fn key_down_event(
- &mut self,
- _context: &mut Context,
- keycode: KeyCode,
- _keymod: KeyMods,
- _repeat: bool,
- ) {
- println!("Key pressed: {:?}", keycode);
-
- let mut input_queue = self.world.write_resource::();
- input_queue.keys_pressed.push(keycode);
- }
-}
-
-// 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());
}
+// ANCHOR_END: handler
+// ANCHOR: main
pub fn main() -> GameResult {
let mut world = World::new();
- register_components(&mut world);
- register_resources(&mut world);
- initialize_level(&mut world);
+ map::initialize_level(&mut world);
+ entities::create_gameplay(&mut world);
// Create a game context and event loop
let context_builder = ggez::ContextBuilder::new("rust_sokoban", "sokoban")
@@ -98,3 +76,6 @@ pub fn main() -> GameResult {
// Run the main event loop
event::run(context, event_loop, game)
}
+// ANCHOR_END: main
+
+/* ANCHOR_END: all */
diff --git a/code/rust-sokoban-c02-05/src/map.rs b/code/rust-sokoban-c02-05/src/map.rs
index 2e3222f..162d440 100644
--- a/code/rust-sokoban-c02-05/src/map.rs
+++ b/code/rust-sokoban-c02-05/src/map.rs
@@ -1,6 +1,22 @@
use crate::components::Position;
use crate::entities::*;
-use specs::World;
+use hecs::World;
+
+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
@@ -19,7 +35,9 @@ pub fn load_map(world: &mut World, map_string: String) {
// Figure out what object we should create
match *column {
- "." => create_floor(world, position),
+ "." => {
+ create_floor(world, position);
+ }
"W" => {
create_floor(world, position);
create_wall(world, position);
diff --git a/code/rust-sokoban-c02-05/src/resources.rs b/code/rust-sokoban-c02-05/src/resources.rs
deleted file mode 100644
index b319be1..0000000
--- a/code/rust-sokoban-c02-05/src/resources.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use ggez::event::KeyCode;
-use specs::World;
-
-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 enum GameplayState {
- Playing,
- 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(())
- }
-}
-
-impl Default for GameplayState {
- fn default() -> Self {
- Self::Playing
- }
-}
-
-#[derive(Default)]
-pub struct Gameplay {
- pub state: GameplayState,
- pub moves_count: u32
-}
diff --git a/code/rust-sokoban-c02-05/src/systems/gameplay.rs b/code/rust-sokoban-c02-05/src/systems/gameplay.rs
new file mode 100644
index 0000000..ab25bca
--- /dev/null
+++ b/code/rust-sokoban-c02-05/src/systems/gameplay.rs
@@ -0,0 +1,37 @@
+use crate::components::*;
+use hecs::World;
+
+use std::collections::HashMap;
+
+pub fn run_gameplay_state(world: &World) {
+ // get all boxes indexed by position
+ let mut query = world.query::<(&Position, &Box)>();
+ let boxes_by_position: HashMap<(u8, u8), &Box> = query
+ .iter()
+ .map(|(e, t)| ((t.0.x, t.0.y), t.1))
+ .collect::>();
+
+ // loop through all box spots and check if there is a corresponding
+ // box at that position
+ let boxes_out_of_position: usize = world
+ .query::<(&Position, &BoxSpot)>()
+ .iter()
+ .map(|(_, (position, _))| {
+ if boxes_by_position.contains_key(&(position.x, position.y)) {
+ 0
+ } else {
+ 1
+ }
+ })
+ .collect::>()
+ .into_iter()
+ .sum();
+
+ // If we made it this far, then all box spots have boxes on them, and the
+ // game has been won
+ if boxes_out_of_position == 0 {
+ let mut query = world.query::<&mut Gameplay>();
+ let mut gameplay = query.iter().next().unwrap().1;
+ gameplay.state = GameplayState::Won;
+ }
+}
diff --git a/code/rust-sokoban-c02-05/src/systems/gameplay_state_system.rs b/code/rust-sokoban-c02-05/src/systems/gameplay_state_system.rs
deleted file mode 100644
index 6593003..0000000
--- a/code/rust-sokoban-c02-05/src/systems/gameplay_state_system.rs
+++ /dev/null
@@ -1,44 +0,0 @@
-use specs::{Join, ReadStorage, System, Write};
-use std::collections::HashMap;
-
-use crate::{
- components::{Box, BoxSpot, Position},
- resources::{Gameplay, GameplayState},
-};
-
-pub struct GameplayStateSystem {}
-
-impl<'a> System<'a> for GameplayStateSystem {
- // Data
- type SystemData = (
- Write<'a, Gameplay>,
- ReadStorage<'a, Position>,
- ReadStorage<'a, Box>,
- ReadStorage<'a, BoxSpot>,
- );
-
- fn run(&mut self, data: Self::SystemData) {
- let (mut gameplay_state, positions, boxes, box_spots) = data;
-
- // get all boxes indexed by position
- let boxes_by_position: HashMap<(u8, u8), &Box> = (&positions, &boxes)
- .join()
- .map(|t| ((t.0.x, t.0.y), t.1))
- .collect::>();
-
- // loop through all box spots and check if there is a corresponding
- // box at that position
- for (_box_spot, position) in (&box_spots, &positions).join() {
- if boxes_by_position.contains_key(&(position.x, position.y)) {
- // continue
- } else {
- gameplay_state.state = GameplayState::Playing;
- return;
- }
- }
-
- // If we made it this far, then all box spots have boxes on them, and the
- // game has been won
- gameplay_state.state = GameplayState::Won;
- }
-}
diff --git a/code/rust-sokoban-c02-05/src/systems/input.rs b/code/rust-sokoban-c02-05/src/systems/input.rs
new file mode 100644
index 0000000..ee423ff
--- /dev/null
+++ b/code/rust-sokoban-c02-05/src/systems/input.rs
@@ -0,0 +1,109 @@
+use ggez::{
+ conf, event,
+ graphics::{self, DrawParam, Image},
+ input::keyboard,
+ input::keyboard::{KeyCode, KeyInput},
+ Context, GameResult,
+};
+use glam::Vec2;
+use hecs::{Entity, World};
+
+use std::collections::HashMap;
+use std::path;
+
+use crate::components::*;
+use crate::constants::*;
+
+pub fn run_input(world: &World, context: &mut Context) {
+ let mut to_move: Vec<(Entity, KeyCode)> = Vec::new();
+
+ // get all the movables and immovables
+ let mov: HashMap<(u8, u8), Entity> = world
+ .query::<(&Position, &Movable)>()
+ .iter()
+ .map(|t| ((t.1 .0.x, t.1 .0.y), t.0.clone()))
+ .collect::>();
+ let immov: HashMap<(u8, u8), Entity> = world
+ .query::<(&Position, &Immovable)>()
+ .iter()
+ .map(|t| ((t.1 .0.x, t.1 .0.y), t.0.clone()))
+ .collect::>();
+
+ for (_, (position, _player)) in world.query::<(&mut Position, &Player)>().iter() {
+ if keyboard::is_key_repeated(context) {
+ continue;
+ }
+
+ // Now iterate through current position to the end of the map
+ // on the correct axis and check what needs to move.
+ let key = if keyboard::is_key_just_pressed(context, KeyCode::Up) {
+ KeyCode::Up
+ } else if keyboard::is_key_just_pressed(context, KeyCode::Down) {
+ KeyCode::Down
+ } else if keyboard::is_key_just_pressed(context, KeyCode::Left) {
+ KeyCode::Left
+ } else if keyboard::is_key_just_pressed(context, KeyCode::Right) {
+ KeyCode::Right
+ } else {
+ continue;
+ };
+
+ let (start, end, is_x) = match key {
+ KeyCode::Up => (position.y, 0, false),
+ KeyCode::Down => (position.y, MAP_HEIGHT - 1, false),
+ KeyCode::Left => (position.x, 0, true),
+ KeyCode::Right => (position.x, MAP_WIDTH - 1, true),
+ _ => continue,
+ };
+
+ let range = if start < end {
+ (start..=end).collect::>()
+ } else {
+ (end..=start).rev().collect::>()
+ };
+
+ for x_or_y in range {
+ let pos = if is_x {
+ (x_or_y, position.y)
+ } else {
+ (position.x, x_or_y)
+ };
+
+ // find a movable
+ // if it exists, we try to move it and continue
+ // if it doesn't exist, we continue and try to find an immovable instead
+ match mov.get(&pos) {
+ Some(entity) => to_move.push((*entity, key)),
+ None => {
+ // find an immovable
+ // if it exists, we need to stop and not move anything
+ // if it doesn't exist, we stop because we found a gap
+ match immov.get(&pos) {
+ Some(_id) => to_move.clear(),
+ None => break,
+ }
+ }
+ }
+ }
+ }
+
+ // Update gameplay moves
+ if to_move.len() > 0 {
+ let mut query = world.query::<&mut Gameplay>();
+ let mut gameplay = query.iter().next().unwrap().1;
+ gameplay.moves_count += 1;
+ }
+
+ // Now actually move what needs to be moved
+ for (entity, key) in to_move {
+ let mut position = world.get::<&mut Position>(entity).unwrap();
+
+ match key {
+ KeyCode::Up => position.y -= 1,
+ KeyCode::Down => position.y += 1,
+ KeyCode::Left => position.x -= 1,
+ KeyCode::Right => position.x += 1,
+ _ => (),
+ }
+ }
+}
diff --git a/code/rust-sokoban-c02-05/src/systems/input_system.rs b/code/rust-sokoban-c02-05/src/systems/input_system.rs
deleted file mode 100644
index 74ef39a..0000000
--- a/code/rust-sokoban-c02-05/src/systems/input_system.rs
+++ /dev/null
@@ -1,103 +0,0 @@
-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 std::collections::HashMap;
-
-pub struct InputSystem {}
-
-// System implementation
-impl<'a> System<'a> for InputSystem {
- // Data
- type SystemData = (
- Write<'a, InputQueue>,
- Write<'a, Gameplay>,
- Entities<'a>,
- WriteStorage<'a, Position>,
- ReadStorage<'a, Player>,
- ReadStorage<'a, Movable>,
- ReadStorage<'a, Immovable>,
- );
-
- fn run(&mut self, data: Self::SystemData) {
- let (mut input_queue, mut gameplay, entities, mut positions, players, movables, immovables) = data;
-
- let mut to_move = Vec::new();
-
- for (position, _player) in (&positions, &players).join() {
- // Get the first key pressed
- if let Some(key) = input_queue.keys_pressed.pop() {
- // get all the movables and immovables
- let mov: HashMap<(u8, u8), Index> = (&entities, &movables, &positions)
- .join()
- .map(|t| ((t.2.x, t.2.y), t.0.id()))
- .collect::>();
- let immov: HashMap<(u8, u8), Index> = (&entities, &immovables, &positions)
- .join()
- .map(|t| ((t.2.x, t.2.y), t.0.id()))
- .collect::>();
-
- // 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),
- _ => continue,
- };
-
- let range = if start < end {
- (start..=end).collect::>()
- } else {
- (end..=start).rev().collect::>()
- };
-
- for x_or_y in range {
- let pos = if is_x {
- (x_or_y, position.y)
- } else {
- (position.x, x_or_y)
- };
-
- // find a movable
- // if it exists, we try to move it and continue
- // if it doesn't exist, we continue and try to find an immovable instead
- match mov.get(&pos) {
- Some(id) => to_move.push((key, id.clone())),
- None => {
- // find an immovable
- // if it exists, we need to stop and not move anything
- // if it doesn't exist, we stop because we found a gap
- match immov.get(&pos) {
- Some(_id) => to_move.clear(),
- None => break,
- }
- }
- }
- }
- }
- }
-
- // We've just moved, so let's increase the number of moves
- if to_move.len() > 0 {
- gameplay.moves_count += 1;
- }
-
- // Now actually move what needs to be moved
- for (key, id) in to_move {
- 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,
- _ => (),
- }
- }
- }
- }
-}
diff --git a/code/rust-sokoban-c02-05/src/systems/mod.rs b/code/rust-sokoban-c02-05/src/systems/mod.rs
index 36ecda1..fe57a1a 100644
--- a/code/rust-sokoban-c02-05/src/systems/mod.rs
+++ b/code/rust-sokoban-c02-05/src/systems/mod.rs
@@ -1,7 +1,3 @@
-mod input_system;
-mod rendering_system;
-mod gameplay_state_system;
-
-pub use self::input_system::InputSystem;
-pub use self::rendering_system::RenderingSystem;
-pub use self::gameplay_state_system::GameplayStateSystem;
+pub mod gameplay;
+pub mod input;
+pub mod rendering;
diff --git a/code/rust-sokoban-c02-05/src/systems/rendering.rs b/code/rust-sokoban-c02-05/src/systems/rendering.rs
new file mode 100644
index 0000000..f976a28
--- /dev/null
+++ b/code/rust-sokoban-c02-05/src/systems/rendering.rs
@@ -0,0 +1,64 @@
+use ggez::{
+ conf, event,
+ graphics::{self, Canvas, Color, DrawParam, Image, PxScale, Text, TextFragment},
+ input::keyboard::{self, KeyCode},
+ Context, GameResult,
+};
+use glam::Vec2;
+use hecs::{Entity, World};
+
+use std::collections::HashMap;
+use std::path;
+
+use crate::components::*;
+use crate::constants::*;
+
+pub fn run_rendering(world: &World, context: &mut Context) {
+ // Clearing the screen (this gives us the background colour)
+ let mut canvas =
+ graphics::Canvas::from_frame(context, graphics::Color::from([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.
+ let mut query = world.query::<(&Position, &Renderable)>();
+ let mut rendering_data: Vec<(Entity, (&Position, &Renderable))> = query.into_iter().collect();
+ rendering_data.sort_by_key(|&k| k.1 .0.z);
+
+ // Iterate through all pairs of positions & renderables, load the image
+ // and draw it at the specified position.
+ for (_, (position, renderable)) in rendering_data.iter() {
+ // Load the image
+ let image = Image::from_path(context, renderable.path.clone()).unwrap();
+ 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));
+ canvas.draw(&image, draw_params);
+ }
+
+ // ANCHOR: draw_gameplay_state
+ // Render any text
+ let mut query = world.query::<&Gameplay>();
+ let gameplay = query.iter().next().unwrap().1;
+ draw_text(&mut canvas, &gameplay.state.to_string(), 525.0, 80.0);
+ draw_text(&mut canvas, &gameplay.moves_count.to_string(), 525.0, 100.0);
+ // ANCHOR_END: draw_gameplay_state
+
+ // Finally, present the canvas, this will actually display everything
+ // on the screen.
+ canvas.finish(context).expect("expected to present");
+}
+
+// ANCHOR: draw_text
+pub fn draw_text(canvas: &mut Canvas, text_string: &str, x: f32, y: f32) {
+ let mut text = Text::new(TextFragment {
+ text: text_string.to_string(),
+ color: Some(Color::new(0.0, 0.0, 0.0, 1.0)),
+ scale: Some(PxScale::from(20.0)),
+ ..Default::default()
+ });
+
+ canvas.draw(&text, Vec2::new(x, y));
+}
+// ANCHOR_END: draw_text
diff --git a/code/rust-sokoban-c02-05/src/systems/rendering_system.rs b/code/rust-sokoban-c02-05/src/systems/rendering_system.rs
deleted file mode 100644
index 2942a42..0000000
--- a/code/rust-sokoban-c02-05/src/systems/rendering_system.rs
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
-
-use crate::components::*;
-use crate::resources::*;
-use crate::constants::TILE_WIDTH;
-
-use ggez::{Context, graphics::{self, DrawParam, Image, Color}};
-use specs::{Join, ReadStorage, System, Read};
-use glam::Vec2;
-
-pub struct RenderingSystem<'a> {
- pub context: &'a mut Context,
-}
-
-impl RenderingSystem<'_> {
- pub fn draw_text(&mut self, 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);
-
- graphics::queue_text(self.context, &text, dimensions, color);
- graphics::draw_queued_text(
- self.context,
- graphics::DrawParam::new().dest(destination),
- None,
- graphics::FilterMode::Linear,
- )
- .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>);
-
- 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));
-
- // Get all the renderables with their positions and sort by the position z
- // This will allow us to have entities layered visually.
- let mut rendering_data = (&positions, &renderables).join().collect::>();
- rendering_data.sort_by_key(|&k| k.0.z);
-
- // Iterate through all pairs of positions & renderables, load the image
- // 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;
-
- // draw
- let draw_params = DrawParam::new().dest(Vec2::new(x, y));
- graphics::draw(self.context, &image, draw_params).expect("expected render");
- }
-
- // 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);
-
- // Finally, present the context, this will actually display everything
- // on the screen.
- graphics::present(self.context).expect("expected to present");
- }
-}
diff --git a/code/rust-sokoban-c03-01/Cargo.toml b/code/rust-sokoban-c03-01/Cargo.toml
index 92e5cac..d39b812 100644
--- a/code/rust-sokoban-c03-01/Cargo.toml
+++ b/code/rust-sokoban-c03-01/Cargo.toml
@@ -7,6 +7,6 @@ edition = "2018"
# 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.9.3"
+glam = { version = "0.24", features = ["mint"] }
+hecs = "0.10.5"
\ 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..da565c8 100644
--- a/code/rust-sokoban-c03-01/src/components.rs
+++ b/code/rust-sokoban-c03-01/src/components.rs
@@ -1,36 +1,32 @@
-use specs::{Component, NullStorage, VecStorage, World, WorldExt};
+use std::fmt;
+use std::fmt::Display;
-use std::fmt::{self, Display};
-
-// Components
-#[derive(Debug, Component, Clone, Copy)]
-#[storage(VecStorage)]
+#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Position {
pub x: u8,
pub y: u8,
pub z: u8,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Renderable {
pub path: String,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Wall {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Player {}
+// ANCHOR: box_colour_eq
#[derive(PartialEq)]
+// ANCHOR: box_colour
pub enum BoxColour {
Red,
Blue,
}
+// ANCHOR_END: box_colour
+// ANCHOR_END: box_colour_eq
+// ANCHOR: box_colour_display
impl Display for BoxColour {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str(match self {
@@ -40,34 +36,45 @@ impl Display for BoxColour {
Ok(())
}
}
+// ANCHOR_END: box_colour_display
-#[derive(Component)]
-#[storage(VecStorage)]
+// ANCHOR: box
pub struct Box {
pub colour: BoxColour,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct BoxSpot {
pub colour: BoxColour,
}
+// ANCHOR_END: box
-#[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::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
+pub enum GameplayState {
+ Playing,
+ Won,
+}
+
+impl Default for GameplayState {
+ fn default() -> Self {
+ GameplayState::Playing
+ }
+}
+
+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(())
+ }
+}
+
+#[derive(Default)]
+pub struct Gameplay {
+ pub state: GameplayState,
+ pub moves_count: u32,
}
diff --git a/code/rust-sokoban-c03-01/src/entities.rs b/code/rust-sokoban-c03-01/src/entities.rs
index 12a7823..3cf6108 100644
--- a/code/rust-sokoban-c03-01/src/entities.rs
+++ b/code/rust-sokoban-c03-01/src/entities.rs
@@ -1,60 +1,60 @@
use crate::components::*;
-use specs::{Builder, World, WorldExt};
+use hecs::{Entity, World};
-// Create a wall entity
-pub fn create_wall(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
+pub fn create_wall(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
path: "/images/wall.png".to_string(),
- })
- .with(Wall {})
- .with(Immovable)
- .build();
+ },
+ Wall {},
+ Immovable {},
+ ))
}
-pub fn create_floor(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 5, ..position })
- .with(Renderable {
+pub fn create_floor(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 5, ..position },
+ Renderable {
path: "/images/floor.png".to_string(),
- })
- .build();
+ },
+ ))
}
-pub fn create_box(world: &mut World, position: Position, colour: BoxColour) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
+// ANCHOR: create_box
+pub fn create_box(world: &mut World, position: Position, colour: BoxColour) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
path: format!("/images/box_{}.png", colour),
- })
- .with(Box { colour })
- .with(Movable)
- .build();
+ },
+ Box { colour },
+ Movable {},
+ ))
}
-pub fn create_box_spot(world: &mut World, position: Position, colour: BoxColour) {
- world
- .create_entity()
- .with(Position { z: 9, ..position })
- .with(Renderable {
+pub fn create_box_spot(world: &mut World, position: Position, colour: BoxColour) -> Entity {
+ world.spawn((
+ Position { z: 9, ..position },
+ Renderable {
path: format!("/images/box_spot_{}.png", colour),
- })
- .with(BoxSpot { colour })
- .build();
+ },
+ BoxSpot { colour },
+ ))
}
+// ANCHOR_END: create_box
-pub fn create_player(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable {
+pub fn create_player(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable {
path: "/images/player.png".to_string(),
- })
- .with(Player {})
- .with(Movable)
- .build();
+ },
+ Player {},
+ Movable {},
+ ))
+}
+
+pub fn create_gameplay(world: &mut World) -> Entity {
+ world.spawn((Gameplay::default(),))
}
diff --git a/code/rust-sokoban-c03-01/src/main.rs b/code/rust-sokoban-c03-01/src/main.rs
index 8336960..691235a 100644
--- a/code/rust-sokoban-c03-01/src/main.rs
+++ b/code/rust-sokoban-c03-01/src/main.rs
@@ -1,38 +1,46 @@
+/* ANCHOR: all */
// Rust sokoban
// main.rs
-use ggez::{conf, event::{self, KeyCode, KeyMods}, Context, GameResult};
-use specs::{RunNow, World, WorldExt};
+use ggez::{
+ conf, event,
+ graphics::{self, DrawParam, Image},
+ input::keyboard,
+ input::keyboard::{KeyCode, KeyInput},
+ Context, GameResult,
+};
+use glam::Vec2;
+use hecs::{Entity, World};
+
+use std::collections::HashMap;
use std::path;
mod components;
mod constants;
mod entities;
mod map;
-mod resources;
mod systems;
-use crate::components::*;
-use crate::map::*;
-use crate::resources::*;
-use crate::systems::*;
-
+// ANCHOR: game
+// This struct will hold all our game state
+// For now there is nothing to be held, but we'll add
+// things shortly.
struct Game {
world: World,
}
+// ANCHOR_END: game
+// ANCHOR: handler
impl event::EventHandler for Game {
- fn update(&mut self, _context: &mut Context) -> GameResult {
+ fn update(&mut self, context: &mut Context) -> GameResult {
// Run input system
{
- let mut is = InputSystem {};
- is.run_now(&self.world);
+ systems::input::run_input(&self.world, context);
}
- // Run gameplay state system
+ // Run gameplay state
{
- let mut gss = GameplayStateSystem {};
- gss.run_now(&self.world);
+ systems::gameplay::run_gameplay_state(&mut self.world);
}
Ok(())
@@ -41,49 +49,19 @@ impl event::EventHandler for Game {
fn draw(&mut self, context: &mut Context) -> GameResult {
// Render game entities
{
- let mut rs = RenderingSystem { context };
- rs.run_now(&self.world);
+ systems::rendering::run_rendering(&self.world, context);
}
Ok(())
}
-
- fn key_down_event(
- &mut self,
- _context: &mut Context,
- keycode: KeyCode,
- _keymod: KeyMods,
- _repeat: bool,
- ) {
- println!("Key pressed: {:?}", keycode);
-
- let mut input_queue = self.world.write_resource::();
- input_queue.keys_pressed.push(keycode);
- }
-}
-
-// 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 . . . BB . . W
- W . . RB . . . W
- W . P . . . . W
- W . . . . RS . W
- W . . BS . . . W
- W . . . . . . W
- W W W W W W W W
- ";
-
- load_map(world, MAP.to_string());
}
+// ANCHOR_END: handler
+// ANCHOR: main
pub fn main() -> GameResult {
let mut world = World::new();
- register_components(&mut world);
- register_resources(&mut world);
- initialize_level(&mut world);
+ map::initialize_level(&mut world);
+ entities::create_gameplay(&mut world);
// Create a game context and event loop
let context_builder = ggez::ContextBuilder::new("rust_sokoban", "sokoban")
@@ -98,3 +76,6 @@ pub fn main() -> GameResult {
// Run the main event loop
event::run(context, event_loop, game)
}
+// ANCHOR_END: main
+
+/* ANCHOR_END: all */
diff --git a/code/rust-sokoban-c03-01/src/map.rs b/code/rust-sokoban-c03-01/src/map.rs
index 65caa3e..34adf76 100644
--- a/code/rust-sokoban-c03-01/src/map.rs
+++ b/code/rust-sokoban-c03-01/src/map.rs
@@ -1,6 +1,24 @@
use crate::components::{BoxColour, Position};
use crate::entities::*;
-use specs::World;
+use hecs::World;
+
+// ANCHOR: initialize_level
+pub fn initialize_level(world: &mut World) {
+ const MAP: &str = "
+ N N W W W W W W
+ W W W . . . . W
+ W . . . BB . . W
+ W . . RB . . . W
+ W . P . . . . W
+ W . . . . RS . W
+ W . . BS . . . W
+ W . . . . . . W
+ W W W W W W W W
+ ";
+
+ load_map(world, MAP.to_string());
+}
+// ANCHOR_END: initialize_level
pub fn load_map(world: &mut World, map_string: String) {
// read all lines
@@ -18,8 +36,11 @@ pub fn load_map(world: &mut World, map_string: String) {
};
// Figure out what object we should create
+ // ANCHOR: map_match
match *column {
- "." => create_floor(world, position),
+ "." => {
+ create_floor(world, position);
+ }
"W" => {
create_floor(world, position);
create_wall(world, position);
@@ -47,6 +68,7 @@ pub fn load_map(world: &mut World, map_string: String) {
"N" => (),
c => panic!("unrecognized map item {}", c),
}
+ // ANCHOR_END: map_match
}
}
}
diff --git a/code/rust-sokoban-c03-01/src/resources.rs b/code/rust-sokoban-c03-01/src/resources.rs
deleted file mode 100644
index b319be1..0000000
--- a/code/rust-sokoban-c03-01/src/resources.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use ggez::event::KeyCode;
-use specs::World;
-
-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 enum GameplayState {
- Playing,
- 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(())
- }
-}
-
-impl Default for GameplayState {
- fn default() -> Self {
- Self::Playing
- }
-}
-
-#[derive(Default)]
-pub struct Gameplay {
- pub state: GameplayState,
- pub moves_count: u32
-}
diff --git a/code/rust-sokoban-c03-01/src/systems/gameplay.rs b/code/rust-sokoban-c03-01/src/systems/gameplay.rs
new file mode 100644
index 0000000..3993878
--- /dev/null
+++ b/code/rust-sokoban-c03-01/src/systems/gameplay.rs
@@ -0,0 +1,41 @@
+use crate::components::*;
+use hecs::World;
+
+use std::collections::HashMap;
+
+pub fn run_gameplay_state(world: &World) {
+ // get all boxes indexed by position
+ let mut query = world.query::<(&Position, &Box)>();
+ let boxes_by_position: HashMap<(u8, u8), &Box> = query
+ .iter()
+ .map(|(e, t)| ((t.0.x, t.0.y), t.1))
+ .collect::>();
+
+ // loop through all box spots and check if there is a corresponding
+ // box at that position
+ let boxes_out_of_position: usize = world
+ .query::<(&Position, &BoxSpot)>()
+ .iter()
+ .map(|(_, (position, box_spot))| {
+ if let Some(the_box) = boxes_by_position.get(&(position.x, position.y)) {
+ if box_spot.colour == the_box.colour {
+ 0
+ } else {
+ 1
+ }
+ } else {
+ 0
+ }
+ })
+ .collect::>()
+ .into_iter()
+ .sum();
+
+ // If we made it this far, then all box spots have boxes on them, and the
+ // game has been won
+ if boxes_out_of_position == 0 {
+ let mut query = world.query::<&mut Gameplay>();
+ let mut gameplay = query.iter().next().unwrap().1;
+ gameplay.state = GameplayState::Won;
+ }
+}
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
deleted file mode 100644
index 9acf34f..0000000
--- a/code/rust-sokoban-c03-01/src/systems/gameplay_state_system.rs
+++ /dev/null
@@ -1,51 +0,0 @@
-use specs::{Join, ReadStorage, System, Write};
-use std::collections::HashMap;
-
-use crate::{
- components::{Box, BoxSpot, Position},
- resources::{Gameplay, GameplayState},
-};
-
-pub struct GameplayStateSystem {}
-
-impl<'a> System<'a> for GameplayStateSystem {
- // Data
- type SystemData = (
- Write<'a, Gameplay>,
- ReadStorage<'a, Position>,
- ReadStorage<'a, Box>,
- ReadStorage<'a, BoxSpot>,
- );
-
- fn run(&mut self, data: Self::SystemData) {
- let (mut gameplay_state, positions, boxes, box_spots) = data;
-
- // get all boxes indexed by position
- let boxes_by_position: HashMap<(u8, u8), &Box> = (&positions, &boxes)
- .join()
- .map(|t| ((t.0.x, t.0.y), t.1))
- .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.
- 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 {
- // continue
- } else {
- // return, haven't won yet
- return;
- }
- } else {
- gameplay_state.state = GameplayState::Playing;
- return;
- }
- }
-
- // If we made it this far, then all box spots have boxes on them, and the
- // game has been won
- gameplay_state.state = GameplayState::Won;
- }
-}
diff --git a/code/rust-sokoban-c03-01/src/systems/input.rs b/code/rust-sokoban-c03-01/src/systems/input.rs
new file mode 100644
index 0000000..ee423ff
--- /dev/null
+++ b/code/rust-sokoban-c03-01/src/systems/input.rs
@@ -0,0 +1,109 @@
+use ggez::{
+ conf, event,
+ graphics::{self, DrawParam, Image},
+ input::keyboard,
+ input::keyboard::{KeyCode, KeyInput},
+ Context, GameResult,
+};
+use glam::Vec2;
+use hecs::{Entity, World};
+
+use std::collections::HashMap;
+use std::path;
+
+use crate::components::*;
+use crate::constants::*;
+
+pub fn run_input(world: &World, context: &mut Context) {
+ let mut to_move: Vec<(Entity, KeyCode)> = Vec::new();
+
+ // get all the movables and immovables
+ let mov: HashMap<(u8, u8), Entity> = world
+ .query::<(&Position, &Movable)>()
+ .iter()
+ .map(|t| ((t.1 .0.x, t.1 .0.y), t.0.clone()))
+ .collect::>();
+ let immov: HashMap<(u8, u8), Entity> = world
+ .query::<(&Position, &Immovable)>()
+ .iter()
+ .map(|t| ((t.1 .0.x, t.1 .0.y), t.0.clone()))
+ .collect::>();
+
+ for (_, (position, _player)) in world.query::<(&mut Position, &Player)>().iter() {
+ if keyboard::is_key_repeated(context) {
+ continue;
+ }
+
+ // Now iterate through current position to the end of the map
+ // on the correct axis and check what needs to move.
+ let key = if keyboard::is_key_just_pressed(context, KeyCode::Up) {
+ KeyCode::Up
+ } else if keyboard::is_key_just_pressed(context, KeyCode::Down) {
+ KeyCode::Down
+ } else if keyboard::is_key_just_pressed(context, KeyCode::Left) {
+ KeyCode::Left
+ } else if keyboard::is_key_just_pressed(context, KeyCode::Right) {
+ KeyCode::Right
+ } else {
+ continue;
+ };
+
+ let (start, end, is_x) = match key {
+ KeyCode::Up => (position.y, 0, false),
+ KeyCode::Down => (position.y, MAP_HEIGHT - 1, false),
+ KeyCode::Left => (position.x, 0, true),
+ KeyCode::Right => (position.x, MAP_WIDTH - 1, true),
+ _ => continue,
+ };
+
+ let range = if start < end {
+ (start..=end).collect::>()
+ } else {
+ (end..=start).rev().collect::>()
+ };
+
+ for x_or_y in range {
+ let pos = if is_x {
+ (x_or_y, position.y)
+ } else {
+ (position.x, x_or_y)
+ };
+
+ // find a movable
+ // if it exists, we try to move it and continue
+ // if it doesn't exist, we continue and try to find an immovable instead
+ match mov.get(&pos) {
+ Some(entity) => to_move.push((*entity, key)),
+ None => {
+ // find an immovable
+ // if it exists, we need to stop and not move anything
+ // if it doesn't exist, we stop because we found a gap
+ match immov.get(&pos) {
+ Some(_id) => to_move.clear(),
+ None => break,
+ }
+ }
+ }
+ }
+ }
+
+ // Update gameplay moves
+ if to_move.len() > 0 {
+ let mut query = world.query::<&mut Gameplay>();
+ let mut gameplay = query.iter().next().unwrap().1;
+ gameplay.moves_count += 1;
+ }
+
+ // Now actually move what needs to be moved
+ for (entity, key) in to_move {
+ let mut position = world.get::<&mut Position>(entity).unwrap();
+
+ match key {
+ KeyCode::Up => position.y -= 1,
+ KeyCode::Down => position.y += 1,
+ KeyCode::Left => position.x -= 1,
+ KeyCode::Right => position.x += 1,
+ _ => (),
+ }
+ }
+}
diff --git a/code/rust-sokoban-c03-01/src/systems/input_system.rs b/code/rust-sokoban-c03-01/src/systems/input_system.rs
deleted file mode 100644
index 74ef39a..0000000
--- a/code/rust-sokoban-c03-01/src/systems/input_system.rs
+++ /dev/null
@@ -1,103 +0,0 @@
-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 std::collections::HashMap;
-
-pub struct InputSystem {}
-
-// System implementation
-impl<'a> System<'a> for InputSystem {
- // Data
- type SystemData = (
- Write<'a, InputQueue>,
- Write<'a, Gameplay>,
- Entities<'a>,
- WriteStorage<'a, Position>,
- ReadStorage<'a, Player>,
- ReadStorage<'a, Movable>,
- ReadStorage<'a, Immovable>,
- );
-
- fn run(&mut self, data: Self::SystemData) {
- let (mut input_queue, mut gameplay, entities, mut positions, players, movables, immovables) = data;
-
- let mut to_move = Vec::new();
-
- for (position, _player) in (&positions, &players).join() {
- // Get the first key pressed
- if let Some(key) = input_queue.keys_pressed.pop() {
- // get all the movables and immovables
- let mov: HashMap<(u8, u8), Index> = (&entities, &movables, &positions)
- .join()
- .map(|t| ((t.2.x, t.2.y), t.0.id()))
- .collect::>();
- let immov: HashMap<(u8, u8), Index> = (&entities, &immovables, &positions)
- .join()
- .map(|t| ((t.2.x, t.2.y), t.0.id()))
- .collect::>();
-
- // 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),
- _ => continue,
- };
-
- let range = if start < end {
- (start..=end).collect::>()
- } else {
- (end..=start).rev().collect::>()
- };
-
- for x_or_y in range {
- let pos = if is_x {
- (x_or_y, position.y)
- } else {
- (position.x, x_or_y)
- };
-
- // find a movable
- // if it exists, we try to move it and continue
- // if it doesn't exist, we continue and try to find an immovable instead
- match mov.get(&pos) {
- Some(id) => to_move.push((key, id.clone())),
- None => {
- // find an immovable
- // if it exists, we need to stop and not move anything
- // if it doesn't exist, we stop because we found a gap
- match immov.get(&pos) {
- Some(_id) => to_move.clear(),
- None => break,
- }
- }
- }
- }
- }
- }
-
- // We've just moved, so let's increase the number of moves
- if to_move.len() > 0 {
- gameplay.moves_count += 1;
- }
-
- // Now actually move what needs to be moved
- for (key, id) in to_move {
- 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,
- _ => (),
- }
- }
- }
- }
-}
diff --git a/code/rust-sokoban-c03-01/src/systems/mod.rs b/code/rust-sokoban-c03-01/src/systems/mod.rs
index 36ecda1..fe57a1a 100644
--- a/code/rust-sokoban-c03-01/src/systems/mod.rs
+++ b/code/rust-sokoban-c03-01/src/systems/mod.rs
@@ -1,7 +1,3 @@
-mod input_system;
-mod rendering_system;
-mod gameplay_state_system;
-
-pub use self::input_system::InputSystem;
-pub use self::rendering_system::RenderingSystem;
-pub use self::gameplay_state_system::GameplayStateSystem;
+pub mod gameplay;
+pub mod input;
+pub mod rendering;
diff --git a/code/rust-sokoban-c03-01/src/systems/rendering.rs b/code/rust-sokoban-c03-01/src/systems/rendering.rs
new file mode 100644
index 0000000..b94b56e
--- /dev/null
+++ b/code/rust-sokoban-c03-01/src/systems/rendering.rs
@@ -0,0 +1,60 @@
+use ggez::{
+ conf, event,
+ graphics::{self, Canvas, Color, DrawParam, Image, PxScale, Text, TextFragment},
+ input::keyboard::{self, KeyCode},
+ Context, GameResult,
+};
+use glam::Vec2;
+use hecs::{Entity, World};
+
+use std::collections::HashMap;
+use std::path;
+
+use crate::components::*;
+use crate::constants::*;
+
+pub fn run_rendering(world: &World, context: &mut Context) {
+ // Clearing the screen (this gives us the background colour)
+ let mut canvas =
+ graphics::Canvas::from_frame(context, graphics::Color::from([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.
+ let mut query = world.query::<(&Position, &Renderable)>();
+ let mut rendering_data: Vec<(Entity, (&Position, &Renderable))> = query.into_iter().collect();
+ rendering_data.sort_by_key(|&k| k.1 .0.z);
+
+ // Iterate through all pairs of positions & renderables, load the image
+ // and draw it at the specified position.
+ for (_, (position, renderable)) in rendering_data.iter() {
+ // Load the image
+ let image = Image::from_path(context, renderable.path.clone()).unwrap();
+ 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));
+ canvas.draw(&image, draw_params);
+ }
+
+ // Render any text
+ let mut query = world.query::<&Gameplay>();
+ let gameplay = query.iter().next().unwrap().1;
+ draw_text(&mut canvas, &gameplay.state.to_string(), 525.0, 80.0);
+ draw_text(&mut canvas, &gameplay.moves_count.to_string(), 525.0, 100.0);
+
+ // Finally, present the canvas, this will actually display everything
+ // on the screen.
+ canvas.finish(context).expect("expected to present");
+}
+
+pub fn draw_text(canvas: &mut Canvas, text_string: &str, x: f32, y: f32) {
+ let mut text = Text::new(TextFragment {
+ text: text_string.to_string(),
+ color: Some(Color::new(0.0, 0.0, 0.0, 1.0)),
+ scale: Some(PxScale::from(20.0)),
+ ..Default::default()
+ });
+
+ canvas.draw(&text, Vec2::new(x, y));
+}
diff --git a/code/rust-sokoban-c03-01/src/systems/rendering_system.rs b/code/rust-sokoban-c03-01/src/systems/rendering_system.rs
deleted file mode 100644
index 2942a42..0000000
--- a/code/rust-sokoban-c03-01/src/systems/rendering_system.rs
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
-
-use crate::components::*;
-use crate::resources::*;
-use crate::constants::TILE_WIDTH;
-
-use ggez::{Context, graphics::{self, DrawParam, Image, Color}};
-use specs::{Join, ReadStorage, System, Read};
-use glam::Vec2;
-
-pub struct RenderingSystem<'a> {
- pub context: &'a mut Context,
-}
-
-impl RenderingSystem<'_> {
- pub fn draw_text(&mut self, 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);
-
- graphics::queue_text(self.context, &text, dimensions, color);
- graphics::draw_queued_text(
- self.context,
- graphics::DrawParam::new().dest(destination),
- None,
- graphics::FilterMode::Linear,
- )
- .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>);
-
- 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));
-
- // Get all the renderables with their positions and sort by the position z
- // This will allow us to have entities layered visually.
- let mut rendering_data = (&positions, &renderables).join().collect::>();
- rendering_data.sort_by_key(|&k| k.0.z);
-
- // Iterate through all pairs of positions & renderables, load the image
- // 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;
-
- // draw
- let draw_params = DrawParam::new().dest(Vec2::new(x, y));
- graphics::draw(self.context, &image, draw_params).expect("expected render");
- }
-
- // 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);
-
- // Finally, present the context, this will actually display everything
- // on the screen.
- graphics::present(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..d39b812 100644
--- a/code/rust-sokoban-c03-02/Cargo.toml
+++ b/code/rust-sokoban-c03-02/Cargo.toml
@@ -7,6 +7,6 @@ edition = "2018"
# 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.9.3"
+glam = { version = "0.24", features = ["mint"] }
+hecs = "0.10.5"
\ No newline at end of file
diff --git a/code/rust-sokoban-c03-02/src/components.rs b/code/rust-sokoban-c03-02/src/components.rs
index 9f4f26d..4091d46 100644
--- a/code/rust-sokoban-c03-02/src/components.rs
+++ b/code/rust-sokoban-c03-02/src/components.rs
@@ -1,36 +1,43 @@
-use specs::{Component, NullStorage, VecStorage, World, WorldExt};
+use std::fmt;
+use std::fmt::Display;
+use std::time::Duration;
-use std::fmt::{self, Display};
-
-// Components
-#[derive(Debug, Component, Clone, Copy)]
-#[storage(VecStorage)]
+#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Position {
pub x: u8,
pub y: u8,
pub z: u8,
}
+// ANCHOR: renderable
+pub struct Renderable {
+ paths: Vec,
+}
+// ANCHOR_END: renderable
+
+// ANCHOR: renderable_kind
pub enum RenderableKind {
Static,
Animated,
}
-
-#[derive(Component)]
-#[storage(VecStorage)]
-pub struct Renderable {
- paths: Vec,
-}
+// ANCHOR_END: renderable_kind
impl Renderable {
- pub fn new_static(path: String) -> Self {
- Self { paths: vec![path] }
+ // ANCHOR: renderable_new_fn
+ pub fn new_static(path: &str) -> Self {
+ Self {
+ paths: vec![path.to_string()],
+ }
}
- pub fn new_animated(paths: Vec) -> Self {
- Self { paths }
+ pub fn new_animated(paths: Vec<&str>) -> Self {
+ Self {
+ paths: paths.iter().map(|p| p.to_string()).collect(),
+ }
}
+ // ANCHOR_END: renderable_new_fn
+ // ANCHOR: renderable_kind_fn
pub fn kind(&self) -> RenderableKind {
match self.paths.len() {
0 => panic!("invalid renderable"),
@@ -38,21 +45,20 @@ impl Renderable {
_ => RenderableKind::Animated,
}
}
+ // ANCHOR_END: renderable_kind_fn
+ // ANCHOR: renderable_path_fn
pub fn path(&self, path_index: usize) -> String {
// If we get asked for a path that is larger than the
// number of paths we actually have, we simply mod the index
// with the length to get an index that is in range.
self.paths[path_index % self.paths.len()].clone()
}
+ // ANCHOR_END: renderable_path_fn
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Wall {}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Player {}
#[derive(PartialEq)]
@@ -71,33 +77,46 @@ impl Display for BoxColour {
}
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct Box {
pub colour: BoxColour,
}
-#[derive(Component)]
-#[storage(VecStorage)]
pub struct BoxSpot {
pub colour: BoxColour,
}
-#[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::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
- world.register::();
+pub enum GameplayState {
+ Playing,
+ Won,
+}
+
+impl Default for GameplayState {
+ fn default() -> Self {
+ GameplayState::Playing
+ }
+}
+
+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(())
+ }
+}
+
+#[derive(Default)]
+pub struct Gameplay {
+ pub state: GameplayState,
+ pub moves_count: u32,
+}
+
+#[derive(Default)]
+pub struct Time {
+ pub delta: Duration,
}
diff --git a/code/rust-sokoban-c03-02/src/entities.rs b/code/rust-sokoban-c03-02/src/entities.rs
index 0457c63..f64aae5 100644
--- a/code/rust-sokoban-c03-02/src/entities.rs
+++ b/code/rust-sokoban-c03-02/src/entities.rs
@@ -1,60 +1,63 @@
use crate::components::*;
-use specs::{Builder, World, WorldExt};
+use hecs::{Entity, World};
-// Create a wall entity
-pub fn create_wall(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable::new_static("/images/wall.png".to_string()))
- .with(Wall {})
- .with(Immovable)
- .build();
+pub fn create_wall(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable::new_static("/images/wall.png"),
+ Wall {},
+ Immovable {},
+ ))
}
-pub fn create_floor(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 5, ..position })
- .with(Renderable::new_static("/images/floor.png".to_string()))
- .build();
+pub fn create_floor(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 5, ..position },
+ Renderable::new_static("/images/floor.png"),
+ ))
}
-pub fn create_box(world: &mut World, position: Position, colour: BoxColour) {
- 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),
- ]))
- .with(Box { colour })
- .with(Movable)
- .build();
+// ANCHOR: create_box
+pub fn create_box(world: &mut World, position: Position, colour: BoxColour) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable::new_animated(vec![
+ &format!("/images/box_{}_1.png", colour),
+ &format!("/images/box_{}_2.png", colour),
+ ]),
+ Box { colour },
+ Movable {},
+ ))
}
-pub fn create_box_spot(world: &mut World, position: Position, colour: BoxColour) {
- world
- .create_entity()
- .with(Position { z: 9, ..position })
- .with(Renderable::new_static(format!(
- "/images/box_spot_{}.png",
- colour
- )))
- .with(BoxSpot { colour })
- .build();
+pub fn create_box_spot(world: &mut World, position: Position, colour: BoxColour) -> Entity {
+ world.spawn((
+ Position { z: 9, ..position },
+ Renderable::new_static(&format!("/images/box_spot_{}.png", colour)),
+ BoxSpot { colour },
+ ))
}
+// ANCHOR_END: create_box
-pub fn create_player(world: &mut World, position: Position) {
- world
- .create_entity()
- .with(Position { z: 10, ..position })
- .with(Renderable::new_animated(vec![
- "/images/player_1.png".to_string(),
- "/images/player_2.png".to_string(),
- "/images/player_3.png".to_string(),
- ]))
- .with(Player {})
- .with(Movable)
- .build();
+pub fn create_player(world: &mut World, position: Position) -> Entity {
+ world.spawn((
+ Position { z: 10, ..position },
+ Renderable::new_animated(vec![
+ "/images/player_1.png",
+ "/images/player_2.png",
+ "/images/player_3.png",
+ ]),
+ Player {},
+ Movable {},
+ ))
}
+
+pub fn create_gameplay(world: &mut World) -> Entity {
+ world.spawn((Gameplay::default(),))
+}
+
+// ANCHOR: create_time
+pub fn create_time(world: &mut World) -> Entity {
+ world.spawn((Time::default(),))
+}
+// ANCHOR_END: create_time
diff --git a/code/rust-sokoban-c03-02/src/main.rs b/code/rust-sokoban-c03-02/src/main.rs
index 6fda229..0482c48 100644
--- a/code/rust-sokoban-c03-02/src/main.rs
+++ b/code/rust-sokoban-c03-02/src/main.rs
@@ -1,95 +1,76 @@
+/* ANCHOR: all */
// Rust sokoban
// main.rs
-use ggez::{conf, event::{self, KeyCode, KeyMods}, timer, Context, GameResult};
-use specs::{RunNow, World, WorldExt};
+use ggez::{
+ conf, event,
+ graphics::{self, DrawParam, Image},
+ input::keyboard::{self, KeyCode},
+ timer, Context, GameResult,
+};
+use glam::Vec2;
+use hecs::{Entity, World};
+
+use std::collections::HashMap;
use std::path;
mod components;
mod constants;
mod entities;
mod map;
-mod resources;
mod systems;
-use crate::components::*;
-use crate::map::*;
-use crate::resources::*;
-use crate::systems::*;
-
+// ANCHOR: game
+// This struct will hold all our game state
+// For now there is nothing to be held, but we'll add
+// things shortly.
struct Game {
world: World,
}
+// ANCHOR_END: game
+// ANCHOR: handler
impl event::EventHandler for Game {
+ // ANCHOR: update
fn update(&mut self, context: &mut Context) -> GameResult {
// Run input system
{
- let mut is = InputSystem {};
- is.run_now(&self.world);
+ systems::input::run_input(&self.world, context);
}
- // Run gameplay state system
+ // Run gameplay state
{
- let mut gss = GameplayStateSystem {};
- gss.run_now(&self.world);
+ systems::gameplay::run_gameplay_state(&mut self.world);
}
// Get and update time resource
{
- let mut time = self.world.write_resource::