We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
5. Build a Dungeon Crawler
Here we go we are now starting in on the rogue like project.
Dividing Your Code Into Modules
This is where we really start to become a real programmer. We want to separate the context and the modules into different files for the following reasons (not all inclusive):
• It’s a lot easier to find your Map functionality in a file named map.rs than
it is to remember that it’s somewhere around line 500 of an ever-growing
main.rs.
• Cargo can compile modules concurrently, leading to much better compilation times.
• Bugs are easier to find in self-contained code that limits linkages to other
sections of code.
Crates and Modules
Most everything that you build could be considered a crate. Our game that we will build is a crate, bracket-lib is a crate as well.
Crates act as namespaces. bracket-lib::prelude refers to the bracket-lib prelude module, you can even reference the current crate with crate::. You can nest creates with :: syntax if they are nested within each other. You might see map::region::town.
Make a Stub Map Module
Start but making a new project
cargo new dungeoncrawl
Then go through the steps to display “Hello Bracket World” on the bracket-lib terminal.
use bracket_lib::prelude::*;
mod map;
struct State {
player: i32,
frame_time: f32,
mode: GameMode,
}
impl State {
fn new() -> Self {
State {
player: 10,
frame_time: 0.0,
mode: GameMode::StartingScreen,
}
}
fn starting_screen(&mut self, ctx: &mut BTerm) {
// ctx.set_active_console(0);
ctx.cls();
ctx.print_centered(9, "Hello Bracket World!")
}
}
impl GameState for State {
fn tick(&mut self, ctx: &mut BTerm) {
match self.mode {
GameMode::StartingScreen => self.starting_screen(ctx),
GameMode::OnMap => todo!(),
GameMode::Battle => todo!(),
GameMode::Menu => todo!(),
GameMode::ItemScreen => todo!(),
}
}
}
enum GameMode {
Menu,
OnMap,
Battle,
ItemScreen,
StartingScreen,
}
const SCREEN_WIDTH: i32 = 640;
const SCREEN_HEIGHT: i32 = 480;
const TILE_SIZE: i32 = 8;
const CONSOLE_WIDTH: i32 = SCREEN_WIDTH / TILE_SIZE;
const CONSOLE_HEIGHT: i32 = SCREEN_HEIGHT / TILE_SIZE;
const FRAME_DURATION: f32 = 50.0;
fn main() -> BError {
let context = BTermBuilder::new()
.with_dimensions(CONSOLE_WIDTH, CONSOLE_HEIGHT)
.with_tile_dimensions(TILE_SIZE, TILE_SIZE)
.with_title("Rusty Roguelike")
.with_font("terminal8x8.png", 8, 8)
.with_simple_console(CONSOLE_WIDTH, CONSOLE_HEIGHT, "terminal8x8.png")
// .with_fancy_console(CONSOLE_WIDTH, CONSOLE_HEIGHT, "terminal8x8.png")
.build()?;
main_loop(context, State::new())
}
/// Cargo.toml
[package]
name = "Rusty_Roguelike"
version = "0.1.0"
authors = ["Adam Vietro <adam.vietro@gmail.com>"]
edition = "2021"
[dependencies]
bracket-lib = "~0.8.1"
See the errors that I had to work with again here. I still needed to set the Rust version with rustup override set 1.74.1
Also notice the mod map; at the top that will be used to import the mod module that we will make now. Go ahead and make a map.rs within the src directory you can leave it blank for now.
Module Scoping
Everything within a module is contained within its self. You don’t have access to a variable unless you set it to be public. You can make most things public like so:
• Functions (e.g. pub fn my_function())
• Structs (e.g. pub struct MyStruct)
• Enumerations (e.g. pub enum MyEnum)
• Implemented functions (e.g. impl MyStruct { pub fn my_function() })
You can even make individual members of a struct public like so
pub struct MyPublicStructure {
pub public_int : i32,
private_int : i32
}
Organizing Imports with a Prelude
Instead of importing bracket_lib and your constants in every file, you can create a local prelude module in main.rs that re-exports everything in one place. Any module that adds use prelude::* gets access to all of it automatically.
mod map;
mod prelude {
pub use bracket_lib::prelude::*;
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 use crate::map::*;
}
-
mod mapregisters themapmodule so the compiler knows to look forsrc/map.rs. It doesn’t need to bepubsince it lives at the crate root. -
pub use bracket_lib::prelude::*re-exports bracket-lib’s types so other modules don’t need to import it separately. -
The constants are marked
pubso they’re visible to any module that imports the prelude. -
pub use crate::map::*re-exports map’s public types through the prelude, so again, no separate import needed elsewhere.
Set Up Your Map Module
Add the following to your map.rs to import the crate and set a NUM_TILES for the number of tiles on the map.
use crate::prelude::*;
const NUM_TILES: usize = (CONSOLE_WIDTH * CONSOLE_HEIGHT) as usize;
Storing the Dungeon Map
Most 2-d games will render a map as a set of tiles. You might even be able to reuse tiles to represent many different sets of maps. We will work on building the idea of a wall and floor and how we can represent those tiles as vectors.
Representing Your Tiles
#[derive(Copy, Clone, PartialEq)]
pub enum TileType {
Wall,
Floor,
}
So a few things to unpack here. First we will be setting a public enum so that anyone that needs to access the type of TileType can see what they are. Also we set up a derive
This is a Rust thing and will go over the different types now:
• Clone adds a clone() function to the type. Calling mytile.clone() makes a deep
copy of the variable without affecting the original. If you clone a struct,
everything the struct contains will also be cloned. This is useful when
you want to safely work with a clone of some data with no risk of altering
the original—or when you need to work around the borrow checker.
• Copy changes the default action when assigning a TileType from one variable
to another. Instead of moving the value, it takes a copy. Smaller types are
often faster when you copy them around. Clippy will warn you if you are
borrowing a variable and it would be faster to copy it.
• PartialEq adds code that allows you to compare TileType values with the ==
operator.
We now have the idea of a tile and what they might be. For now we want to draw a wall as “#” and the player as a “@”.
Create an Empty Map
pub struct Map {
pub tiles: Vec<TileType>,
}
impl Map {
pub fn new() -> Self {
Self {
tiles: vec![TileType::Floor; NUM_TILES],
}
}
}
We have decided to build the map as a set of vectors. We can see that we are building a vector with the exact size that we want. The entry will have the TileType as the data type.
Index the Map
Since this is a 1 dimension vector and we want to be able to build a 2-d map we will need a way to transform the vector index, this is called striding. This will use modular arithmetic and will be based on the modulus of the width we want and the amount of entries to go from the index to an x and y, we get to just multiply the y by the WIDTH and then add x to get the index going the other way. We will use “#” for a wall and “.” for a floor.
pub fn map_idx(x: i32, y: i32) -> usize {
((y * CONSOLE_WIDTH) + x) as usize
}
Render the Map
Add the following inside the impl for the Map. This will be used to build the map.
pub fn render(&self, ctx: &mut BTerm) {
for y in 0..CONSOLE_HEIGHT {
for x in 0..CONSOLE_WIDTH {
let idx = map_idx(x, y);
match self.tiles[idx] {
TileType::Floor => {
ctx.set(x, y, YELLOW, BLACK, to_cp437('.'));
}
TileType::Wall => {
ctx.set(x, y, GREEN, BLACK, to_cp437('#'));
}
}
}
}
}
Consume the Map API
Okay we have the module for the map now we can use it.
struct State {
player: i32,
frame_time: f32,
mode: GameMode,
map: Map,
}
impl State {
fn new() -> Self {
State {
player: 10,
frame_time: 0.0,
mode: GameMode::OnMap,
map: Map::new(),
}
}
fn starting_screen(&mut self, ctx: &mut BTerm) {
// ctx.set_active_console(0);
ctx.cls();
ctx.print_centered(9, "Hello Bracket World!")
}
fn on_map(&mut self, ctx: &mut BTerm) {
ctx.cls();
self.map.render(ctx);
}
}
impl GameState for State {
fn tick(&mut self, ctx: &mut BTerm) {
match self.mode {
GameMode::StartingScreen => self.starting_screen(ctx),
GameMode::OnMap => self.on_map(ctx),
GameMode::Battle => todo!(),
GameMode::Menu => todo!(),
GameMode::ItemScreen => todo!(),
}
}
}
fn main() -> BError {
let context = BTermBuilder::new()
.with_dimensions(CONSOLE_WIDTH, CONSOLE_HEIGHT)
.with_tile_dimensions(TILE_SIZE, TILE_SIZE)
.with_title("Rusty Roguelike")
.with_font("terminal8x8.png", 8, 8)
.with_simple_console(CONSOLE_WIDTH, CONSOLE_HEIGHT, "terminal8x8.png")
// .with_fancy_console(CONSOLE_WIDTH, CONSOLE_HEIGHT, "terminal8x8.png")
.with_fps_cap(FRAME_DURATION)
.build()?;
main_loop(context, State::new())
}
There is a little bit of code not added here but everything that changed is here. Test it out!!!
cargo run
Adding the Adventurer
We need to add the adventurer, track the player position, move the player around based off of keyboard inputs, deal with player interactions with at least walls so far as well.
Extend the Map API
Let’s start by adding in some checks for player bounds. Then we can add the ability to check if a point is available to move into and then finally combine them into 1 function.
impl Map {
...
pub fn in_bounds(&self, point: Point) -> bool {
point.x >= 0 && point.x < CONSOLE_WIDTH && point.y >= 0 && point.y < CONSOLE_HEIGHT
}
pub fn can_enter_tile(&self, point: Point) -> bool {
self.in_bounds(point) && self.tiles[map_idx(point.x, point.y)] == TileType::Floor
}
pub fn try_idx(&self, point: Point) -> Option<usize> {
if !self.in_bounds(point) {
None
} else {
Some(map_idx(point.x, point.y))
}
}
}
You can see that we have built the needed functions to check for those things we talked about above.
Create the Player Struct
Let’s first start by adding the following to your main.rs, and then create a new file in the src folder called player.rs.
mod map;
mod player;
mod prelude {
pub use bracket_lib::prelude::*;
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 use crate::map::*;
pub use crate::player::*;
}
Now let’s work on the player.rs add this simple version of the player to the file player.rs
use crate::prelude::*;
pub struct Player {
pub position: Point,
}
impl Player {
pub fn new(position: Point) -> Self {
Self { position }
}
}
Point comes from bracket-lib just so you know.
Render the Player
Now we can add in the Player::render() to the implementation.
pub fn render(&self, ctx: &mut BTerm) {
ctx.set(
self.position.x,
self.position.y,
WHITE,
BLACK,
to_cp437('@'),
);
}
Notice that its simply a matter of rendering the player at the location with color, also notice that this is a public function.
Move the Player
Let’s add in a way to move the Player.
pub fn update(&mut self, ctx: &mut BTerm, map: &Map) {
if let Some(key) = ctx.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::zero(),
};
let new_position = self.position + delta;
if map.can_enter_tile(new_position) {
self.position = new_position;
}
}
}
Okay so we now have the ability to detect input and render the player let’s add this to the consume that new API
Consume the Player API
struct State {
frame_time: f32,
mode: GameMode,
map: Map,
player: Player,
}
impl State {
fn new() -> Self {
State {
frame_time: 0.0,
mode: GameMode::OnMap,
map: Map::new(),
player: Player::new(Point::new(CONSOLE_WIDTH / 2, CONSOLE_HEIGHT / 2)),
}
}
...
fn on_map(&mut self, ctx: &mut BTerm) {
ctx.cls();
self.map.render(ctx);
self.player.update(ctx, &self.map);
self.player.render(ctx);
}
}
Test it out cargo run
Building a Dungeon
Now we get to work on the logic to build a dungeon and have it be randomized.
Create a Map Builder Module
Create a map_builder.rs module. Be sure to add it to your main.rs prelude as well.
mod map;
mod player;
mod map_builder;
mod prelude {
pub use bracket_lib::prelude::*;
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 use crate::map::*;
pub use crate::player::*;
pub use crate::map_builder::*;
}
Now let’s work on the map builder module.
use crate::prelude::*;
const NUM_ROOMS: usize = 20;
pub struct MapBuilder {
pub map: Map,
pub rooms: Vec<Rect>,
pub player_start: Point,
}
Map is the copy of the map that the builder will work on, rooms will use the Rect from bracket-lib to build rooms, and player_start will be the location the player will start the dungeon.
Fill the Map With Walls
impl MapBuilder {
fn fill(&mut self, tile: TileType) {
self.map.tiles.iter_mut().for_each(|t| *t = tile);
}
}
This uses the the iter_mut() and the .for_each one will pull all the tiles and the other will iterate through them. Once we have the tile |t| we need to say that we want to not use the reference but the actual tile needs to be changed, that is done with the *
Carving Rooms
fn build_random_rooms(&mut self, rng: &mut RandomNumberGenerator) {
while self.rooms.len() < NUM_ROOMS {
let room = Rect::with_size(
rng.range(1, CONSOLE_WIDTH - 10),
rng.range(1, CONSOLE_HEIGHT - 10),
rng.range(2, 10),
rng.range(2, 10),
);
let mut overlap = false;
for r in self.rooms.iter() {
if r.intersect(&room) {
overlap = true;
}
}
if !overlap {
room.for_each(|p| {
if p.x > 0 && p.x < CONSOLE_WIDTH && p.y > 0 && p.y < CONSOLE_HEIGHT {
let idx = map_idx(p.x, p.y);
self.map.tiles[idx] = TileType::Floor;
}
});
self.rooms.push(room)
}
}
}
This is a set of functions that will build a random room of size 2-10 on either side. Once the rectangle is set we then need to test if it intersects with any other room we have built. It if passed the intersection test it will make all the points on the rect into floors, this assumes that everything was a wall to begin with. This will continue until there is NUM_ROOMS amount of rooms.
Carving Corridors
We will try to be sure to build dog-leg corridors from one to the other. We will have a horizontal and vertical set of corridors that we will build.
fn apply_vertical_tunnel(&mut self, y1: i32, y2: i32, x: i32) {
use std::cmp::{max, min};
for y in min(y1, y2)..=max(y1, y2) {
if let Some(idx) = self.map.try_idx(Point::new(x, y)) {
self.map.tiles[idx as usize] = TileType::Floor;
}
}
}
fn apply_horizontal_tunnel(&mut self, x1: i32, x2: i32, y: i32) {
use std::cmp::{max, min};
for x in min(x1, x2)..=max(x1, x2) {
if let Some(idx) = self.map.try_idx(Point::new(x, y)) {
self.map.tiles[idx as usize] = TileType::Floor;
}
}
}
fn build_corridors(&mut self, rng: &mut RandomNumberGenerator) {
let mut rooms = self.rooms.clone();
rooms.sort_by(|a, b| a.center().x.cmp(&b.center().x));
for (i, room) in rooms.iter().enumerate().skip(1) {
let prev = rooms[i - 1].center();
let new = room.center();
if rng.range(0, 2) == 1 {
self.apply_horizontal_tunnel(prev.x, new.x, prev.y);
self.apply_vertical_tunnel(prev.y, new.y, new.x);
} else {
self.apply_vertical_tunnel(prev.y, new.y, prev.x);
self.apply_horizontal_tunnel(prev.x, new.x, new.y);
}
}
}
This might look intimidating but keep in mind we are only trying to build floors from a map that is entirely walls. We will need to simple add floors that connect one room to the next. We sorted the rooms based off of there centers so we can simple connect one room to the next and then everything will be set. We have a simple check for rng to see if we to the vertical or horizontal first.
There are 2 functions that I want to go over here. Keeping in mind that center comes with bracket-lib
/// sort by will allow us to compare elements
/// we take the center to better maker sure that we are connecting rooms that are close.
/// cmp() is a compare function we are saying a.center() and b.center compare and see if they are the same or
/// which one is larger.
rooms.sort_by(|a,b| a.center().x.cmp(&b.center().x));
/// iter() will go through the elements of the data
/// enumerate will counts the items and includes the index as the first item in the tuple that it returns
for (i, room) in rooms.iter().enumerate().skip(1) {
Build and the Map and Place the Player
Let’s make the new implementation for the map_builder struct.
pub fn new(rng: &mut RandomNumberGenerator) -> Self {
let mut mb = MapBuilder {
map: Map::new(),
rooms: Vec::new(),
player_start: Point::zero(),
};
mb.fill(TileType::Wall);
mb.build_random_rooms(rng);
mb.build_corridors(rng);
mb.player_start = mb.rooms[0].center();
mb
}
You will be placed in the starting room that should be at the top left of the dungeon.
Consume the MapBuilder API
You have done this before but here we go.
impl State {
fn new() -> Self {
let mut rng = RandomNumberGenerator::new();
let map_builder = MapBuilder::new(&mut rng);
Self {
frame_time: 0.0,
mode: GameMode::OnMap,
map: map_builder.map,
player: Player::new(map_builder.player_start),
}
}
...
}
Test it out cargo run
Graphics, Camera, Action
Here is where we get to change things up and make them look a lot better.
Programmer Art for the Dungeon
So when we use bracket-lib in order to put symbols on the screen it is taking a list of fonts and simply rendering them on the screen. We can use this to our advantage by replacing the call to @ with a different image. We can then replace them all with different images. Create a directory called resources within the main directory for the rouge-like.
Then you can add that to the font with the following lines added to your BTermBuilder
.with_resource_path("resources/")
.with_font("dungeonfont.png", 32, 32)
.with_simple_console(CONSOLE_WIDTH, CONSOLE_HEIGHT, "dungeonfont.png")
Graphic Layers
Right now we have a simple 1 large map with the player being a small part of that. We also display the map and then display the character on top of it. We can do better with adding in layers. Let’s do some clean up and get ready to add in layers.
/// Add in new constants to only display half the console
pub const DISPLAY_WIDTH: i32 = CONSOLE_WIDTH / 2;
pub const DISPLAY_HEIGHT: i32 = CONSOLE_HEIGHT / 2;
/// Now add in an other console layer.
fn main() -> BError {
let context = BTermBuilder::new()
.with_title("Rusty Roguelike")
.with_fps_cap(FRAME_DURATION)
.with_dimensions(CONSOLE_WIDTH, CONSOLE_HEIGHT)
.with_tile_dimensions(TILE_SIZE, TILE_SIZE)
.with_resource_path("resources/")
.with_font("dungeonfont.png", 32, 32)
.with_simple_console(DISPLAY_WIDTH, DISPLAY_HEIGHT, "dungeonfont.png")
.with_simple_console_no_bg(DISPLAY_WIDTH, DISPLAY_HEIGHT, "dungeonfont.png")
// .with_fancy_console(CONSOLE_WIDTH, CONSOLE_HEIGHT, "terminal8x8.png")
.build()?;
main_loop(context, State::new())
}
Make a Camera
Now we get to build the camera that will only display the part of the map the player is on. First create a new file camera.rs within the src folder.
use crate::prelude::*;
pub struct Camera {
pub left_x: i32,
pub right_x: i32,
pub top_y: i32,
pub bottom_y: i32,
}
impl Camera {
pub fn new(player_position: Point) -> Self {
Self {
left_x: player_position.x - DISPLAY_WIDTH / 2,
right_x: player_position.x + DISPLAY_WIDTH / 2,
top_y: player_position.y - DISPLAY_HEIGHT / 2,
bottom_y: player_position.y + DISPLAY_HEIGHT / 2,
}
}
pub fn on_player_move(&mut self, player_position: Point) {
self.left_x = player_position.x - DISPLAY_WIDTH / 2;
self.right_x = player_position.x + DISPLAY_WIDTH / 2;
self.top_y = player_position.y - DISPLAY_HEIGHT / 2;
self.bottom_y = player_position.y + DISPLAY_HEIGHT / 2;
}
}
Now this will build the window around the player so that they will only see a portion of the map and be able to see it far better.
Now you can consume the new module and then start to use it.
...
mod camera;
mod prelude {
pub use bracket_lib::prelude::*;
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::*;
pub use crate::player::*;
}
use prelude::*;
struct State {
frame_time: f32,
mode: GameMode,
map: Map,
player: Player,
camera: Camera,
}
impl State {
fn new() -> Self {
let mut rng = RandomNumberGenerator::new();
let map_builder = MapBuilder::new(&mut rng);
Self {
frame_time: 0.0,
mode: GameMode::OnMap,
map: map_builder.map,
player: Player::new(map_builder.player_start),
camera: Camera::new(map_builder.player_start),
}
}
...
}
Use the Camera for Rendering the Map
Okay so now we have to things to do make sure the active console it set the bg and then make sure the map will render the right part of the map. Head back to map.rs
pub fn render(&self, ctx: &mut BTerm, camera: &Camera) {
ctx.set_active_console(0);
for y in camera.top_y..camera.bottom_y {
for x in camera.left_x..camera.right_x {
if self.in_bounds(Point::new(x, y)) {
let idx = map_idx(x, y);
match self.tiles[idx] {
TileType::Floor => {
ctx.set(
x - camera.left_x,
y - camera.top_y,
WHITE,
BLACK,
to_cp437('.'),
);
}
TileType::Wall => {
ctx.set(
x - camera.left_x,
y - camera.top_y,
WHITE,
BLACK,
to_cp437('#'),
);
}
}
}
}
}
}
pub fn in_bounds(&self, point: Point) -> bool {
point.x >= 0 && point.x < CONSOLE_WIDTH && point.y >= 0 && point.y < CONSOLE_HEIGHT
}
We replaced the functionality of the simple render to something that will only render the location of the player plus half the map size. We also made a change to the in_bounds as when you are using a different set of coordinates you could run into issues.
Connect the Player to the Camera
Now we need to make sure that we update the Camera with the player. We will do this by also passing the Camera to the player update.
impl Player {
...
pub fn render(&self, ctx: &mut BTerm, camera: &Camera) {
ctx.set_active_console(1);
ctx.set(
self.position.x - camera.left_x,
self.position.y - camera.top_y,
WHITE,
BLACK,
to_cp437('@'),
);
}
pub fn update(&mut self, ctx: &mut BTerm, map: &Map, camera: &mut Camera) {
if let Some(key) = ctx.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::zero(),
};
let new_position = self.position + delta;
if map.can_enter_tile(new_position) {
self.position = new_position;
camera.on_player_move(new_position)
}
}
}
}
We now will update the Camera that will be mutable so that we can then update it as needed when we update the player. We also set the active console to 1 as we update the player.
Clear Layer, Connect Functions
This is the last step to take care of the needed functionality. Let’s update the on_map to reflect the current state of the code.
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)
}
Test it out!!! cargo run
Wrap-Up
You did it!!! Now we can start to add enemies and much more to the dungeon take a minute to really get a good hold on what you have done.