“文字地牢”小游戏通关 Rust 入门
“文字地牢”小游戏通关 Rust 入门
“文字地牢”将会是一系列的文章,循序渐进、可直接上手实践的“博文式”教程,本篇我们将从 0 开始,用一个可玩的文字地牢小游戏,串起 Rust 入门的关键知识点与最佳实践。目前的每一节都进行了简单的讲解,并给出项目里的真实代码与动手步骤,确保你看完就能跑、改完就能玩。示例我上传到这里了,大家可以直接运行尝试,后续博主也会通过多篇文章对具体的内容进行讲解。
适合读者:
- 想用“做一个小成品”快速上手 Rust 的同学
- 需要把《Rust 程序设计语言》中的概念落地到真实项目的同学
阅读收获:
- 从工具链、依赖管理到主循环、输入输出与数据建模的完整路径
- 用
Result/?进行优雅错误处理,完成 JSON 存档/读档 - 会跑会改的终端小游戏,可持续扩展为更完整的 roguelike
文章结构(可跳读):
-
- 从零到可玩(总览步骤)
- 1-2. 工具链与 Cargo
- 3-7. 主循环、IO、控制流、数据模型、所有权
- 8-9. 错误处理与文件 IO
- 10-12. 测试、扩展方向、调试技巧
-
- 总结与练习
0. 动手实践:从零到可玩
- 新建项目并运行“Hello, world!”
- 命令:
cargo new dungeon_explorer
cd dungeon_explorer
cargo run
- 你会看到:终端打印“Hello, world!”(在我们仓库里这一步已替换为游戏主循环)。
- 添加依赖与构建
- 编辑
Cargo.toml并添加serde,serde_json,rand。 - 命令:
cargo build。
- 读取用户输入并打印反馈
- 在
src/io/mod.rs中实现read_input_line();主循环中打印地图与读取输入。
- 使用
match分派命令
- 识别
w/a/s/d、save/load、q,给出明确反馈。
- 引入结构体与枚举建模
- 定义
Position、Player、GameState、Tile、Map,实现渲染与移动。
- 错误处理与文件 IO
- 用
Result/?传播错误;serde_json完成存档/读档。
- 测试与扩展
- 添加基本单元测试;扩展迭代器统计、随机生成、物品与敌人等。
1. 安装工具链与创建项目
概念:
rustup管理工具链通道(stable/beta/nightly)与目标平台;cargo负责构建、依赖与工作流;rustc是编译器。- 组件:
rustfmt(统一格式)、clippy(代码建议),在团队协作中尤为重要。
为什么:
- 可重复、可升级的工具链管理是“代码能跑、到处能跑”的前提;统一的格式与建议减少风格分歧和隐藏 bug。
如何做(命令):
curl https://sh.rustup.rs -sSf | sh
rustup update
rustc --version
cargo --version
rustup component add rustfmt clippy
新建项目(本仓库已就绪):
cargo new dungeon_explorer
常见坑:
- Mac 上安装后首次运行较慢,多为编译缓存未生成;可先
cargo check预热。 - 忘记安装
clippy导致无法执行cargo clippy;或 IDE 未启用rust-analyzer导致补全缺失。
小练习:
- 运行
cargo fmt和cargo clippy -- -D warnings,确保代码通过格式化与静态检查。
2. Cargo 与依赖管理
概念:
Cargo.toml声明包与依赖,Cargo.lock锁定版本;feature 用于按需启用功能(如serde/derive)。- 常用命令:
cargo run(编译并运行)、cargo build(构建二进制/库)、cargo check(快速类型检查)、cargo fmt、cargo clippy、cargo test。
为什么:
- 精准声明依赖和 feature 能避免“臃肿/未使用/编译慢”;锁定版本保障团队与 CI 一致性。
如何做(项目用法):
[package]
name = "dungeon_explorer"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rand = "0.8"
serde+derive让结构体/枚举支持序列化。serde_json用于 JSON 存档。rand后续用于随机地图。
常见坑:
- 未启用
serde的derivefeature,导致Serialize/Deserialize无法派生。 - 复制代码时混入制表与空格导致
Cargo.toml解析失败(请保持缩进一致)。
小练习:
- 新增依赖
ron = "0.8",为后续可选的 RON 存档做准备,并执行cargo check验证。
3. 程序入口与模块组织
概念:
mod将子模块纳入当前 crate;use将路径引入作用域,减少长路径污染。fn main() -> Result<()>使入口函数能以?传播可恢复错误,避免unwrap导致崩溃。
为什么:
- 清晰的模块边界与公共 API 能让游戏状态、地图、IO 职责分离,易于扩展(敌人/物品/AI)。
项目实现(src/main.rs):
mod game;
mod map;
mod io;
mod errors;
use errors::Result;
use game::{Game, GameState, Player, Position};
fn main() -> Result<()> {
let map = map::Map::new(10, 10);
let player = Player { position: Position { x: 1, y: 1 }, hp: 10 };
let state = GameState { map, player };
let mut game = Game::new(state);
println!("欢迎来到文字地牢!使用 w/a/s/d 移动,save 保存,load 读取,q 退出。");
loop {
println!("{}", game.render());
let input = io::read_input_line()?;
let cmd = input.trim().to_lowercase();
match cmd.as_str() {
"w" | "a" | "s" | "d" => game.handle_move(cmd.chars().next().unwrap()),
"save" => { io::save_state("save.json", &game.state)?; println!("已保存到 save.json"); }
"load" => match io::load_state("save.json") {
Ok(loaded) => { game.state = loaded; println!("已读取存档"); }
Err(e) => println!("读取失败: {}", e),
},
"q" => { println!("退出游戏,再见!"); break; }
_ => println!("未知命令:{}", cmd),
}
}
Ok(())
}
运行效果示例(部分):
##########
#........#
#.@......#
#........#
#........#
#........#
#........#
#........#
#........#
##########
HP: 10 Pos:(1, 1)
>
设计要点:
- 将
Game的渲染与状态更新封装在game模块,io仅负责人机交互与存档/读档,map负责数据与不变式,彼此通过清晰方法通信。 - 对输入先标准化(
trim+to_lowercase)再match,降低分支复杂度。
小练习:
- 在
main中将cmd.chars().next()的unwrap()改为安全分支:空输入时跳过当前循环并提示。
4. 读取输入与基础 IO
概念:
- 标准输入输出:
stdin/stdout;print!不换行常用于提示符,需手动flush及时显示。 - 文本处理:
String(可变、拥有所有权)与&str(切片视图)的区分;trim()去除尾随换行与空白。
项目实现(src/io/mod.rs):
use std::io::{self, Write};
use serde_json;
use crate::errors::Result;
use crate::game::GameState;
pub fn read_input_line() -> Result<String> {
print!("> ");
io::stdout().flush().ok();
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
Ok(buf)
}
pub fn save_state(path: &str, state: &GameState) -> Result<()> {
let s = serde_json::to_string_pretty(state)?;
std::fs::write(path, s)?;
Ok(())
}
pub fn load_state(path: &str) -> Result<GameState> {
let s = std::fs::read_to_string(path)?;
let state: GameState = serde_json::from_str(&s)?;
Ok(state)
}
健壮性建议:
read_line返回0可能表示 EOF(如管道输入结束),应在调用端处理为空输入的分支。- Windows 与 Unix 换行差异由
trim()统一处理,避免显式判断\r\n。
动手验证:
- 输入
save后生成save.json;输入load恢复状态;输入q退出。
5. 控制流与模式匹配
概念先导:
- 控制流:
loop用于主循环,搭配break退出;for适合迭代场景。 match:穷尽匹配、模式组合、解构,配合枚举可表达明确状态机。
在游戏中的应用:
- 用
loop驱动帧循环;用match分派命令(移动、存档、读档、退出)。
常见坑:
- 直接
unwrap()处理空输入会崩溃;建议改为if let Some(c) = cmd.chars().next()。 - 分支遗漏导致“未知命令”无反馈,最好统一落到
_分支给出帮助提示。
小练习:
- 定义
enum Command { Move(char), Save, Load, Quit, Help, Unknown(String) },实现parse_command(&str)并在main中match该枚举。
6. 数据模型:结构体与枚举
概念先导:
- 结构体
struct:组合相关字段,可实现方法与 trait;适合实体建模(玩家、地图)。 - 枚举
enum:表达有限状态并可携带数据,适合地图瓦片、命令解析、错误类型。 - 常用派生:
Debug/Clone/Copy/PartialEq/Serialize/Deserialize,便于调试、比较与(反)序列化。
在游戏中的应用(src/game/mod.rs 与 src/map/mod.rs):
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub struct Position { pub x: usize, pub y: usize }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Player { pub position: Position, pub hp: i32 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameState { pub map: Map, pub player: Player }
地图与瓦片:
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum Tile { Wall, Floor }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Map { pub width: usize, pub height: usize, pub tiles: Vec<Tile> }
impl Map {
pub fn new(width: usize, height: usize) -> Self { /* 边框设为墙 */ }
pub fn get_tile(&self, x: usize, y: usize) -> Tile { /* 下标计算 */ }
pub fn set_tile(&mut self, x: usize, y: usize, tile: Tile) { /* 可变借用修改 */ }
}
设计取舍:
Position采用usize与网格一致;如需负偏移或相对坐标可引入i32辅助。Map.tiles使用扁平Vec<Tile>而非二维Vec<Vec<Tile>>,以降低内存碎片并简化序列化;通过方法隐藏索引换算保持不变式。
小练习:
- 为
Tile扩展Item(Kind)与Trap(Damage)变体;在render()中以不同字符呈现。
7. 所有权与借用
&T(共享借用)可并发读取;&mut T(独占借用)可修改但同一时刻唯一。- Rust 在编译期以借用规则保障数据一致性与线程安全。
项目签名:
pub fn render(&self) -> String { /* 读取状态并拼接字符串 */ }
pub fn handle_move(&mut self, dir: char) { /* 修改玩家坐标 */ }
常见坑:
- 同时持有对
Map与Player的可变借用会引发借用冲突;建议以更粗粒度方法实现移动逻辑,或拆分不可变/可变借用的生命周期。 - 返回借用的引用必须不超过拥有者的生命周期,避免将局部数据的引用存入全局状态。
小练习:
- 在
handle_move内避免多重可变借用:先计算目标位置,再一次性更新玩家坐标。
8. 错误处理:Result、Option 与 ?
概念:
- 可恢复错误用
Result表达并向上传递;不可恢复错误用panic!(仅在不可恢复的不变量被破坏时)。 ?操作符简化传播并保持控制流清晰。
项目实现(src/errors.rs):
pub type Result<T> = std::result::Result<T, AppError>;
#[derive(Debug)]
pub enum AppError { Io(std::io::Error), Serde(serde_json::Error) }
impl From<std::io::Error> for AppError { /* ... */ }
impl From<serde_json::Error> for AppError { /* ... */ }
友好提示:
- 在读档时区分“文件不存在”和“格式错误”,对用户给出“是否先保存一次?”或“检查文件内容是否有效 JSON”的建议。
小练习:
- 为
AppError新增NotFound变体,或在load_state中匹配io::ErrorKind::NotFound并输出定制消息。
9. 文件 IO 与序列化
概念:
- 文本(JSON/RON/TOML)可读易调试;二进制(bincode)体积小、速度快。
- 版本兼容:在存档结构加入
version字段,或使用带默认值的可选字段。
项目实现:
serde_json::to_string_pretty生成可读 JSON,std::fs::write/read_to_string完成写读。
小练习:
- 为
GameState增加version: u32字段,调整序列化后进行一次存档/读档回归测试。
10. 测试:验证核心逻辑
概念:
- 单元测试置于模块末尾
#[cfg(test)];集成测试置于tests/目录。 - 行为导向:将关键逻辑(边框为墙、撞墙不移动)写成用例,避免回归。
示例:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn map_border_is_wall() { /* 检查边框都是墙 */ }
#[test]
fn move_blocked_by_wall() { /* 检查撞墙不移动 */ }
}
命令:
cargo test
检查清单:
- 渲染字符串包含正确宽高与玩家位置
- 撞墙保持坐标不变;地板移动坐标正确
- 存档后文件存在且可读;读档后状态等价
11. 逐步扩展
- 随机地图:用
rand生成通道/房间。 - 物品与敌人:
Item、Enemy,引入行为 trait,如Actor。 - HUD 与统计:迭代器统计地板/墙数量并显示。
示例迭代器统计:
let floor_count = (0..game.state.map.height)
.flat_map(|y| (0..game.state.map.width).map(move |x| (x, y)))
.map(|(x,y)| game.state.map.get_tile(x,y))
.filter(|t| matches!(t, Tile::Floor))
.count();
如何落地:
- 将地图生成抽象为
MapGeneratortrait,实现不同风格(隧道/房间/混合)的生成器;在main通过参数选择。 - 定义
Actor行为(tick(&mut self, &Map)),让Enemy通过最短路或贪心靠近玩家。
12. 开发命令与调试技巧
- 常用:
cargo run、cargo check、cargo fmt、cargo clippy、cargo test。 - 调试:
dbg!(&value)、println!。 - 日志:
log+env_logger或tracing(进阶)。
建议:
- 将
RUST_LOG=info与env_logger结合,区分用户输出与开发日志。 - 以
cargo watch -x check -x test -x run(需安装)获得快速反馈循环。
13. 总结与练习
你已完成一个可运行终端小游戏的搭建与拆解,涵盖:
- 安装与工具链、Cargo 项目结构与依赖管理
- 主循环、输入输出、控制流与模式匹配
- 数据模型(结构体/枚举)、所有权与借用
- 错误处理与文件 IO(存档/读档)
- 测试与扩展方向
练习清单:
- 将命令解析抽象为
parse_command(&str) -> Command枚举,为未知命令输出帮助列表。 - 在
render()中显示更多 HUD:步数、地板/墙统计、玩家坐标。 - 引入
Actortrait 与Enemy,实现简易 AI(朝玩家移动但不穿墙)。 - 将存档格式改为
ron或toml,并为存档结构添加version字段以做兼容。 - 为
handle_move编写更完整的测试覆盖:四方向、边界、墙阻挡、连续移动。
结语与下一步
如果你完成了上面的练习,此时已经具备将《文字地牢》教程中的模式(读取输入、解析、匹配、循环、错误处理)迁移到更大项目中的能力。
下一步建议:
- 将本项目的命令解析独立为
Command与parse_command,为后续加入物品/敌人/AI 打好基础。 - 引入日志与更结构化的错误,补充边界测试与属性测试,持续把“可玩 demo”进化成“可维护的小项目”。
后续博主也会针对每个章节中的内容进行讲解,敬请期待。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)