Skip to content

Commit

Permalink
Fix book text
Browse files Browse the repository at this point in the history
  • Loading branch information
iolivia committed Dec 13, 2024
1 parent cb88a7c commit daf462f
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 83 deletions.
22 changes: 11 additions & 11 deletions books/en_US/src/c02-03-push-box.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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).
> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c02-03).
20 changes: 5 additions & 15 deletions books/en_US/src/c02-04-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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).

70 changes: 19 additions & 51 deletions books/en_US/src/c02-05-gameplay.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`
Expand All @@ -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:
Expand All @@ -65,104 +53,84 @@ 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}}
...
```

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`
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

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).
> **_CODELINK:_** You can see the full code in this example [here](https://github.com/iolivia/rust-sokoban/tree/master/code/rust-sokoban-c02-05).
2 changes: 2 additions & 0 deletions code/rust-sokoban-c02-03/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ pub struct Box {}

pub struct BoxSpot {}

// ANCHOR: components_movement
pub struct Movable;

pub struct Immovable;
// ANCHOR_END: components_movement

// ANCHOR_END: components

Expand Down
18 changes: 12 additions & 6 deletions code/rust-sokoban-c02-05/src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,28 @@ pub struct Movable;

pub struct Immovable;

// 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 {
Expand All @@ -44,9 +55,4 @@ impl Display for GameplayState {
Ok(())
}
}

#[derive(Default)]
pub struct Gameplay {
pub state: GameplayState,
pub moves_count: u32,
}
// ANCHOR_END: gameplay_state_impl_display
4 changes: 4 additions & 0 deletions code/rust-sokoban-c02-05/src/systems/rendering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,20 @@ pub fn run_rendering(world: &World, context: &mut Context) {
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(),
Expand All @@ -58,3 +61,4 @@ pub fn draw_text(canvas: &mut Canvas, text_string: &str, x: f32, y: f32) {

canvas.draw(&text, Vec2::new(x, y));
}
// ANCHOR_END: draw_text

0 comments on commit daf462f

Please sign in to comment.