Hands-on Rust 学习之旅(5)—— Modules 学习

2,155 阅读2分钟

有了上一篇文章的小鸟游戏,对游戏入门有点了解了。

今天我们开始新游戏 (Build a Dungeon Crawler) 开发学习。

今天的任务,就是设计游戏地图 (Map) 和 Player。

例行,看看执行效果:

dc_background

Modules

在建 Map 之前,我们必须先要解决一个棘手问题,在之前的游戏 demo 中,我们把代码都写在一个 main.rs 里,这不符合代码逻辑和规范,我们需要把代码拆解到其它文件中 —— 即根据业务将代码分模块 (Modules)。

就犹如图上所示,我们需要创建两个模块组件:crate::map 和 crate::player

按照惯例,我们新建一个 dungeoncrawl 项目:

cargo new dungeoncrawl

Map

src 文件夹创建文件:map.rs

在地图上,主要有两种类型的东西:

#[derive(Copy, Clone, PartialEq)]
pub enum TileType {
    Wall,
    Floor,
}

其中,Wall 利用符号 # 表示,Floor.平铺:

// 地图类型平铺,使用 Vec 类型
pub struct Map {
    pub tiles: Vec<TileType>,
}

// 
pub fn render(&self, ctx: &mut BTerm) {
    for y in 0..SCREEN_HEIGHT {
        for x in 0..SCREEN_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('#'));
                }
            }
        }
    }
}

Create the Player Structure

在之前的小鸟游戏已经定义 Player,这里基本一样,同样的,创建一个文件 player.rs

use crate::prelude::*;

pub struct Player {
    pub position: Point
}

impl Player {
    pub fn new(position: Point) -> Self {
        Self {
            position
        }
    }

    pub fn render(&self, ctx: &mut BTerm) {
        ctx.set(
            self.position.x,
            self.position.y,
            WHITE,
            BLACK,
            to_cp437('@'),
        )
    }

    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;
            }
        }
    }
}

道理也简单,Player 使用字符 @ 表示,通过键盘「上下左右」控制 Player 走向,每按一次,走一格,同时,更新自己的位置。

但是,这里有一个前提,更新的点位,必须是 Floor 类型,就是说不能是墙:

pub fn in_bounds(&self, point: Point) -> bool {
    point.x >= 0 && point.x < SCREEN_WIDTH
    && point.y >= 0 && point.y < SCREEN_HEIGHT
}
    
pub fn can_enter_tile(&self, point: Point) -> bool {
    self.in_bounds(point) && self.tiles[map_idx(point.x, point.y)] == TileType::Floor
}

有了 MapPlayer,这就可以在 main.rs 上引用这两个模块:

use prelude::*;

mod map;
mod player;

mod prelude {
    pub use bracket_lib::prelude::*;
    pub const SCREEN_WIDTH: i32 = 80;
    pub const SCREEN_HEIGHT: i32 = 50;
    pub use crate::map::*;
    pub use crate::player::*;
}

通过 mod 关键引用,同样的,借助 mod prelude 把引用的几个模块和 bracket_lib 放在一起。

同样的,我们创建 State,把 Map, Player 初始化,放入屏幕中:

struct State {
    map: Map,
    player: Player,
}

impl State {
    fn new() -> Self {
        Self { 
            map: Map::new(),
            player: Player::new(Point::new(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2)),
        }
    }
}

impl GameState for State {
    fn tick(&mut self, ctx: &mut BTerm) {
        ctx.cls();
        self.player.update(ctx, &self.map);
        self.map.render(ctx);
        self.player.render(ctx);
    }
}

剩下的就是和小鸟 game 一样了:

fn main() -> BError {
    let context = BTermBuilder::simple80x50()
        .with_title("叶梅树学习 Dungeon Crawler")
        .with_fps_cap(30.0)
        .build()?;

    main_loop(context, State::new())
}

总结

结果就如同一开始的截图效果一致了,有了 MapPlayer 接下来就好弄了。

今天主要是学习了 Module 开发,把代码分模块独立出去。