We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
6. Compose Dungeon Denizens
Let’s learn a new term, Entity Component System (ECS). This will be the way we store increasingly larger and larger data sets for the games that we build. For this book we will use Legion which is a rust crate that is open source.
Understanding Terminology
Let’s define some of the terms that we will be using:
• An entity can be anything: an adventurer, an orc, or a pair of shoes.
• A component describes a property an entity may have.
• Systems query the entities and components and provide one element of
game-play/world simulation
• Resources are shared data available to multiple systems.
Composing Entities
So keep in mind that you are going to need to build Components for all the attributes for an Entity. You will need to build Render, Position and more for each. But once you build a Render component you will be able to use it for all the other entries that you build. Take this description of a Goblin.
“a small, green, angry humanoid. They roam
the dungeon, preying upon unsuspecting adventurers. They prefer to attack
from a distance, and are cowardly and quite weak.”
What did this tell us? What components should be build for this creature (entity)
• It’s a small humanoid requiring the same Render components as other
humanoids.
• It has a Position on the map.
• It’s angry, so you need an AI component denoting that it attacks on sight.
• It prefers ranged attacks, so your AI components should indicate that.
• It is cowardly, so maybe that needs a component when you implement
running away.
• It is quite weak, implying that while it has a Health component, the number
of hit points is quite low.
Installing and Using Legion
Okay let’s install Legion. Let’t first head to the dependencies part of the Cargo.toml.
[dependencies]
bracket-lib = "~0.8.1"
legion = "=0.3.1"
Add Legion to the Prelude
mod prelude {
pub use bracket_lib::prelude::*;
pub use legion::*;
pub use legion::world::SubWorld;
pub use legion::systems::CommandBuffer;
...
}
Remove Old Code
Okay so this will be an different way of creating a game. We will go away from the self made modules for the different entities and building off the ECS system. So in that vein we will need to remove: player.rs, mod player, and then the pub use crate::player::*;. Lastly we need to update the tick to just have the active consoles.
// main.rs
mod camera;
mod map;
mod map_builder;
mod prelude {
pub use bracket_lib::prelude::*;
pub use legion::systems::CommandBuffer;
pub use legion::world::SubWorld;
pub use legion::*;
pub const SCREEN_WIDTH: i32 = 640;
pub const SCREEN_HEIGHT: i32 = 480;
pub const TILE_SIZE: i32 = 8;
pub const CONSOLE_WIDTH: i32 = SCREEN_WIDTH / TILE_SIZE;
pub const CONSOLE_HEIGHT: i32 = SCREEN_HEIGHT / TILE_SIZE;
pub const FRAME_DURATION: f32 = 50.0;
pub const DISPLAY_WIDTH: i32 = CONSOLE_WIDTH / 2;
pub const DISPLAY_HEIGHT: i32 = CONSOLE_HEIGHT / 2;
pub use crate::camera::*;
pub use crate::map::*;
pub use crate::map_builder::*;
}
impl State {
...
fn on_map(&mut self, ctx: &mut BTerm) {
ctx.set_active_console(0);
ctx.cls();
ctx.set_active_console(1);
ctx.cls();
self.player.update(ctx, &self.map, &mut self.camera);
self.map.render(ctx, &self.camera);
self.player.render(ctx, &self.camera)
}
}
Finally delete the render from the map.rs
Create the World
Okay so we will not build the World this is where all the game entities will be stored. For this we will use the State struct to hold this and we will then be able to pass that information into any of the resources that need it.
struct State {
ecs: World,
resources: Resources,
systems: Schedule,
}
impl State {
fn new() -> Self {
let mut ecs = World::default();
let mut resources = Resources::default();
let mut rng = RandomNumberGenerator::new();
let map_builder = MapBuilder::new(&mut rng);
resources.insert(map_builder.map);
resources.insert(Camera::new(map_builder.player_start));
Self {
ecs,
resources,
systems: build_scheduler(),
}
}
...
}
Couple things to note. You are building the World and the Resources with some defaults and the build_scheduler() hasn’t been defined yet. Next we can build the Player with a component.
Composing the Player
Rather than coding all things player related into a single module we will build all its components and then use those to describe the player. Let’s think about what a player might need.
• A Player component indicating that the player is a Player. Components don’t
have to contain any fields. An empty component is sometimes called a
“tag”—it serves to flag that a property exists.
• A Render component describing how the player appears on the screen.
• A Position component indicating where the entity is on the map. The Point
structure from bracket-lib is ideal for this; it contains an x and a y component,
and it also provides several point-related math functions that will
prove useful.
Create a new file src/components.rs this will hold all your components for the game and thus needs to be added into your prelude.
mod components;
mod prelude {
...
pub use crate::components::*;
}
Now we can start to build the components.rs.
pub use crate::prelude::*;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Render {
pub color: ColorPair,
pub glyph: FontCharType,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Player;
ColorPair is a bracket-lib class that stores the foreground and background in one single struct. FontCarType is designed to store a single character glyph. We then use the player struct as a way to store the tag of player.
Add the Player to the World
Now we need to add a way to spawn the player into the world. Create a new file src/spawners.rs then be sure to add that to the prelude then we can build it.
mod spawner;
...
mod prelude {
...
pub use crate::spawner::*;
}
Now we can start to work on the spawner.rs
use crate::prelude::*;
pub fn spawn_player(ecs: &mut World, pos: Point) {
ecs.push((
Player,
pos,
Render {
color: ColorPair::new(WHITE, BLACK),
glyph: to_cp437('@'),
},
));
}
Okay so to go over this we are: passing in the World to the spawn_player, using the ecs.push function to create the component, add the tag, the position, then send the render information to the render. We now have a way to add the player into the world let’s add that to the State.new()
fn new() -> Self {
...
let map_builder = MapBuilder::new(&mut rng);
spawn_player(&mut ecs, map_builder.player_start);
...
}
Managing Complexity with Systems
Now we need to add some systems that will use the data. Systems are a special type of data that queries the ECS for data and performs operations on that data. Systems are managed by a Scheduler. The Scheduler will try and manage the way in which the systems are ordered and processed. It will also try an combine and see which ones can run concurrently. It will also run the system in the order in which they are presented.
Multi-File Modules
We now need to create a folder to house all the systems that we need and a way to organize those systems. Create a new folder and a file within that folder. src/systems/mod.rs, then what???? Add the new file to the prelude!!!
mod systems;
...
mod prelude {
...
pub use crate::systems::*;
}
Okay so there is a way to use Rust’s nested modules. We need to add in a ECS (Legion) schedule builder that returns an empty Legion schedule. Add this to the systems/mod.rs.
use crate::prelude::*;
pub fn build_scheduler() -> Schedule {
Schedule::builder()
.build()
}
Okay so we now built that build_scheduler we talked about earlier. We can now add that to the new and the tick. Ill talk about a few things afterwards.
fn on_map(&mut self, ctx: &mut BTerm) {
ctx.set_active_console(0);
ctx.cls();
ctx.set_active_console(1);
ctx.cls();
self.resources.insert((ctx.key));
self.systems.execute(&mut self.ecs, &mut self.resources);
// TODO - Render Draw Buffer
}
The systems will take care of a lot of the work that needs to be done within the program. Once that is done we will then need to render the outcome.
Let’s talk about what we just did: _self.resources.insert() adds the current keyboard to the world so we can access that, then we execute the systems.
Understanding Queries
When we run a query within the ECS we are sending a set of components and we will only get back those entities that match ALL entities that match ALL components. You can even then use the filter option to filter out entities that have that option.
Player Input as a System
Okay we are getting closer and closer. We need to add an other systems that will deal with the player input. Create a new file src/systems/player_input.rs. This will be our first nested module. Once this is made head to the _src/systems/mod.rs and add the following.
mod player_input;
Within the systems module you can now access the player_input module with player_input::*, and since it is not pub you can’t access it anywhere in your code. Now let’s start to work with the player_input
#[system]
#[write_component(Point)]
#[read_component(Player)]
pub fn player_input(
ecs: &mut SubWorld,
#[resource] map: &Map,
#[resource] key: &Option<VirtualKeyCode>,
#[resource] camera: &mut Camera,
) {
}
[system] annotates the player_input withe macro for system, [write_component] makes it so we are about to write to the Point component, [read_component] makes it so we can have read only access to the Player. Keep in mind that many different systems can have access to a read-only but only one can have access to a write. There are a few other things it this does for us.
• The first function parameter requests a SubWorld. A SubWorld is like a
World—but can only see the components you requested.
• #[resource] requests access to types you stored in Legion’s Resource handler.
It is also a procedural macro.
• You access the map by listing map: &Map after the resource annotation.
This is just like borrowing elsewhere—you are requesting a read-only
reference to the map.
• You access the camera with &mut Camera. This is just like a mutable borrow—you
are requesting a mutable reference to the camera. Your code
can change the contents of the Camera struct, and the global resource is
updated with the new values.
Okay now that we set up the parameters and the access points we need to build the main body of the function.
use crate::prelude::*;
#[system]
#[write_component(Point)]
#[read_component(Player)]
pub fn player_input(
ecs: &mut SubWorld,
#[resource] map: &Map,
#[resource] key: &Option<VirtualKeyCode>,
#[resource] camera: &mut Camera,
) {
if let Some(key) = key {
let delta = match key {
VirtualKeyCode::Left => Point::new(-1, 0),
VirtualKeyCode::Right => Point::new(1, 0),
VirtualKeyCode::Up => Point::new(0, -1),
VirtualKeyCode::Down => Point::new(0, 1),
_ => Point::new(0, 0),
};
if delta.x != 0 || delta.y != 0 {
let mut players = <&mut Point>::query().filter(component::<Player>());
players.iter_mut(ecs).for_each(|pos| {
let destination = *pos + delta;
if map.can_enter_tile(destination) {
*pos = destination;
camera.on_player_move(destination);
}
});
}
}
}
So we are first grabbing all the Points, then we filter to only grab the Players from that list. We then run the position change and then check to be sure the player can enter the space. Now finally we need to add that new system to the scheduler. Head back to systems/mod.rs and add this line.
mod player_input;
use crate::prelude::*;
pub fn build_scheduler() -> Schedule {
Schedule::builder()
.add_system(player_input::player_input_system())
.build()
}
Batched Rendering
What is great and not so great is that a systems will make any program multi-threaded. This will do some strange things to the render and many other parts as we will be doing things to parts at the same time. We could work with a built-in feature of bracket-lib where we lock the render and unlock it but that is a lot of overhead. We will then use a batched render. This will create a queue for renders that will be rendered in order to avoid things happening at once. Let’s add that to our tick()
fn on_map(&mut self, ctx: &mut BTerm) {
...
self.systems.execute(&mut self.ecs, &mut self.resources);
render_draw_buffer(ctx).expect("Render error")
}
Map Rendering System
Okay so now we need to make a map render system. Create a new file src/systems/map\render.rs and then add that to the mod.rs. We will use a lot of the old map render with some changes.
use crate::prelude::*;
#[system]
pub fn map_render(#[resource] map: &Map, #[resource] camera: &Camera) {
let mut draw_batch = DrawBatch::new();
draw_batch.target(0);
for y in camera.top_y..=camera.bottom_y {
for x in camera.left_x..camera.right_x {
let pt = Point::new(x, y);
let offset = Point::new(camera.left_x, camera.top_y);
if map.in_bounds(pt) {
let idx = map_idx(x, y);
let glyph = match map.tiles[idx] {
TileType::Floor => to_cp437('.'),
TileType::Wall => to_cp437('#'),
};
draw_batch.set(pt - offset, ColorPair::new(WHITE, BLACK), glyph);
}
}
}
draw_batch.submit(0).expect("Batch error");
}
We still add the [system], We ask for permission for the map and the camera, We then create a new drawbatch to store the needed renders, then we submit that new render to the batch. Now add that to the systems in _mod.rs
mod map_render;
mod player_input;
use crate::prelude::*;
pub fn build_scheduler() -> Schedule {
Schedule::builder()
.add_system(player_input::player_input_system())
.add_system(map_render::map_render_system())
.build()
}
Render Entities in a System
Okay now we need an ability to render anything with a Point and a Render component. This will be the player and anything else that we add that has those to components. Make a new file src/systems/entity_render.rs. What do we need to do after that? Add to the mod!!!
use crate::prelude::*;
#[system]
#[read_component(Point)]
#[read_component(Render)]
pub fn entity_render(ecs: &SubWorld, #[resource] camera: &Camera) {}
We are just getting started but what are the things we need access to? Points and Renders. Now we can build the main guts of the system. Remember that we can do a multi-component query and it will return all components that have BOTH of the components. Let’s build the function.
use crate::prelude::*;
#[system]
#[read_component(Point)]
#[read_component(Render)]
pub fn entity_render(ecs: &SubWorld, #[resource] camera: &Camera) {
let mut draw_batch = DrawBatch::new();
draw_batch.target(1);
let offset = Point::new(camera.left_x, camera.top_y);
<(&Point, &Render)>::query()
.iter(ecs)
.for_each(|(pos, render)| {
draw_batch.set(*pos - offset, render.color, render.glyph);
});
draw_batch.submit(5000).expect("Batch error");
}
We always start with a new DrawBatch, then we query for Points and Renders We then iterate the results, then we make sure we are rendering in the screen, then we add the new batch to the render_batch. Now we need to add that to the system and we should be able to run the game.
mod entity_render;
mod map_render;
mod player_input;
use crate::prelude::*;
pub fn build_scheduler() -> Schedule {
Schedule::builder()
.add_system(player_input::player_input_system())
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.build()
}
Run your game to see how it all works.
Issues
First lets deal with the clippy issues.
warning: unnecessary parentheses around method argument
--> src/main.rs:68:31
|
68 | self.resources.insert((ctx.key));
| ^ ^
|
= note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
|
68 - self.resources.insert((ctx.key));
68 + self.resources.insert(ctx.key);
|
error[E0425]: cannot find function `build_scheduler` in this scope
--> src/main.rs:51:22
|
51 | systems: build_scheduler(),
| ^^^^^^^^^^^^^^^ not found in this scope
error[E0609]: no field `mode` on type `&mut State`
--> src/main.rs:76:20
|
76 | match self.mode {
| ^^^^ unknown field
|
= note: available fields are: `ecs`, `resources`, `systems`
/// Fixes
mod camera;
mod components;
mod map;
mod map_builder;
mod spawner;
mod systems;
mod prelude {
pub use bracket_lib::prelude::*;
pub use legion::systems::CommandBuffer;
pub use legion::world::SubWorld;
pub use legion::*;
pub const SCREEN_WIDTH: i32 = 640;
pub const SCREEN_HEIGHT: i32 = 480;
pub const TILE_SIZE: i32 = 8;
pub const CONSOLE_WIDTH: i32 = SCREEN_WIDTH / TILE_SIZE;
pub const CONSOLE_HEIGHT: i32 = SCREEN_HEIGHT / TILE_SIZE;
pub const FRAME_DURATION: f32 = 50.0;
pub const DISPLAY_WIDTH: i32 = CONSOLE_WIDTH / 2;
pub const DISPLAY_HEIGHT: i32 = CONSOLE_HEIGHT / 2;
pub use crate::camera::*;
pub use crate::components::*;
pub use crate::map::*;
pub use crate::map_builder::*;
pub use crate::spawner::*;
pub use crate::systems::*;
}
struct State {
mode: GameMode,
ecs: World,
resources: Resources,
systems: Schedule,
}
Self {
mode: GameMode::OnMap,
ecs,
resources,
systems: build_scheduler(),
}
fn on_map(&mut self, ctx: &mut BTerm) {
ctx.set_active_console(0);
ctx.cls();
ctx.set_active_console(1);
ctx.cls();
self.resources.insert(ctx.key);
self.systems.execute(&mut self.ecs, &mut self.resources);
render_draw_buffer(ctx).expect("Render error")
}
Fixing Dependency Version Conflicts with Older Rust
Problem
When using older versions of Rust (e.g. 1.74.1 for bracket-lib compatibility), newer transitive dependencies may require a newer rustc version.
Tools
-
cargo tree- shows the full dependency tree -
cargo tree | grep <package>- find a specific package -
cargo tree --invert <package>- show what depends on a package -
cargo update <package>@<version> --precise <target-version>- downgrade a package
Process
-
Run
cargo buildto see which package is failing and why -
Use
cargo tree --invert <package>to trace the dependency chain - Go to crates.io and check the MSRV (shown as the rust icon + version) for each version of the problematic package
- Downgrade the parent package first, then the problematic package
- Repeat until the chain resolves
Example
legion → rayon → rayon-core (required rustc 1.80, we had 1.74.1)
cargo update rayon@1.12.0 --precise 1.10.0
cargo update rayon-core@1.13.0 --precise 1.12.1
Key Insight
You often need to downgrade the parent dependency before you can downgrade the problematic one, since the parent may have a minimum version requirement on the child.
Adding Monsters
Now we can add in a Monster, we need to add an enemy struct to the components first then we can add a spawn_monster() to the spawners.rs
// components.rs
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Enemy
// spawner.rs
pub fn spawn_monster(ecs: &mut World, rng: &mut RandomNumberGenerator, pos: Point) {
ecs.push((
Enemy,
pos,
Render {
color: ColorPair::new(WHITE, BLACK),
glyph: match rng.range(0, 4) {
0 => to_cp437('E'),
1 => to_cp437('O'),
2 => to_cp437('o'),
_ => to_cp437('g'),
},
},
));
}
This creates a monster of 4 different types. Also bare in mind that we need to pass the World and and RNG and a point into the spawn.
Add Monsters to the Map
Okay so now we want to add in a monster into every room besides the player. We will do this within the main.rs
impl State {
fn new() -> Self {
let mut ecs = World::default();
let mut resources = Resources::default();
let mut rng = RandomNumberGenerator::new();
let map_builder = MapBuilder::new(&mut rng);
spawn_player(&mut ecs, map_builder.player_start);
map_builder
.rooms
.iter()
.skip(1)
.map(|r| r.center())
.for_each(|pos| spawn_monster(&mut ecs, &mut rng, pos));
resources.insert(map_builder.map);
resources.insert(Camera::new(map_builder.player_start));
Self {
mode: GameMode::OnMap,
ecs,
resources,
systems: build_scheduler(),
}
}
...
}
We used some iterator magic here. We take the build_builder.rooms then create an iter, then skip the first room, then map the center, then for each we spawn_monster. TEST IT OUT!!!
Collision Detection
Now we get to add in a collision detection system. Create a new file src/systems/collisions_detections.rs then be sure to add it to the mods.rs
// mod.rs
mod collisions;
// collision_detection.rs
use crate::prelude::*;
#[system]
#[read_component(Point)]
#[read_component(Player)]
#[read_component(Enemy)]
pub fn collisions(ecs: &mut SubWorld, commands: &mut CommandBuffer) {}
As per usual we need to set up the reads to this. There is a new thing that we added and that is the command buffer that will execute commands after the system is run. Let’s build the rest then add that to the systems.
use crate::prelude::*;
#[system]
#[read_component(Point)]
#[read_component(Player)]
#[read_component(Enemy)]
pub fn collisions(ecs: &mut SubWorld, commands: &mut CommandBuffer) {
let mut player_pos = Point::zero();
let mut players = <&Point>::query().filter(component::<Player>());
players.iter(ecs).for_each(|pos| player_pos = *pos);
let mut enemies = <(Entity, &Point)>::query().filter(component::<Enemy>());
enemies
.iter(ecs)
.filter(|(_, pos)| **pos == player_pos)
.for_each(|(entity, _)| {
commands.remove(*entity);
});
}
// mod.rs
mod collisions;
mod entity_render;
mod map_render;
mod player_input;
use crate::prelude::*;
pub fn build_scheduler() -> Schedule {
Schedule::builder()
.add_system(player_input::player_input_system())
.add_system(collisions::collisions_system())
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.build()
}
Okay so a few things to go over here. We pass in the World and the command buffer, we set all the positions of the entities, then for each enemy if we find that the player and the enemy share the same location we remove the enemy.
Wrap-Up
Great job with the game. You implemented a new way of doing ticks and dealing with the data we are using. Next we will make it so when we move then the enemies will move.