Crawler
Crawler
Crawler
Hands-on Rust
Effective Learning through 2D Game Development and Play
This PDF file contains pages extracted from Hands-on Rust, published by the
Pragmatic Bookshelf. For more information or to purchase a paperback or PDF
copy, please visit http://www.pragprog.com.
Note: This extract contains some colored text (particularly in code listing). This
is available only in online versions of the books. The printed versions are black
and white. Pagination might vary between the online and printed versions; the
content is otherwise identical.
Copyright © 2021 The Pragmatic Programmers, LLC.
Herbert Wolverson
All rights reserved. No part of this publication may be reproduced, stored in a retrieval system,
or transmitted, in any form, or by any means, electronic, mechanical, photocopying, recording,
or otherwise, without the prior consent of the publisher.
ISBN-13: 978-1-68050-816-1
Encoded using the finest acid-free high-entropy binary digits.
Book version: P1.0—July 2021
Graphics, Camera, Action
ASCII is a great prototyping tool—even used for some full games such as Rogue
or Nethack. Most games feature graphics, but this early in development isn’t
the right time to find an artist and make beautiful content—you might decide
to change the game and waste hours of the artist’s work. In early development,
it’s a much better idea to use Programmer Art—rough graphics designed to
give you an idea for the feel of a game, but without requiring a significant
time investment if (when) you decide to change things.
Create a new directory named resources in your project’s root directory. This
directory will hold all graphical resources needed for the game. Copy dungeon-
font.png into this directory. (It’s already present in the example source code.)
Rather than recreate this file throughout the book, the file includes all of the
graphics needed for the game.
/ Huge Sword
The font file with all of the graphic elements defined looks like this:
6. https://www.gimp.org/
The dungeon floor, wall, and adventurer graphics were kindly provided by
Buch for free.7 Potion and scroll graphics are from Melissa Krautheim’s Fan-
tasy Magic Set.8 Weaponry is from Melle’s Fantasy Sword Set.9 Monster
graphics are from the game Dungeon Crawl Stone Soup (CC0 license), packaged
by Chris Hamons.10
Graphics Layers
Currently, your game renders everything to a single layer. The map is drawn
and then the player is drawn on top of it. This works with graphics but tends
to leave artifacts around the player’s graphic. You can get much better results
7. https://opengameart.org/content/unfinished-dungeon-tileset
8. https://opengameart.org/content/fantasy-magic-set
9. https://opengameart.org/content/fantasy-sword-set
10. https://github.com/crawl/tiles
by using layers. The map is rendered to a base layer, and the player to the
layer on top of it—with transparency, so the floor remains visible. Later in
this book, you’ll add a third layer for game information.
Start with a little housekeeping. Using large tiles makes the window
huge—larger than many screens. Instead, render the game window as a
smaller view of part of the map, centered on the player. Add some constants
to your prelude in main.rs to indicate the dimensions of the smaller viewport
into your world:
BasicDungeonCrawler/dungeon_crawl_graphics/src/main.rs
pub const DISPLAY_WIDTH: i32 = SCREEN_WIDTH / 2;
pub const DISPLAY_HEIGHT: i32 = SCREEN_HEIGHT / 2;
❸ The tile dimensions are the size of each character in your font file, in this
case 32x32.
❺ The name of the font file to load and the character dimensions. These are
usually the same as tile dimensions, but can be different for some
advanced forms of rendering.
❻ Add a console using the dimensions already specified and the named tile
graphics file.
This code creates a terminal with two console layers, one for the map and
one for the player. You won’t be rendering the whole map at once—and to
limit the viewport, you use a camera.
Make a Camera
The camera acts as your game’s window into the world. It defines the section
of the map that is currently visible. Create a new file, camera.rs. Import your
prelude, and create a structure with enough information to define the
boundaries of the camera view:
BasicDungeonCrawler/dungeon_crawl_graphics/src/camera.rs
use crate::prelude::*;
You need to be able to create a camera and update it when the player moves.
Because the camera is centered on the player, you need the player’s position
for both of these functions:
BasicDungeonCrawler/dungeon_crawl_graphics/src/camera.rs
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
}
}
The new and on_player_move functions are essentially the same: they define the
visible window as being centered on the player. The left-most visible tile is
the player’s x coordinate, minus half of the screen size. The right-most visible
tile is the x coordinate plus one half of the screen size. The y dimensions are
the same, but with screen height.
Add the camera structure to your prelude and module imports in main.rs:
mod camera;
mod prelude {
...
pub use crate::camera::*;
}
You also need to update your state’s new function to initialize the camera:
BasicDungeonCrawler/dungeon_crawl_graphics/src/main.rs
fn new() -> Self {
let mut rng = RandomNumberGenerator::new();
let map_builder = MapBuilder::new(&mut rng);
Self {
map : map_builder.map,
player: Player::new(map_builder.player_start),
➤ camera: Camera::new(map_builder.player_start)
}
}
}
TileType::Wall => {
ctx.set(
x - camera.left_x,
y - camera.top_y,
WHITE,
BLACK,
to_cp437('#')
);
}
}
}
}
}
}
The function receives a borrowed Camera, and uses the boundaries from the
camera to render just the visible part of the map. Notice that it now calls
in_bounds to make sure that each tile exists. The screen coordinates sent to the
set function have left_x and top_y subtracted from them—moving them to be
relative to the camera. Notice that it calls set_active_console(0)—this tells the
library to render to the first console layer, the base map.
BasicDungeonCrawler/dungeon_crawl_graphics/src/player.rs
pub fn update(&mut self, ctx: &mut BTerm, map : &Map, camera: &mut Camera)
{
Notice that it receives a mutable camera—it will use it to send updates if the
player moves:
BasicDungeonCrawler/dungeon_crawl_graphics/src/player.rs
if map.can_enter_tile(new_position) {
self.position = new_position;
camera.on_player_move(new_position);
}
Lastly, for the player, you need to update the render() function to take into
account camera placement:
BasicDungeonCrawler/dungeon_crawl_graphics/src/player.rs
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('@'),
);
}
Just like the map, this subtracts left_x and top_y from the player’s coordinates
when rendering. Notice the call to set_active_console. This specifies that you want
to use the second layer for the player.