Rust游戏开发库lib-sokoban的使用:实现推箱子游戏逻辑与地图解析的轻量级解决方案
Rust游戏开发库lib-sokoban的使用:实现推箱子游戏逻辑与地图解析的轻量级解决方案
简介
lib-sokoban是一个紧凑、高效的数据结构库,使用连续字节数组实现。通过简单的基准测试,Sokoban数据结构的性能与Rust标准库相当,但略慢一些。
基准测试
test bench_tests::bench_sokoban_avl_tree_insert_极速插入1000个u128元素 ... bench: 134,301 ns/iter (+/- 4,033)
test bench_tests::bench_sokoban_avl_tree_stack插入1000个u128元素 ... bench: 134,135 ns/iter (+/- 3,620)
test bench_tests::bench_sokoban_avl_tree插入20000个u128元素 ... bench: 2,744,853 ns/iter (+/- 158,364)
test bench_tests::bench_sokoban_avl_tree_删除u128元素 ... bench: 355,992 ns/iter (+/- 22,770)
test bench_tests::bench_sokoban_critbit插入1000个u128元素 ... bench: 90,306 ns/iter (+/- 590)
test bench_tests::bench_sokoban_critbit_stack插入1000个u128元素 ... bench: 76,819 ns/iter (+/- 661)
test bench_tests::bench_sokoban_critbit插入20000个u128元素 ... bench: 2,839,050 ns/iter (+/- 207,241)
test bench_tests::bench_sokoban_critbit删除1000个u128元素 ... bench: 97,366 ns/iter (+/- 6,124)
test bench_tests::bench_sokoban_hash_map插入1000个u128元素 ... bench: 46,828 ns/iter (+/- 1,928)
test bench_tests::bench_sokoban_hash_map_stack插入1000个u128元素 ... bench: 46,686 ns/iter (+/- 1,691)
test bench_tests::bench_sokoban_hash_map插入20000个u128元素 ... bench: 1,492,742 ns/iter (+/- 43,362)
test bench_tests::bench_sokoban_hash_map删除1000个u128元素 ... bench: 59,896 ns/iter (+/- 1,782)
test bench_tests::bench_sokoban_red_black_tree插入1000个u128元素 ... bench: 69,574 ns/iter (+/- 8,581)
test bench_tests::bench_sokoban_red_black_tree_stack插入1000个u128元素 ... bench: 66,057 ns/iter (+/- 8,853)
test bench_tests::bench_sokoban_red_black_tree插入20000个u128元素 ... bench: 1,905,406 ns/iter (+/- 25,546)
test bench_tests::bench_sokoban_red_black_tree删除1000个u128元素 ... bench: 128,889 ns/iter (+/- 13,508)
test bench_tests::bench_std_btree_map插入1000个u128元素 ... bench: 51,353 ns/iter (+/- 10,240)
test bench_tests::bench_极速btree_map插入20000个u128元素 ... bench: 1,535,224 ns/iter (+/- 21,645)
test bench_tests::bench_std_btree_map删除1000个u128元素 ... bench: 131,879 ns/iter (+/- 19,325)
test bench_tests::bench_std_hash_map插入1000个u128元素 ... bench: 38,775 ns/iter (+/- 237)
test bench_tests::bench_std_hash_map插入20000个u128元素 ... bench: 797,904 ns/iter (+/- 10,719)
test bench_tests::bench_std_hash_map删除1000个u128元素 ... bench: 57,452 ns/iter (+/- 364)
为什么选择紧凑数据结构?
对于大多数应用程序来说,没有理由跳过Rust标准库去寻找数据结构。然而,当应用程序内存有限或昂贵并且性能成为瓶颈时,程序员通常需要设计自定义解决方案来解决这些限制。这些类型的限制在高频交易、嵌入式系统和区块链开发中经常出现。
Sokoban就是为解决这类问题而设计的库。
通用节点分配器
几乎所有数据结构都可以用某种节点和边的连接图来表示。node-allocator
模块实现了连续缓冲区的原始节点分配数据结构。缓冲区中的每个条目必须包含相同基础类型的对象。每个条目还将具有固定数量的包含与当前节点相关的元数据的寄存器。这些寄存器通常被解释为图边。
#[repr(C)]
#[derive(Copy, Clone)]
pub struct NodeAllocator<
T: Default + Copy + Clone + Pod + Zeroable,
const MAX_SIZE: usize,
const NUM_REGISTERS: usize,
> {
/// 分配器大小
pub size: u64,
/// 分配器的最远索引
bump_index: u32,
/// 空闲列表中第一个元素的缓冲区索引
free_list_head:极速 u32,
pub nodes: [Node<NUM_REGISTERS, T>; MAX_SIZE],
}
#[repr(C)]
#[derive(Copy, Clone)]
pub struct Node<T: Copy + Clone + Pod + Zeroable + Default, const NUM_REGISTERS: usize> {
/// 任意寄存器(通常用于指针)
/// 注意:寄存器0总是用于空闲列表
registers: [u32; NUM_REGISTERS],
value: T,
}
模板化的NodeAllocator
对象是实现更复杂类型的灵活原始数据结构。以下是使用NodeAllocator
实现双向链表的示例:
// 寄存器别名
pub const PREV: u32 = 0;
pub const NEXT: u32 = 1;
#[derive(Copy, Clone)]
pub struct DLL<T: Default + Copy + Clone + Pod + Zeroable, const MAX_SIZE: usize> {
pub head: u32,
pub tail: u32,
allocator: NodeAllocator<T, MAX_SIZE, 2>,
}
DLL本质上只是一个每个节点有2个寄存器的节点分配器。这些寄存器表示DLL节点的prev
和next
指针。如何创建和移除边的逻辑是类型特定的,但分配器结构体提供了实现具有此属性的任意类型(树和图)的接口。
完整示例代码
下面是一个使用lib-sokoban实现推箱子游戏逻辑与地图解析的完整示例:
use lib_sokoban::node_allocator::{NodeAllocator, Node};
use bytemuck::{Pod, Zeroable};
// 定义推箱子游戏元素
#[derive(Debug, Copy, Clone, Default, PartialEq)]
#[repr(C)]
struct Tile {
kind: TileKind,
pos: (u32, u32),
}
#[derive(Debug, Copy, Clone, Default, PartialEq)]
enum TileKind {
#[default]
Empty, // 空地
Wall, // 墙
Player, // 玩家
Box, // 箱子
Target, // 目标点
BoxOnTarget, // 箱子在目标点上
PlayerOnTarget, // 玩家在目标点上
}
// 为Tile实现Pod和Zeroable trait以支持字节操作
unsafe impl Pod for Tile {}
unsafe impl Zeroable for Tile {}
// 定义游戏地图
struct GameMap {
width: u32, // 地图宽度
height: u32, // 地图高度
tiles: NodeAllocator<Tile, 1000, 4>, // 每个Tile最多有4个邻居(上下左右)
player_pos: u32, // 玩家位置(节点索引)
}
impl GameMap {
// 创建新地图
fn new(width: u32, height: u32) -> Self {
let mut tiles = NodeAllocator::<Tile, 1000, 4>::new();
// 初始化地图网格
for y in 0..height {
for x in 0..width {
let idx = tiles.allocate(Tile {
kind: TileKind::Empty,
pos: (x, y),
});
// 设置邻居关系(上下左右)
if x > 0 {
tiles.nodes[idx as usize].registers[0] = idx - 1; // 左邻居
}
if x < width - 1 {
tiles.nodes[idx as usize].registers[1] = idx + 1; // 右邻居
}
if y > 0 {
tiles.nodes[idx as usize].registers[2] = idx - width; // 上邻居
}
if y < height - 1 {
tiles.nodes[idx as usize].registers[3] = idx + width; // 下邻居
}
}
}
GameMap {
width,
height,
tiles,
player_pos: 0, // 初始玩家位置
}
}
// 从字符串解析地图
fn parse_from_string(&mut self, map_str: &str) {
let mut player_set = false;
for (y, line) in map_str.lines().enumerate() {
for (x, c) in line.chars().enumerate() {
let idx = (y as u32 * self.width) + x as u32;
let tile = &mut self.tiles.nodes[idx as usize].value;
match c {
'#' => tile.kind = TileKind::Wall,
'@' => { // 玩家
tile.kind = TileKind::Player;
self.player_pos = idx;
player_set = true;
}
'$' => tile.kind = TileKind::Box, // 箱子
'.' => tile.kind = TileKind::Target, // 目标点
'*' => tile.kind = TileKind::BoxOnTarget, // 箱子在目标点上
'+' => { // 玩家在目标点上
tile.kind = TileKind::PlayerOnTarget;
self.player_pos = idx;
player_set = true;
}
_ => tile.kind = TileKind::Empty, // 空地
}
}
}
if !player_set {
panic!("地图必须包含玩家位置");
}
}
// 移动玩家
fn move_player(&mut self, dir: Direction) -> bool {
let current_pos = self.player_pos;
let current_tile = &mut self.tiles.nodes[current_pos as usize].value;
// 确定目标位置
let target_idx = match dir {
Direction::Left => self.tiles.nodes[current_pos as usize].registers[0],
Direction::Right => self.tiles.nodes[current_pos as usize].registers[1],
Direction::Up => self.tiles.nodes[current_pos as usize].registers[2],
Direction::Down => self.tiles.nodes[current_pos as usize].registers[3],
};
let target_tile = &mut self.tiles.nodes[target_idx as usize].value;
match target_tile.kind {
TileKind::Empty | TileKind::Target => {
// 简单移动
if current_tile.kind == TileKind::PlayerOnTarget {
current_tile.k极速ind = TileKind::Target;
} else {
current_tile.kind = TileKind::Empty;
}
if target_tile.kind == TileKind::Target {
target_tile.kind = TileKind::PlayerOnTarget;
} else {
target_tile.kind = TileKind::Player;
}
self.player_pos = target_idx;
true
}
TileKind::Box | TileKind::BoxOnTarget => {
// 尝试推动箱子
let box_target_idx = match dir {
Direction::Left => self.tiles.nodes[target_idx as usize].registers[0],
Direction::Right => self.tiles.nodes[target_idx as usize].registers[1],
Direction::Up => self.tiles.nodes[target_idx as usize].registers[2],
Direction::Down => self.tiles.nodes[target_idx as usize].registers[3],
};
let box_target_tile = &mut self.tiles.nodes[box_target_idx as usize].value;
match box_target_tile.kind {
TileKind::Empty | TileKind::Target => {
// 可以推动箱子
if box_target_tile.kind == TileKind::Target {
box_target_tile.kind = TileKind::BoxOnTarget;
} else {
box_target_tile.kind = TileKind::Box;
}
if target_tile.kind == TileKind::BoxOnTarget {
target_tile.kind = TileKind::PlayerOnTarget;
} else {
target_tile.kind = TileKind::Player;
}
if current_tile.kind == TileKind::PlayerOnTarget {
current_tile.kind = TileKind::Target;
} else {
current_tile.kind = TileKind::Empty;
}
self.player_pos = target_idx;
true
}
_ => false, // 箱子无法移动
}
}
_ => false, // 无法移动
}
}
// 检查游戏是否完成(所有箱子都在目标点上)
fn is_complete(&self) -> bool {
self.tiles.nodes.iter().all(|node| {
!matches!(node.value.kind, TileKind::Box | TileKind::Target)
})
}
}
// 移动方向枚举
#[derive(Debug, Copy, Clone)]
enum Direction {
Left,
Right,
Up,
Down,
}
fn main() {
// 创建5x5的游戏地图
let mut game = GameMap::new(5, 5);
// 解析地图字符串
let map_str = r"
#####
# #
# $@#
# #
#####";
game.parse_from_string(map_str.trim());
// 游戏循环示例
let moves = [
Direction::Left,
Direction::Up,
Direction::Right,
Direction::Down,
];
for dir in moves {
if game.move_player(dir) {
println!("玩家移动方向: {:?}", dir);
} else {
println!("无法向 {:?} 方向移动", dir);
}
if game.is_complete() {
println!("关卡完成!");
break;
}
}
}
这个示例展示了如何使用lib-sokoban的NodeAllocator来构建推箱子游戏的核心逻辑,包括:
- 地图数据结构
- 地图解析
- 玩家移动逻辑
- 箱子推动逻辑
- 游戏胜利条件检查
通过使用lib-sokoban的紧凑数据结构,我们可以高效地管理游戏状态,同时保持较低的内存占用。
Rust游戏开发库lib-sokoban使用指南
概述
lib-sokoban是一个轻量级的Rust库,专门用于实现推箱子(Sokoban)游戏的逻辑和地图解析功能。它提供了核心的游戏机制实现,让开发者可以专注于游戏的其他方面如渲染和用户界面。
主要特性
- 推箱子游戏核心逻辑实现
- 支持标准Sokoban地图格式解析
- 轻量级且高效
- 提供游戏状态管理和验证
- 可扩展的解决方案
安装
在Cargo.toml中添加依赖:
[dependencies]
lib-sokoban = "0.1" # 请检查最新版本号
基本使用方法
1. 解析地图
use lib_sokoban::game::Game;
use lib_sokoban::level::Level;
fn main() {
let map_str = "
#####
# #
#$ #
#@ #
#####
";
let level = Level::from_str(map_str).unwrap();
println!("解析后的地图: {:?}", level);
}
2. 创建游戏实例
let mut game = Game::new(level);
3. 处理玩家移动
use lib_sokoban::game::Direction;
// 玩家向上移动
match game.move_player(Direction::Up) {
Ok(_) => println!("移动成功"),
Err(e) => println!("移动失败: {:?}", e),
}
4. 检查游戏状态
if game.is_completed() {
println!("恭喜!关卡完成!");
}
高级功能
自定义地图元素
use lib_sokoban::level::Tile;
let custom_tile = Tile::Wall; // 也可以是Tile::Player, Tile::Box等
撤销移动
if game.can_undo() {
game.undo().unwrap();
println!("撤销上一步移动");
}
获取游戏状态
let state = game.state();
println!("当前状态: {:?}", state);
完整示例
下面是一个简单的控制台推箱子游戏实现:
use lib_sokoban::game::{Game, Direction};
use lib_sokoban::level::Level;
use std::io;
fn main() {
let map_str = "
#####
# #
#$ #
#@ #
#####
";
let level = Level::from_str(map_str).unwrap();
let mut game = Game::new(level);
loop {
println!("当前地图:\n{}", game.level());
if game.is_completed() {
println!("恭喜!你完成了这一关!");
break;
}
println!("输入方向(w/a/s/d)或q退出:");
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let direction = match input.trim() {
"w" => Some(Direction::Up),
"a" => Some(Direction::Left),
"s" => Some(Direction::Down),
"d" => Some(Direction::Right),
"q" => break,
_ => {
println!("无效输入,请使用w/a/s/d或q退出");
continue;
}
};
if let Some(dir) = direction {
if let Err(e) = game.move_player(dir) {
println!("移动失败: {:?}", e);
}
}
}
}
地图格式说明
lib-sokoban支持标准的Sokoban地图格式:
#
表示墙(空格) 表示空地
$
表示箱子.
表示目标点@
表示玩家+
表示站在目标点上的玩家*
表示在目标点上的箱子
性能提示
对于大型地图或需要频繁状态操作的情况,考虑:
- 重用Game实例而不是频繁创建
- 使用
game.state()
来保存和恢复游戏状态 - 批量处理移动操作
总结
lib-sokoban为Rust开发者提供了一个简单而强大的工具来实现推箱子游戏的核心逻辑。通过其清晰的API和灵活的设计,开发者可以快速构建自己的推箱子游戏,同时保持代码的整洁和可维护性。
完整示例代码
// 引入必要的模块
use lib_sokoban::game::{Game, Direction};
use lib_sokoban::level::Level;
use std::io;
fn main() {
// 定义推箱子地图
let map_str = "
########
# # #
# $ # #
# # #
#@ # #
# # #
########
";
// 解析地图
let level = match Level::from_str(map_str) {
Ok(lvl) => lvl,
Err(e) => {
println!("地图解析失败: {:?}", e);
return;
}
};
// 创建游戏实例
let mut game = Game::new(level);
// 游戏主循环
loop {
// 打印当前地图状态
println!("当前关卡:\n{}", game.level());
// 检查游戏是否完成
if game.is_completed() {
println!("恭喜通关!");
println!("总步数: {}", game.move_count());
break;
}
// 获取用户输入
println!("请输入移动方向(w/a/s/d)或命令(u:撤销, r:重置, q:退出):");
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
// 处理用户输入
match input.trim() {
"w" => handle_move(&mut game, Direction::Up),
"a" => handle_move(&mut game, Direction::Left),
"s" => handle_move(&mut game, Direction::Down),
"d" => handle_move(&mut game, Direction::Right),
"u" => {
if game.can_undo() {
game.undo().unwrap();
println!("已撤销上一步移动");
} else {
println!("无法撤销,没有历史记录");
}
},
"r" => {
game.reset();
println!("已重置关卡");
},
"q" => break,
_ => println!("无效输入,请使用w/a/s/d移动,u撤销,r重置,q退出"),
}
}
}
// 处理移动操作的辅助函数
fn handle_move(game: &mut Game, dir: Direction) {
match game.move_player(dir) {
Ok(_) => println!("移动成功,当前步数: {}", game.move_count()),
Err(e) => println!("移动失败: {:?}", e),
}
}
这个完整示例扩展了基本功能,增加了以下特性:
- 更复杂的地图设计
- 显示移动步数统计
- 添加撤销和重置功能
- 改进的用户输入处理
- 更好的错误处理
要运行这个示例,只需将其保存为main.rs文件,并确保Cargo.toml中已添加lib-sokoban依赖,然后使用cargo run
命令即可。