Skip to content

Commit

Permalink
docs: add tile-based game example w/ a tutorial in the book, replacin…
Browse files Browse the repository at this point in the history
…g getting-started guide (#269)

Closes #51 

The existing getting-started guide wasn't a satisfactory "tutorial"
according to the "grand unified theory of documentation". This PR adds a
more complete-minimal-project tutorial that covers the steps that the
getting-started guide did, as well as steps for setting up the LDtk
project and adding gameplay. This tutorial is associated with a new
example - `tile_based_game`. Most of the code snippets in the tutorial
try to include code sections from the example to make sure these two
match.

I will likely update the `lib.rs` to link to this tutorial instead of
copy-pasting the same getting-started guide in a future PR. I might also
remove the getting-started guide in the `README.md`, but I'm more okay
with it existing there as marketing material.
  • Loading branch information
Trouv authored Dec 2, 2023
1 parent bb91660 commit 2d43efa
Show file tree
Hide file tree
Showing 26 changed files with 2,321 additions and 31 deletions.
Binary file added assets/atlas/SunnyLand-player.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1,673 changes: 1,673 additions & 0 deletions assets/tile-based-game.ldtk

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion book/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Deployment of this book to github pages is also performed by `bevy_ecs_ldtk`'s C
Splitting the documentation up this way means that this book is not necessarily meant to be read in order.
Some chapters are intended to be read while working on your own project, while others are meant to be more like studying material.
The following chapters are good jumping-off points for beginners:
- [*Getting Started* tutorial](getting-started.md)
- [*Tile-based Game* tutorial](tutorials/tile-based-game/index.html)
- [*Game Logic Integration* explanation]()

## Other resources
Expand Down
6 changes: 4 additions & 2 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

[Introduction](README.md)
# Tutorials
- [Getting Started](tutorials/getting-started.md)
- [Sokoban]()
- [Tile-based Game](tutorials/tile-based-game/README.md)
- [Create your LDtk project](tutorials/tile-based-game/create-your-ldtk-project.md)
- [Spawn your LDtk project in Bevy](tutorials/tile-based-game/spawn-your-ldtk-project-in-bevy.md)
- [Add gameplay to your project](tutorials/tile-based-game/add-gameplay-to-your-project.md)
- [Platformer]()
# Explanation
- [Game Logic Integration]()
Expand Down
28 changes: 0 additions & 28 deletions book/src/tutorials/getting-started.md

This file was deleted.

21 changes: 21 additions & 0 deletions book/src/tutorials/tile-based-game/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Tile-based Game
In this tutorial you will make a tile-based game with LDtk levels.
Game entities will be locked to a grid of tiles like sokoban, or snake.
You will go through the process of creating an LDtk project, loading the project into bevy, and adding gameplay.

This tutorial does have an example associated with it in the [`bevy_ecs_ldtk` repository](https://github.com/trouv/bevy_ecs_ldtk):
```bash
$ cargo run --example tile_based_game --release
```

## Prerequisites
You will need to perform the following setup/installations:
- [Bevy project setup](https://bevyengine.org/learn/book/getting-started/setup/) for the version specified in the [compatibility chart](https://github.com/Trouv/bevy_ecs_ldtk#compatibility).
- [LDtk installation](https://ldtk.io/versions/), for the version specified in the [compatibility chart](https://github.com/Trouv/bevy_ecs_ldtk#compatibility).

You will also need some simple assets:
- A tileset for the environment with at least a background tile, a wall tile, and a "goal"-ish tile.
- A tileset for the the player.

For these purposes this tutorial will use the `environment/tileset.png` and `spritesheets/player.png` assets respectively from [SunnyLand by Ansimuz](https://ansimuz.itch.io/sunny-land-pixel-game-art), licensed under [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/).
However, you will be able to follow this tutorial using any tilesets, so long as they have tiles appropriate for the above purposes.
221 changes: 221 additions & 0 deletions book/src/tutorials/tile-based-game/add-gameplay-to-your-project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
# Add gameplay to your project
In this section, you will integrate gameplay to the Bevy/LDtk project created in the previous sections.
This includes tile-based movement, collision, and level transitions.
You are welcome to bring your own tile-based LDtk project to this tutorial, but some of the values specified in here are specific to the LDtk project created in this tutorial, such as...
- the IntGrid value of walls (1)

For details about the tutorial in general, including prerequisites, please see the parent page.

## Add marker component and `GridCoords` to the player
In order to implement tile-based movement and tile-based mechanics, you'll need to deal with an entity's position in tile-space rather than just Bevy world translation.
`bevy_ecs_ldtk` provides a component that is suitable for this, and it has integration with the `LdtkEntity` derive.
Add the `GridCoords` component to the `PlayerBundle`, and give it the `#[grid_coords]` attribute.
The player entity will then be spawned with a `GridCoords` component whose value matches the entity's position in grid-space.

Also give it a `Player` marker component so that you can query for it more easily in future systems.
Derive `Default` for this component.
`bevy_ecs_ldtk` will use this default implementation when spawning the component unless otherwise specified.
```rust,no_run
# use bevy::prelude::*;
# use bevy_ecs_ldtk::prelude::*;
{{#include ../../../../examples/tile_based_game.rs:42:52}}
```

## Implement tile-based movement
The player now has the components you will need to implement tile-based movement.
Write a system that checks for just-pressed WASD input and converts it to a `GridCoords` direction.
I.e., `(0,1)` for W, `(-1,0)` for A, `(0,-1)` for S, and `(1,0)` for D.
Then, add the new direction to the player entity's `GridCoords` component.
```rust,no_run
# use bevy::prelude::*;
# use bevy_ecs_ldtk::prelude::*;
# #[derive(Component)]
# struct Player;
fn main() {
App::new()
// other App builders
.add_systems(Update, move_player_from_input)
.run();
}
{{#include ../../../../examples/tile_based_game.rs:91:93}}
{{#include ../../../../examples/tile_based_game.rs:95:109}}
*player_grid_coords = destination;
}
}
```

## Update translation from `GridCoords` value
If you play the game at this point, you'll notice that the player entity doesn't appear to be moving at all.
The `GridCoords` component may be updating correctly, but the entity's `Transform` is what determines where it is rendered.
`bevy_ecs_ldtk` does not maintain the `Transform` of `GridCoords` entities automatically.
This is left up to the user, which allows you to implement custom tweening or animation of the transform as you please.

Write a system that updates the `Transform` of `GridCoords` entities when their `GridCoords` value changes.
`bevy_ecs_ldtk` does provide a utility function to help calculate the resulting translation - provided you know the size of the cells of the grid.
For the LDtk project set up in this tutorial using the `SunnyLand` tilesets, this grid size is 16.
```rust,no_run
# use bevy::prelude::*;
# use bevy_ecs_ldtk::prelude::*;
# fn move_player_from_input() {}
fn main() {
App::new()
// other App builders
{{#include ../../../../examples/tile_based_game.rs:15:19}}
),
)
.run();
}
{{#include ../../../../examples/tile_based_game.rs:116:126}}
```

## Prevent tile-based movement into walls
Movement works logically *and* visually now.
However, you might notice that you can move *into* the walls of the level.
To implement tile-based collision, you will need to add components to the walls to identify their locations, and check against these locations when trying to move the player.

Create a new bundle for the wall entities, and give them a marker component.
Derive `LdtkIntCell` for this bundle, and register it to the app with `register_ldtk_int_cell` and the wall's intgrid value.
This bundle actually only needs this one marker component - IntGrid entities spawn with a `GridCoords` without requesting it.
```rust,no_run
# use bevy::prelude::*;
# use bevy_ecs_ldtk::prelude::*;
fn main() {
App::new()
// other App builders
{{#include ../../../../examples/tile_based_game.rs:24}}
.run();
}
{{#include ../../../../examples/tile_based_game.rs:66:72}}
```

There are a lot of ways to go about implementing the collision systems.
Naively, you could query for all of the `Wall` entities every time the player tries to move and check their `GridCoords` values.
In this tutorial, you will implement something a little more optimized: caching the wall locations into a resource when levels spawn.

Create a `LevelWalls` resource for storing the current wall locations that can be looked up by-value.
Give it a `HashSet<GridCoords>` field for the wall locations.
Give it fields for the level's width and height as well so you can prevent the player from moving out-of-bounds.
Then, implement a method `fn in_wall(&self, grid_coords: &GridCoords) -> bool` that returns true if the provided `grid_coords` is outside the level bounds or contained in the `HashSet`.
```rust,no_run
# use bevy::prelude::*;
# use bevy_ecs_ldtk::prelude::*;
use std::collections::HashSet;
fn main() {
App::new()
// other App builders
{{#include ../../../../examples/tile_based_game.rs:25}}
.run();
}
{{#include ../../../../examples/tile_based_game.rs:74:89}}
```

Now, add a system that listens for `LevelEvent::Spawned` and populates this resource.
It will need access to all of the wall locations to populate the `HashSet` (`Query<&GridCoords, With<Wall>>`).
It will also need access to the `LdtkProject` data to find the current level's width/height (`Query<&Handle<LdtkProject>>` and `Res<Assets<LdtkProject>>`).
```rust,no_run
# use bevy::prelude::*;
# use bevy_ecs_ldtk::prelude::*;
# use std::collections::HashSet;
# const GRID_SIZE: i32 = 16;
# #[derive(Default, Resource)]
# struct LevelWalls {
# wall_locations: HashSet<GridCoords>,
# level_width: i32,
# level_height: i32,
# }
# impl LevelWalls {
# fn in_wall(&self, grid_coords: &GridCoords) -> bool {
# grid_coords.x < 0
# || grid_coords.y < 0
# || grid_coords.x >= self.level_width
# || grid_coords.y >= self.level_height
# || self.wall_locations.contains(grid_coords)
# }
# }
# #[derive(Component)]
# struct Wall;
# fn move_player_from_input() {}
# fn translate_grid_coords_entities() {}
fn main() {
App::new()
// other App builders
{{#include ../../../../examples/tile_based_game.rs:15:20}}
)
)
.run();
}
{{#include ../../../../examples/tile_based_game.rs:128:155}}
```

Finally, update the `move_player_from_input` system to access the `LevelWalls` resource and check whether or not the player's destination is in a wall.
```rust,no_run
# use bevy::prelude::*;
# use bevy_ecs_ldtk::prelude::*;
# use std::collections::HashSet;
# #[derive(Component)]
# struct Player;
# #[derive(Default, Resource)]
# struct LevelWalls {
# wall_locations: HashSet<GridCoords>,
# level_width: i32,
# level_height: i32,
# }
# impl LevelWalls {
# fn in_wall(&self, grid_coords: &GridCoords) -> bool {
# grid_coords.x < 0
# || grid_coords.y < 0
# || grid_coords.x >= self.level_width
# || grid_coords.y >= self.level_height
# || self.wall_locations.contains(grid_coords)
# }
# }
{{#include ../../../../examples/tile_based_game.rs:91:114}}
```

With this check in place, the player should now be unable to move into walls!

## Trigger level transitions on victory
The final step is to implement the goal functionality.
When the player reaches the goal, the next level should spawn until there are no levels remaining.

Similar to the `PlayerBundle`, give the `GoalBundle` its own marker component and `GridCoords`.
```rust,no_run
# use bevy::prelude::*;
# use bevy_ecs_ldtk::prelude::*;
{{#include ../../../../examples/tile_based_game.rs:54:64}}
```

Then, write a system that checks if the player's `GridCoords` and the goal's `GridCoords` match.
For a small optimization, filter the player query for `Changed<GridCoords>` so it's only populated if the player moves.
If they do match, update the `LevelSelection` resource, increasing its level index by 1.
`bevy_ecs_ldtk` will automatically despawn the current level and spawn the next one when this resource is updated.
```rust,no_run
# use bevy::prelude::*;
# use bevy_ecs_ldtk::prelude::*;
# #[derive(Component)]
# struct Player;
# #[derive(Component)]
# struct Goal;
# fn move_player_from_input() {}
# fn translate_grid_coords_entities() {}
# fn cache_wall_locations() {}
fn main() {
App::new()
// other App builders
{{#include ../../../../examples/tile_based_game.rs:15:23}}
.run();
}
{{#include ../../../../examples/tile_based_game.rs:157::}}
```

With this, the simple tile-based game is complete.
When you navigate the player to the goal, the next level will begin until there are no levels remaining.

<div style="width:100%;height:0px;position:relative;padding-bottom:56.250%;"><iframe src="https://streamable.com/e/i342f8" frameborder="0" width="100%" height="100%" allowfullscreen style="width:100%;height:100%;position:absolute;left:0px;top:0px;overflow:hidden;"></iframe></div>
Loading

0 comments on commit 2d43efa

Please sign in to comment.