将地图尺寸与终端尺寸解耦


关于本教程

本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。我希望您会喜欢本教程,并制作出色的游戏!

如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon

Hands-On Rust


到目前为止,我们已经将地图尺寸牢牢地绑定到终端分辨率。您有一个 80x50 的屏幕,并使用几行来显示用户界面 - 所以我们制作的所有东西都是 80 个瓦片宽和 43 个瓦片高。正如您在之前的章节中看到的,您可以使用 3,440 个瓦片做 很多 事情 - 但有时您想要更多(有时您想要更少)。您可能还想要一个大的、开放的世界设定 - 但我们现在还不打算涉及那里!本章将首先将相机地图解耦,然后使地图尺寸和屏幕尺寸能够不同。用户界面大小调整这个难题将留待未来开发。

引入相机

游戏中常见的抽象概念是将您正在查看的内容(地图和实体)与您如何查看它(相机)分离开来。相机通常跟随您勇敢的冒险家在地图上移动,从他们的角度向您展示世界。在 3D 游戏中,相机可能非常复杂;在自上而下的 Roguelike 游戏中(从上方查看地图),它通常将视图中心对准玩家的 @

可以预见的是,我们将从创建一个新文件开始:camera.rs。为了启用它,在 main.rs 的顶部附近添加 pub mod camera(与其他模块访问方式相同)。

我们将从创建一个函数 render_camera 开始,并进行一些我们需要的计算:

#![allow(unused)]
fn main() {
use specs::prelude::*;
use super::{Map,TileType,Position,Renderable,Hidden};
use rltk::{Point, Rltk, RGB};

const SHOW_BOUNDARIES : bool = true;

pub fn render_camera(ecs: &World, ctx : &mut Rltk) {
    let map = ecs.fetch::<Map>();
    let player_pos = ecs.fetch::<Point>();
    let (x_chars, y_chars) = ctx.get_char_size();

    let center_x = (x_chars / 2) as i32;
    let center_y = (y_chars / 2) as i32;

    let min_x = player_pos.x - center_x;
    let max_x = min_x + x_chars as i32;
    let min_y = player_pos.y - center_y;
    let max_y = min_.y + y_chars as i32;
    ...
}

我已将其分解为几个步骤,以清楚地说明正在发生的事情:

  1. 我们创建一个常量 SHOW_BOUNDARIES。如果为 true,我们将为超出边界的瓦片渲染一个标记,以便我们知道地图的边缘在哪里。大多数时候,这将是 false(不需要玩家获得该信息),但它对于调试非常方便。
  2. 我们首先从 ECS World 中检索地图。
  3. 然后,我们从 ECS World 中检索玩家的位置。
  4. 我们向 RLTK 请求当前的控制台尺寸,以字符空间为单位(因此对于 8x8 字体,为 80x50)。
  5. 我们计算控制台的中心。
  6. 我们将 min_x 设置为最左边的瓦片,相对于玩家。所以玩家的 x 位置,减去控制台的中心。这将使 x 轴以玩家为中心。
  7. 我们将 max_x 设置为 min_x 加上控制台宽度 - 再次,确保玩家居中。
  8. 我们对 min_ymax_y 执行相同的操作。

所以我们已经确定了相机在世界空间中的位置 - 即地图本身的坐标。我们还确定了使用我们的相机视图,这应该是渲染区域的中心。

现在我们将渲染实际的地图:

#![allow(unused)]
fn main() {
let map_width = map.width-1;
let map_height = map.height-1;

let mut y = 0;
for ty in min_y .. max_y {
    let mut x = 0;
    for tx in min_x .. max_x {
        if tx > 0 && tx < map_width && ty > 0 && ty < map_height {
            let idx = map.xy_idx(tx, ty);
            if map.revealed_tiles[idx] {
                let (glyph, fg, bg) = get_tile_glyph(idx, &*map);
                ctx.set(x, y, fg, bg, glyph);
            }
        } else if SHOW_BOUNDARIES {
            ctx.set(x, y, RGB::named(rltk::GRAY), RGB::named(rltk::BLACK), rltk::to_cp437('·'));
        }
        x += 1;
    }
    y += 1;
}
}

这与我们旧的 draw_map 代码类似,但稍微复杂一些。让我们逐步了解它:

  1. 我们将 y 设置为 0;我们使用 xy 来表示实际的屏幕坐标。
  2. 我们从 min_ymax_y 循环 ty。我们使用 txty 表示地图坐标 - 或“瓦片空间”坐标(因此为 t)。
    1. 我们将 x 设置为零,因为我们要在屏幕上开始新的一行。
    2. 我们在变量 tx 中从 min_xmax_x 循环 - 因此我们在 tx 中覆盖了可见的瓦片空间
      1. 我们进行裁剪检查。我们检查 txty 是否真的在地图边界内。玩家很可能会访问地图的边缘,您不希望因为他们可以看到不在地图区域内的瓦片而崩溃!
      2. 我们计算 tx/ty 位置的 idx(索引),告诉我们屏幕上的这个位置在地图上的哪里。
      3. 如果它是已显示的,我们为这个索引调用神秘的 get_tile_glyph 函数(稍后会详细介绍),并将结果设置在屏幕上。
      4. 如果瓦片超出地图范围且 SHOW_BOUNDARIEStrue - 我们绘制一个点。
      5. 无论是否裁剪,我们都将 x 加 1 - 我们正在移动到下一列。
    3. 我们将 y 加一,因为我们现在正在向下移动屏幕。
  3. 我们渲染了一张地图!

这实际上非常简单 - 我们渲染的实际上是一个窗口,它查看地图的一部分,而不是整个地图 - 并将窗口中心对准玩家。

接下来,我们需要渲染我们的实体:

#![allow(unused)]
fn main() {
let positions = ecs.read_storage::<Position>();
let renderables = ecs.read_storage::<Renderable>();
let hidden = ecs.read_storage::<Hidden>();
let map = ecs.fetch::<Map>();

let mut data = (&positions, &renderables, !&hidden).join().collect::<Vec<_>>();
data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order) );
for (pos, render, _hidden) in data.iter() {
    let idx = map.xy_idx(pos.x, pos.y);
    if map.visible_tiles[idx] {
        let entity_screen_x = pos.x - min_x;
        let entity_screen_y = pos.y - min_y;
        if entity_screen_x > 0 && entity_screen_x < map_width && entity_screen_y > 0 && entity_screen_y < map_height {
            ctx.set(entity_screen_x, entity_screen_y, render.fg, render.bg, render.glyph);
        }
    }
}
}

如果这看起来很眼熟,那是因为它与曾经存在于 main.rs 中的渲染代码相同。有两个主要的区别:我们从 xy 坐标中减去 min_xmin_y,以使实体与我们的相机视图对齐。我们还对坐标执行裁剪 - 我们不会尝试渲染任何不在屏幕上的东西。

我们之前提到了 get_tile_glyph,所以这里是它:

#![allow(unused)]
fn main() {
fn get_tile_glyph(idx: usize, map : &Map) -> (rltk::FontCharType, RGB, RGB) {
    let glyph;
    let mut fg;
    let mut bg = RGB::from_f32(0., 0., 0.);

    match map.tiles[idx] {
        TileType::Floor => {
            glyph = rltk::to_cp437('.');
            fg = RGB::from_f32(0.0, 0.5, 0.5);
        }
        TileType::Wall => {
            let x = idx as i32 % map.width;
            let y = idx as i32 / map.width;
            glyph = wall_glyph(&*map, x, y);
            fg = RGB::from_f32(0., 1.0, 0.);
        }
        TileType::DownStairs => {
            glyph = rltk::to_cp437('>');
            fg = RGB::from_f32(0., 1.0, 1.0);
        }
    }
    if map.bloodstains.contains(&idx) { bg = RGB::from_f32(0.75, 0., 0.); }
    if !map.visible_tiles[idx] {
        fg = fg.to_greyscale();
        bg = RGB::from_f32(0., 0., 0.); // 不显示视野范围外的血迹
    }

    (glyph, fg, bg)
}
}

这与我们很久以前编写的 draw_map 中的代码非常相似,但它不是绘制到地图,而是返回字形、前景色和背景色。它仍然处理血迹,灰化您看不到的区域,并为漂亮的墙壁调用 wall_glyph。我们只是从 map.rs 中复制了 wall_glyph

#![allow(unused)]
fn main() {
fn wall_glyph(map : &Map, x: i32, y:i32) -> rltk::FontCharType {
    if x < 1 || x > map.width-2 || y < 1 || y > map.height-2 as i32 { return 35; }
    let mut mask : u8 = 0;

    if is_revealed_and_wall(map, x, y - 1) { mask +=1; }
    if is_revealed_and_wall(map, x, y + 1) { mask +=2; }
    if is_revealed_and_wall(map, x - 1, y) { mask +=4; }
    if is_revealed_and_wall(map, x + 1, y) { mask +=8; }

    match mask {
        0 => { 9 } // 柱子,因为我们看不到邻居
        1 => { 186 } // 仅北面有墙
        2 => { 186 } // 仅南面有墙
        3 => { 186 } // 北面和南面都有墙
        4 => { 205 } // 仅西面有墙
        5 => { 188 } // 北面和西面都有墙
        6 => { 187 } // 南面和西面都有墙
        7 => { 185 } // 北面、南面和西面都有墙
        8 => { 205 } // 仅东面有墙
        9 => { 200 } // 北面和东面都有墙
        10 => { 201 } // 南面和东面都有墙
        11 => { 204 } // 北面、南面和东面都有墙
        12 => { 205 } // 东面和西面都有墙
        13 => { 202 } // 东面、西面和南面都有墙
        14 => { 203 } // 东面、西面和北面都有墙
        15 => { 206 }  // ╬ 四面都有墙
        _ => { 35 } // 我们遗漏了一个?
    }
}

fn is_revealed_and_wall(map: &Map, x: i32, y: i32) -> bool {
    let idx = map.xy_idx(x, y);
    map.tiles[idx] == TileType::Wall && map.revealed_tiles[idx]
}
}

最后,在 main.rs 中找到以下代码:

#![allow(unused)]
fn main() {
...
RunState::GameOver{..} => {}
_ => {
    draw_map(&self.ecs.fetch::<Map>(), ctx);
    let positions = self.ecs.read_storage::<Position>();
    let renderables = self.ecs.read_storage::<Renderable>();
    let hidden = self.ecs.read_storage::<Hidden>();
    let map = self.ecs.fetch::<Map>();

    let mut data = (&positions, &renderables, !&hidden).join().collect::<Vec<_>>();
    data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order) );
    for (pos, render, _hidden) in data.iter() {
        let idx = map.xy_idx(pos.x, pos.y);
        if map.visible_tiles[idx] { ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph) }
    }
    gui::draw_ui(&self.ecs, ctx);
}
...
}

我们现在可以用一段更短的代码替换它:

#![allow(unused)]
fn main() {
RunState::GameOver{..} => {}
_ => {
    camera::render_camera(&self.ecs, ctx);
    gui::draw_ui(&self.ecs, ctx);
}
}

如果您现在 cargo run 项目,您将看到我们仍然可以玩 - 并且相机以玩家为中心:

Screenshot

糟糕 - 我们没有移动工具提示或目标!

如果您玩一会儿,您可能会注意到工具提示不起作用(它们仍然绑定到地图坐标)。我们应该修复它!首先,屏幕边界显然是我们不仅在绘制代码中需要的东西,所以让我们把它分解成 camera.rs 中的一个单独的函数:

#![allow(unused)]
fn main() {
pub fn get_screen_bounds(ecs: &World, ctx : &mut Rltk) -> (i32, i32, i32, i32) {
    let player_pos = ecs.fetch::<Point>();
    let (x_chars, y_chars) = ctx.get_char_size();

    let center_x = (x_chars / 2) as i32;
    let center_y = (y_chars / 2) as i32;

    let min_x = player_pos.x - center_x;
    let max_x = min_x + x_chars as i32;
    let min_y = player_pos.y - center_y;
    let max_y = min_y + y_chars as i32;

    (min_x, max_x, min_y, max_y)
}

pub fn render_camera(ecs: &World, ctx : &mut Rltk) {
    let map = ecs.fetch::<Map>();
    let (min_x, max_x, min_y, max_y) = get_screen_bounds(ecs, ctx);
}

它是 render_camera 中的相同代码 - 只是移动到一个函数中。我们还扩展了 render_camera 以使用该函数,而不是重复我们自己。现在我们可以进入 gui.rs 并轻松编辑 draw_tooltips 以使用相机位置:

#![allow(unused)]
fn main() {
fn draw_tooltips(ecs: &World, ctx : &mut Rltk) {
    let (min_x, _max_x, min_y, _max_y) = camera::get_screen_bounds(ecs, ctx);
    let map = ecs.fetch::<Map>();
    let names = ecs.read_storage::<Name>();
    let positions = ecs.read_storage::<Position>();
    let hidden = ecs.read_storage::<Hidden>();

    let mouse_pos = ctx.mouse_pos();
    let mut mouse_map_pos = mouse_pos;
    mouse_map_pos.0 += min_x;
    mouse_map_pos.1 += min_y;
    if mouse_map_pos.0 >= map.width-1 || mouse_map_pos.1 >= map.height-1 || mouse_map_pos.0 < 1 || mouse_map_pos.1 < 1
    {
        return;
    }
    if !map.visible_tiles[map.xy_idx(mouse_map_pos.0, mouse_map_pos.1)] { return; }
    let mut tooltip : Vec<String> = Vec::new();
    for (name, position, _hidden) in (&names, &positions, !&hidden).join() {
        if position.x == mouse_map_pos.0 && position.y == mouse_map_pos.1 {
            tooltip.push(name.name.to_string());
        }
    }
    ...
}

所以我们的更改是:

  1. 在开始时,我们使用 camera::get_screen_bounds 检索屏幕边界。我们不打算使用 max 变量,所以我们在它们前面加上下划线,让 Rust 知道我们有意忽略它们。
  2. 在获取 mouse_pos 后,我们创建一个新的 mouse_map_pos 变量。它等于 mouse_pos,但我们添加min_xmin_y 值 - 将其偏移以匹配可见坐标。
  3. 我们扩展了裁剪以检查所有方向,因此当您查看实际地图之外的区域时,工具提示不会使游戏崩溃,因为视口位于地图的极端末端。
  4. 我们对 position 的比较现在与 mouse_map_pos 而不是 mouse_pos 进行比较。
  5. 就这样 - 其余的可以保持不变。

如果您现在 cargo run,工具提示将起作用:

Screenshot

修复目标

如果您玩一会儿,您还会注意到,如果您尝试使用火球或类似效果 - 目标系统完全失灵了。它仍然引用屏幕/地图位置,因为它们曾经直接链接。所以您看到了可用的瓦片,但它们的位置完全错误!我们也应该修复它。

gui.rs 中,我们将编辑函数 ranged_target

#![allow(unused)]
fn main() {
pub fn ranged_target(gs : &mut State, ctx : &mut Rltk, range : i32) -> (ItemMenuResult, Option<Point>) {
    let (min_x, max_x, min_y, max_y) = camera::get_screen_bounds(&gs.ecs, ctx);
    let player_entity = gs.ecs.fetch::<Entity>();
    let player_pos = gs.ecs.fetch::<Point>();
    let viewsheds = gs.ecs.read_storage::<Viewshed>();

    ctx.print_color(5, 0, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Select Target:");

    // 高亮显示可用的目标单元格
    let mut available_cells = Vec::new();
    let visible = viewsheds.get(*player_entity);
    if let Some(visible) = visible {
        // 我们有一个视野
        for idx in visible.visible_tiles.iter() {
            let distance = rltk::DistanceAlg::Pythagoras.distance2d(*player_pos, *idx);
            if distance <= range as f32 {
                let screen_x = idx.x - min_x;
                let screen_y = idx.y - min_y;
                if screen_x > 1 && screen_x < (max_x - min_x)-1 && screen_y > 1 && screen_y < (max_y - min_y)-1 {
                    ctx.set_bg(screen_x, screen_y, RGB::named(rltk::BLUE));
                    available_cells.push(idx);
                }
            }
        }
    } else {
        return (ItemMenuResult::Cancel, None);
    }

    // 绘制鼠标光标
    let mouse_pos = ctx.mouse_pos();
    let mut mouse_map_pos = mouse_pos;
    mouse_map_pos.0 += min_x;
    mouse_map_pos.1 += min_y;
    let mut valid_target = false;
    for idx in available_cells.iter() { if idx.x == mouse_map_pos.0 && idx.y == mouse_map_pos.1 { valid_target = true; } }
    if valid_target {
        ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::CYAN));
        if ctx.left_click {
            return (ItemMenuResult::Selected, Some(Point::new(mouse_map_pos.0, mouse_map_pos.1)));
        }
    } else {
        ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::RED));
        if ctx.left_click {
            return (ItemMenuResult::Cancel, None);
        }
    }

    (ItemMenuResult::NoResponse, None)
}
}

这基本上是我们以前拥有的,但做了一些更改:

  1. 我们在开始时获取边界,再次使用 camera::get_screen_bounds
  2. 在我们的可见目标瓦片部分中,我们通过获取地图索引并添加我们的 min_xmin_y 值来计算 screen_xscreen_y。然后,我们检查它是否在屏幕上,然后在这些位置绘制目标高亮。
  3. 在计算 mouse_pos 后,我们使用相同的 mouse_map_pos 计算。
  4. 然后,我们在检查目标是否在鼠标下方或被选中时引用 mouse_map_pos

如果您现在 cargo run,目标将起作用:

Screenshot

可变的地图尺寸

现在我们的地图没有直接链接到我们的屏幕,我们可以拥有任何我们想要的尺寸的地图!温馨提示:如果您使用巨大的地图,您的玩家将需要很长时间才能探索完所有地图 - 并且越来越难以确保所有地图都足够有趣,值得访问。

一个简单的开始

让我们从最简单的例子开始:全局更改地图的大小。转到 map.rs,找到常量 MAPWIDTHMAPHEIGHTMAPCOUNT。让我们将它们更改为方形地图:

#![allow(unused)]
fn main() {
pub const MAPWIDTH : usize = 64;
pub const MAPHEIGHT : usize = 64;
pub const MAPCOUNT : usize = MAPHEIGHT * MAPWIDTH;
}

如果您 cargo run 该项目,它应该可以工作 - 我们在整个程序中都很好地使用了 map.width/map.height 或这些常量。这些算法运行,并尝试为您制作一张地图。这是我们的玩家在 64x64 地图上漫游 - 请注意地图的侧面是如何显示为超出边界的:

Screenshot

更难:删除常量

现在从 map.rs删除这三个常量,并观察您的 IDE 将世界涂成红色。在我们开始修复问题之前,我们将添加更多的红色:

#![allow(unused)]
fn main() {
/// 生成一个空地图,完全由实心墙壁组成
pub fn new(new_depth : i32, width: i32, height: i32) -> Map {
    Map{
        tiles : vec![TileType::Wall; MAPCOUNT],
        width,
        height,
        revealed_tiles : vec![false; MAPCOUNT],
        visible_tiles : vec![false; MAPCOUNT],
        blocked : vec![false; MAPCOUNT],
        tile_content : vec![Vec::new(); MAPCOUNT],
        depth: new_depth,
        bloodstains: HashSet::new(),
        view_blocked : HashSet::new()
    }
}
}

现在创建地图需要您指定大小和深度。我们可以通过再次更改构造函数以在创建各种向量时使用指定的大小,来开始修复一些错误:

#![allow(unused)]
fn main() {
pub fn new(new_depth : i32, width: i32, height: i32) -> Map {
    let map_tile_count = (width*height) as usize;
    Map{
        tiles : vec![TileType::Wall; map_tile_count],
        width,
        height,
        revealed_tiles : vec![false; map_tile_count],
        visible_tiles : vec![false; map_tile_count],
        blocked : vec![false; map_tile_count],
        tile_content : vec![Vec::new(); map_tile_count],
        depth: new_depth,
        bloodstains: HashSet::new(),
        view_blocked : HashSet::new()
    }
}
}

map.rsdraw_map 中也有一个错误。幸运的是,这是一个简单的修复:

#![allow(unused)]
fn main() {
...
// 移动坐标
x += 1;
if x > (map.width * map.height) as i32-1 {
    x = 0;
    y += 1;
}
...
}

spawner.rs 也是一个同样容易修复的问题。从开头的 use 导入列表中删除 map::MAPWIDTH,并找到 spawn_entity 函数。我们可以直接从 ECS 获取地图宽度:

#![allow(unused)]
fn main() {
pub fn spawn_entity(ecs: &mut World, spawn : &(&usize, &String)) {
    let map = ecs.fetch::<Map>();
    let width = map.width as usize;
    let x = (*spawn.0 % width) as i32;
    let y = (*spawn.0 / width) as i32;
    std::mem::drop(map);
    ...
}

saveload_system.rs 中的问题也很容易修复。在第 102 行左右,您可以将 MAPCOUNT 替换为 (worldmap.width * worldmap.height) as usize

#![allow(unused)]
fn main() {
...
let mut deleteme : Option<Entity> = None;
{
    let entities = ecs.entities();
    let helper = ecs.read_storage::<SerializationHelper>();
    let player = ecs.read_storage::<Player>();
    let position = ecs.read_storage::<Position>();
    for (e,h) in (&entities, &helper).join() {
        let mut worldmap = ecs.write_resource::<super::map::Map>();
        *worldmap = h.map.clone();
        worldmap.tile_content = vec![Vec::new(); (worldmap.height * worldmap.width) as usize];
        deleteme = Some(e);
    }
    ...
}

main.rs 也需要一些帮助。在 tick 中,MagicMapReveal 代码是一个简单的修复:

#![allow(unused)]
fn main() {
RunState::MagicMapReveal{row} => {
    let mut map = self.ecs.fetch_mut::<Map>();
    for x in 0..map.width {
        let idx = map.xy_idx(x as i32,row);
        map.revealed_tiles[idx] = true;
    }
    if row == map.height-1 {
        newrunstate = RunState::MonsterTurn;
    } else {
        newrunstate = RunState::MagicMapReveal{ row: row+1 };
    }
}
}

在第 451 行附近,我们也在用 map::new(1) 创建地图。我们想在这里引入地图尺寸,所以我们使用 map::new(1, 64, 64)(尺寸并不重要,因为我们无论如何都会用来自构建器的地图替换它)。

打开 player.rs,您会发现我们犯了一个真正的编程罪过。我们硬编码了 7949 作为玩家移动的地图边界!让我们修复它:

#![allow(unused)]
fn main() {
if !map.blocked[destination_idx] {
pos.x = min(map.width-1 , max(0, pos.x + delta_x));
pos.y = min(map.height-1, max(0, pos.y + delta_y));
}

最后,展开我们的 map_builders 文件夹会显示一些错误。我们将在修复它们之前再引入几个错误!在 map_builders/mod.rs 中,我们将存储请求的地图大小:

#![allow(unused)]
fn main() {
pub struct BuilderMap {
    pub spawn_list : Vec<(usize, String)>,
    pub map : Map,
    pub starting_position : Option<Position>,
    pub rooms: Option<Vec<Rect>>,
    pub corridors: Option<Vec<Vec<usize>>>,
    pub history : Vec<Map>,
    pub width: i32,
    pub height: i32
}
}

然后我们将更新构造函数以使用它:

#![allow(unused)]
fn main() {
impl BuilderChain {
    pub fn new(new_depth : i32, width: i32, height: i32) -> BuilderChain {
        BuilderChain{
            starter: None,
            builders: Vec::new(),
            build_data : BuilderMap {
                spawn_list: Vec::new(),
                map: Map::new(new_depth, width, height),
                starting_position: None,
                rooms: None,
                corridors: None,
                history : Vec::new(),
                width,
                height
            }
        }
    }
}

我们还需要调整 random_builder 的签名以接受地图大小:

#![allow(unused)]
fn main() {
pub fn random_builder(new_depth: i32, rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
    let mut builder = BuilderChain::new(new_depth, width, height);
...
}

我们还将访问 map_builders/waveform_collapse/mod.rs 并进行一些修复。基本上,我们对 Map::new 的所有引用都需要包括新的大小。

最后,回到 main.rs,在第 370 行左右,您会找到我们对 random_builder 的调用。我们需要向其中添加宽度和高度;现在,我们将使用 64x64:

#![allow(unused)]
fn main() {
let mut builder = map_builders::random_builder(new_depth, &mut rng, 64, 64);
}

就是这样!如果您现在 cargo run 该项目,您可以漫游 64x64 的地图:

Screenshot

如果您将该行更改为不同的尺寸,则可以漫游巨大的地图:

#![allow(unused)]
fn main() {
let mut builder = map_builders::random_builder(new_depth, &mut rng, 128, 128);
}

瞧 - 您正在漫游一张巨大的地图!巨大地图的明显缺点,以及滚动一个大部分开放区域的缺点是,有时它可能真的很难生存:

Screenshot

重新访问 draw_map 以进行渐进式地图渲染。

如果您保留巨大的地图,打开 main.rs 并将 const SHOW_MAPGEN_VISUALIZER : bool = false; 设置为 true - 恭喜您,您刚刚使游戏崩溃了!这是因为我们从未调整我们用于验证地图创建的 draw_map 函数来处理原始尺寸以外的任何尺寸的地图。哎呀。这确实提出了一个问题:在 ASCII 终端上,我们不能简单地渲染整个地图并将其缩小以适应。因此,我们将满足于渲染地图的一部分。

我们将在 camera.rs 中添加一个新函数:

#![allow(unused)]
fn main() {
pub fn render_debug_map(map : &Map, ctx : &mut Rltk) {
    let player_pos = Point::new(map.width / 2, map.height / 2);
    let (x_chars, y_chars) = ctx.get_char_size();

    let center_x = (x_chars / 2) as i32;
    let center_y = (y_chars / 2) as i32;

    let min_x = player_pos.x - center_x;
    let max_x = min_x + x_chars as i32;
    let min_y = player_pos.y - center_y;
    let max_y = min_y + y_chars as i32;

    let map_width = map.width-1;
    let map_height = map.height-1;

    let mut y = 0;
    for ty in min_y .. max_y {
        let mut x = 0;
        for tx in min_x .. max_x {
            if tx > 0 && tx < map_width && ty > 0 && ty < map_height {
                let idx = map.xy_idx(tx, ty);
                if map.revealed_tiles[idx] {
                    let (glyph, fg, bg) = get_tile_glyph(idx, &*map);
                    ctx.set(x, y, fg, bg, glyph);
                }
            } else if SHOW_BOUNDARIES {
                ctx.set(x, y, RGB::named(rltk::GRAY), RGB::named(rltk::BLACK), rltk::to_cp437('·'));
            }
            x += 1;
        }
        y += 1;
    }
}
}

这很像我们常规的地图绘制,但我们将相机锁定在地图的中间 - 并且不渲染实体。

main.rs 中,将对 draw_map 的调用替换为:

#![allow(unused)]
fn main() {
if self.mapgen_index < self.mapgen_history.len() { camera::render_debug_map(&self.mapgen_history[self.mapgen_index], ctx); }
}

现在您可以进入 map.rs 并完全删除 draw_mapwall_glyphis_revealed_and_wall

总结

我们将在 main.rs 中将地图尺寸设置回合理的尺寸:

#![allow(unused)]
fn main() {
let mut builder = map_builders::random_builder(new_depth, &mut rng, 80, 50);
}

并且 - 我们完成了!在本章中,我们使拥有任何您喜欢的地图尺寸成为可能。我们最终恢复为“正常”尺寸 - 但我们将在未来发现此功能非常有用。我们可以放大或缩小地图 - 系统一点也不介意。

...

本章的源代码可以在这里找到

使用 WebAssembly 在您的浏览器中运行本章的示例(需要 WebGL2)

版权所有 (C) 2019, Herbert Wolverson。