大家新年好呀
在迎接辉煌的龙年之际,我打算用一种别具一格的方式来庆祝这个特别的时刻:我用Rust语言编写了一个独特的游戏------"贪吃龙"。这款游戏灵感来源于经典的贪吃蛇游戏,但融入了丰富的中国文化元素和对即将到来的新年的祝福。在这篇文章中,我将带领大家一步步了解这个游戏的构建过程。
我们将从零开始,一点点揭开"贪吃龙"游戏的神秘面纱。无论您是编程高手,还是初涉编程的新手,都可以跟随我的步伐,体验编写这款游戏的乐趣。我将详细介绍每个编程阶段,包括代码设计、功能实现,以及如何将传统文化元素融入游戏中,使之成为一款富有中国特色的数字作品。
在这篇文章的里面,大家不仅能够学习到Rust编程的精妙,还能感受龙年的喜庆气氛。现在,就让我们一起开启这段编程之旅,创造一个既有趣又有意义的"贪吃龙"游戏,以此献礼龙年新年吧!
游戏原理
我们的童年回忆贪吃蛇游戏玩儿都玩过,但是具体是如何实现的呢?
其实并不复杂,其实就类似于动画,将每一帧的状态都画好,然后一帧一帧播放,就可以实现动态效果。
该过程主要涉及到游戏循环(game loop)、龙的数据结构表示、用户输入处理以及屏幕刷新。以下是详细步骤:
1. 游戏循环(Game Loop)
游戏通常运行在一个持续的循环中,这个循环负责处理用户输入、更新游戏状态(如龙的位置和方向)以及渲染游戏画面。每次循环迭代都代表游戏的一个"帧"。
2. 龙的数据结构表示
在游戏中,龙通常由一系列连接的点(或称为"节")表示。这些点可以存储在数组、列表或其他合适的数据结构中。每个点代表龙身体的一部分的位置,头部是其中一个特殊的点。
3. 用户输入处理
游戏循环在每次迭代中检查用户输入(如键盘按键),以确定龙的移动方向。例如,如果用户按下"向上"键,龙的方向将设置为向上移动。
4. 更新龙的位置
根据龙的当前方向和速度,游戏计算龙头的新位置,并将其添加到表示龙的数据结构的前端。同时,为了模拟龙的移动,从龙的尾部移除一个点。当龙吃到食物时,不移除尾部的点,使得龙的长度增加。
5. 碰撞检测
每次更新龙的位置时,游戏会检查龙头是否与游戏边界或其自身的其他部分相撞。如果发生碰撞,游戏可能会结束。
6. 屏幕刷新
在每次循环的最后,游戏会在屏幕上重新绘制龙的当前位置(包括龙头和龙身),以及食物和其他游戏元素。这个绘制过程将静态的龙数据结构转化为玩家可以看到的动态图像。
7. 时间管理
为了确保龙以恒定的速度移动,游戏循环通常包含时间管理。这意味着每次更新龙的位置之间有一个固定的时间间隔,确保游戏在不同的硬件上运行时具有一致的速度
创建项目文件
这个项目中,我们将这个游戏分为了控制和游戏两个不同的包
toml
members = [
"ctrl",
"game"
]
上面是项目的cargo.toml文件
ctrl
包:可以专注于游戏的逻辑和控制,例如处理用户输入、游戏状态管理和游戏规则。game
包:可以集中于游戏界面、图形渲染和用户交互。
这种分离确保了代码的清晰性和维护性,同时也使得未来的扩展和修改更加容易。
再来看看这两个包的toml文件:
toml
[package]
name = "loong_ctrl"
version = "0.1.0"
authors = ["alain <zhengze01@gmail.com>"]
edition = "2021"
[lib]
crate_type = [ "staticlib", "cdylib", "rlib" ]
[dependencies]
# thiserror: 用于简化错误处理和构造符合 Rust 错误处理约定的错误类型
thiserror = "1.0.20"
# rand: 提供随机数生成功能,支持多种随机数生成器和分布
rand = "0.7.3"
toml
[package]
name = "loong_game_rs"
version = "0.1.0"
authors = ["alain <zhengze01@gmail.com>"]
edition = "2021"
[dependencies]
# piston: 模块化的游戏引擎,提供窗口创建、事件循环和图形渲染
piston = "0.52.0"
# piston_window: 用于窗口的创建和管理
piston_window = "0.112.0"
# piston2d-graphics: 用于 2D 图形渲染
piston2d-graphics = "0.37.0"
# pistoncore-glutin_window: 提供基于 glutin 的窗口后端
pistoncore-glutin_window = "0.66.0"
# piston2d-opengl_graphics: 基于 OpenGL 的 2D 图形渲染
piston2d-opengl_graphics = "0.74.0"
# colorsys: 提供颜色转换和操作功能,支持多种颜色模型和格式
colorsys = "0.5.7"
# image: 用于图像编码和解码,支持多种图像格式
image = "0.23"
# lazy_static: 允许创建静态生命周期的变量,首次访问时初始化
lazy_static = "1.4.0"
# dirs: 用于获取系统特定的文件夹路径,如用户目录、缓存目录等
dirs = "3.0.1"
# piston2d-sprite: 用于处理和渲染 2D 精灵
piston2d-sprite = "0.61.0"
# loong_ctrl: 项目中的另一个包,包含游戏的控制逻辑
loong_ctrl = { path = "../ctrl" }
控制模块 (ctrl
)
ctrl
模块负责游戏的核心逻辑,包括处理用户输入、游戏状态更新、游戏规则等。
文件结构:
-
board.rs
:用于定义游戏板的逻辑,如格子布局和游戏对象的位置。 -
err.rs
:定义项目特有的错误类型。 -
full_state.rs
:管理游戏的完整状态,包括游戏进度、分数等。 -
matrix.rs
:用于处理二维数组或游戏逻辑中的矩阵操作。 -
options.rs
:存储游戏设置和配置选项。
游戏模块 (game
)
game
模块主要关注游戏的用户界面和视觉效果,包括绘制游戏元素、处理动画和响应用户交互。
文件结构:
-
consts.rs
:定义游戏中的常量,如颜色、尺寸、速度等。 -
main.rs
:游戏的主入口文件,负责初始化游戏并启动游戏循环。 -
record.rs
:可能用于记录和管理游戏成绩或历史记录。 -
timer.rs
:处理游戏中的计时逻辑。 -
view
子目录:包含与游戏视图相关的模块,如colors.rs
、draw_snake.rs
和sprites.rs
,用于处理游戏的视觉呈现。
游戏开发流程
- 开始阶段: 设置项目结构,定义基本的游戏元素和规则。
- 开发阶段: 实现
ctrl
和game
模块的具体功能,逐步构建游戏逻辑和用户界面。 - 测试与调试: 在开发过程中不断测试游戏,确保功能正确,同时修复发现的任何问题。
- 优化与完善: 根据测试反馈优化游戏性能和用户体验。
从ctrl模块开始吧
编写的贪吃龙游戏的控制模块
该模块包含三部分,龙,食物,和背景板以及需要的方法
Board
结构体
Board
结构体代表游戏板,包含游戏配置、蛇和食物的位置等信息。方法如下:
new
: 初始化游戏板,包括蛇和食物的初始位置。restart
: 重启游戏,重置蛇和食物。move_loong
: 移动蛇,并检查是否吃到食物。generate_food
: 生成新的食物位置。center_of
: 计算板中心点,辅助蛇的初始放置。clone_loong
和clone_food
: 克隆当前蛇和食物的状态。get_matrix
: 生成表示游戏板当前状态的矩阵。
rust
pub(crate) struct Board {
cfg: Rc<InnerCfg>,
dim_x: u16,
dim_y: u16,
pub(crate) loong: Loong,
pub(crate) food: Food,
}
impl Board {
pub(crate) fn new(cfg: Rc<InnerCfg>) -> LoongCtrlResult<Self> {
let InnerCfg {
dimension_x,
dimension_y,
initial_loong_size,
..
} = *cfg;
let center = Board::center_of(dimension_x, dimension_y);
let loong = Loong::new(center, initial_loong_size)?;
let mut board = Board {
cfg,
dim_x: dimension_x,
dim_y: dimension_y,
loong,
food:Food::new() ,
};
if board.cfg.auto_gen_food {
board.generate_food();
}
Ok(board)
}
pub fn restart(&mut self) -> LoongCtrlResult<()> {
self.loong = Loong::new(
Board::center_of(self.cfg.dimension_x, self.cfg.dimension_y),
self.cfg.initial_loong_size,
)?;
self.food = Food::new();
if self.cfg.auto_gen_food {
self.generate_food();
}
Ok(())
}
pub(crate) fn move_loong(
&mut self,
direction: Direction,
) -> LoongCtrlResult<bool> {
let removed_last =
Loong::move_loong(&self.cfg, &mut self.loong, direction)?;
let eaten = Loong::has_eaten(self.loong.clone(), &self.food.clone());
if let Some(e) = eaten {
self.loong.body.push(removed_last);
Food::clear_eaten(&mut self.food, &e);
if self.cfg.auto_gen_food {
self.generate_food();
}
return Ok(true);
}
return Ok(false);
}
pub(crate) fn generate_food(&mut self) {
self
.food.positions
.push(
Food::generate(&self.cfg, &self.loong, &self.food
));
}
fn center_of(dim_x: u16, dim_y: u16) -> Point {
let center_x = dim_x / 2;
let center_y = dim_y / 2;
Point(center_x, center_y)
}
pub(crate) fn clone_loong(&self) -> Loong {
Loong{body: self.loong.body.clone()}
}
pub(crate) fn clone_food(&self) -> Vec<Point> {
self.food.positions.clone()
}
pub(crate) fn get_matrix(&self) -> Matrix {
let mut m = Matrix::new(self.dim_x, self.dim_y);
m.add_loong(&self.loong.body);
m.add_food(&self.food);
m
}
}
Food
结构体
Food
结构体代表游戏中的食物,包含以下方法:
generate
: 生成新的食物位置。避免将食物放在蛇或已有食物的位置上。clear_eaten
: 移除被蛇吃掉的食物。
rust
#[derive(Debug,Clone)]
pub struct Food {pub positions: Vec<Point>}
impl Food {
fn generate(cfg: &InnerCfg, loong: &Loong, food: &Food) -> Point {
let max_x = cfg.dimension_x;
let max_y = cfg.dimension_y;
if (max_x * max_y - 1) <= (loong.body.len() + food.positions.len()) as u16 {}
let mut rng = rand::rngs::ThreadRng::default();
let mut apple = loong.body[0];
let occupied_points = [loong.body.clone(), food.positions.clone()].concat();
while occupied_points.iter().any(|p| p == &apple) {
let x = rng.gen_range(0, max_x);
let y = rng.gen_range(0, max_y);
apple = Point(x, y);
}
apple
}
fn clear_eaten(food: &mut Food, eaten: &Point) {
if let Some(pos) = food.positions.iter().position(|p| p == eaten) {
food.positions.remove(pos);
}
}
fn new()->Self{
Food{positions:Vec::with_capacity(1)}
}
}
Loong
结构体
Loong
结构体代表游戏中的蛇,包含以下方法:
create
: 创建蛇的初始状态,根据配置中心位置和大小。move_loong
: 移动蛇。根据方向更新蛇的头部位置,并处理穿墙逻辑。is_ate_itself
: 检查蛇是否咬到自己。has_eaten
: 检查蛇是否吃到了食物。try_teleport_head_if_need
: 如果配置允许,处理蛇头穿墙的逻辑。
rust
#[derive(Debug,Clone)]
pub struct Loong {
pub body: Vec<Point>,
}
impl Loong {
pub fn new(center: Point, size: u16) -> LoongCtrlResult<Self> {
let Point(center_x, center_y) = center;
if center_x < size {
return Err(LoongCtrlErr::InitLoongSizeIsBig);
}
let mut loong = Loong{body:Vec::with_capacity(usize::from(size))};
for loong_part_ind in 0..size {
loong.body.push(Point(center_x - loong_part_ind, center_y));
}
Ok(loong)
}
fn create(center: Point, size: u16) -> LoongCtrlResult<Vec<Point>> {
let Point(center_x, center_y) = center;
if center_x < size {
return Err(LoongCtrlErr::InitLoongSizeIsBig);
}
let mut loong = Vec::with_capacity(usize::from(size));
for loong_part_ind in 0..size {
loong.push(Point(center_x - loong_part_ind, center_y));
}
Ok(loong)
}
fn move_loong(
cfg: &InnerCfg,
loong: &mut Loong,
direction: Direction,
) -> LoongCtrlResult<Point> {
let last = if let Some(l) = loong.body.pop() {
l
} else {
return Err(LoongCtrlErr::LoongIsZero);
};
let head = if let Some(f) = loong.body.first() {
f
} else {
return Err(LoongCtrlErr::LoongIsZero);
};
let head_x = head.0;
let head_y = head.1;
let new_head = match direction {
Direction::Right => (head_x as i32 + 1, head_y as i32),
Direction::Top => (head_x as i32, head_y as i32 + 1),
Direction::Bottom => (head_x as i32, head_y as i32 - 1),
Direction::Left => (head_x as i32 - 1, head_y as i32),
};
let new_head = Loong::try_teleport_head_if_need(cfg, new_head)?;
loong.body.insert(0, new_head);
if Loong::is_ate_itself(loong) {
return Err(LoongCtrlErr::LoongAteItself);
}
Ok(last)
}
fn is_ate_itself(loong: &mut Loong) -> bool {
let head = &loong.body[0];
for i in 1..loong.body.len() {
let loong_part = &loong.body[i];
if loong_part == head {
return true;
}
}
false
}
fn has_eaten(loong: Loong, food: &Food) -> Option<Point> {
let first = &loong.body[0];
for f in food.positions.iter() {
if f == first {
return Some(*f);
}
}
None
}
fn try_teleport_head_if_need(
cfg: &InnerCfg,
new_head_unnormalized: (i32, i32),
) -> LoongCtrlResult<Point> {
let (new_head_x, new_head_y) = new_head_unnormalized;
let can_teleport = cfg.walking_through_the_walls;
let err =
|| -> LoongCtrlResult<Point> { Err(LoongCtrlErr::LoongHitTheWall) };
if new_head_unnormalized.0 < 0 {
if !can_teleport {
return err();
}
return Ok(Point(cfg.dimension_x - 1, new_head_y as u16));
} else if new_head_y < 0 {
if !can_teleport {
return err();
}
return Ok(Point(new_head_x as u16, cfg.dimension_y - 1));
} else if new_head_x > (cfg.dimension_x as i32 - 1) {
if !can_teleport {
return err();
}
return Ok(Point(0, new_head_y as u16));
} else if new_head_unnormalized.1 > (cfg.dimension_y as i32 - 1) {
if !can_teleport {
return err();
}
return Ok(Point(new_head_x as u16, 0));
}
Ok(Point(new_head_x as u16, new_head_y as u16))
}
}
错误处理
在这里定义了该游戏会出现的错误,并且声明了一个类型别名用来简化使用。这里使用thiserror库用来简化错误转换和简化错误声明 详细的thiserror用法,可以参考这篇文章:Rust:如何使用thiserror
rust
#[derive(Error, Debug)]
pub enum LoongCtrlErr {
#[error("row index ({0}) is out of bounds")]
RowIndexOutOfBounds(u16),
#[error("column index ({0}) is out of bounds")]
ColumnIndexOutOfBounds(u16),
#[error("the loong ate itself")]
LoongAteItself,
#[error("the loong hit the wall")]
LoongHitTheWall,
#[error("initial loong size is more than possible")]
InitLoongSizeIsBig,
#[error("something is really wrong. your loong size is zero")]
LoongIsZero,
}
pub type LoongCtrlResult<T> = Result<T, LoongCtrlErr>;
游戏状态
这段代码是一个贪吃龙游戏中龙的状态控制部分,其中包含了定义龙的各个部分的类型、状态以及如何根据当前情况计算龙的完整状态。下面是对代码中包含的内容和逻辑的详细解释:
rust
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum LoongCornerVariant {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
impl LoongCornerVariant {
fn reverse_y(&self) -> Self {
match self {
LoongCornerVariant::TopLeft => LoongCornerVariant::BottomLeft,
LoongCornerVariant::TopRight => LoongCornerVariant::BottomRight,
LoongCornerVariant::BottomLeft => LoongCornerVariant::TopLeft,
LoongCornerVariant::BottomRight => LoongCornerVariant::TopRight,
}
}
}
pub enum LoongPartVariant {
Head(Direction),
Tail(Direction),
Body(bool),
Corner(LoongCornerVariant),
}
pub struct LoongPart {
pub point: Point,
pub variant: LoongPartVariant,
}
impl LoongPart {
fn new(p: Point, v: LoongPartVariant) -> LoongPart {
LoongPart {
point: p,
variant: v,
}
}
}
pub struct LoongCtrlFullState {
pub loong: Vec<LoongPart>,
pub food: Vec<Point>,
pub direction: Direction,
}
pub(crate) fn calc_full_state(
loong: &Loong,
food: &Food,
current_direction: Direction,
dim_y: u16,
reverse_y: bool,
) -> LoongCtrlFullState {
let loong_len = loong.body.len();
let max_ind = loong.body.len() - 1;
let mut result: Vec<LoongPart> = Vec::with_capacity(loong_len);
for (ind, curr_point) in loong.body.iter().enumerate() {
if ind == 0 {
result.push(LoongPart::new(
*curr_point,
LoongPartVariant::Head(current_direction),
));
} else if ind == max_ind {
let pre_tail = loong.body.get(max_ind - 1).unwrap();
let mut tail_direction = curr_point.offset_from_near(&pre_tail).unwrap();
if !pre_tail.is_near_with(&curr_point) {
tail_direction = tail_direction.opposite_direction();
}
result.push(LoongPart::new(
*curr_point,
LoongPartVariant::Tail(tail_direction),
))
} else {
let prev = loong.body.get(ind + 1).unwrap();
let next = loong.body.get(ind - 1).unwrap();
result.push(LoongPart::new(
*curr_point,
get_body_part_variant(prev, curr_point, next),
))
}
}
let mut f = food.to_owned();
f.positions.iter_mut().for_each(|p| p.reverse_y(dim_y));
if reverse_y {
result.iter_mut().for_each(|p| {
p.point.reverse_y(dim_y);
if let LoongPartVariant::Corner(v) = p.variant {
p.variant = LoongPartVariant::Corner(v.reverse_y());
}
});
}
LoongCtrlFullState {
loong: result,
food: f.positions,
direction: current_direction,
}
}
fn get_body_part_variant(
prev: &Point,
curr: &Point,
next: &Point,
) -> LoongPartVariant {
let mut offset_from_prev = curr.offset_from_near(prev).unwrap();
let mut offset_from_next = next.offset_from_near(curr).unwrap();
if offset_from_prev == offset_from_next
|| offset_from_prev == offset_from_next.opposite_direction()
{
return LoongPartVariant::Body(offset_from_prev.is_vertical());
}
if !next.is_near_with(curr) {
offset_from_next = offset_from_next.opposite_direction();
}
if !prev.is_near_with(curr) {
offset_from_prev = offset_from_prev.opposite_direction();
}
match offset_from_prev {
Direction::Top => {
if offset_from_next == Direction::Right {
LoongPartVariant::Corner(LoongCornerVariant::BottomLeft)
} else {
LoongPartVariant::Corner(LoongCornerVariant::BottomRight)
}
}
Direction::Bottom => {
if offset_from_next == Direction::Right {
LoongPartVariant::Corner(LoongCornerVariant::TopLeft)
} else {
LoongPartVariant::Corner(LoongCornerVariant::TopRight)
}
}
Direction::Left => {
if offset_from_next == Direction::Top {
LoongPartVariant::Corner(LoongCornerVariant::TopLeft)
} else {
LoongPartVariant::Corner(LoongCornerVariant::BottomLeft)
}
}
Direction::Right => {
if offset_from_next == Direction::Top {
LoongPartVariant::Corner(LoongCornerVariant::TopRight)
} else {
LoongPartVariant::Corner(LoongCornerVariant::BottomRight)
}
}
}
}
枚举和结构体定义
LoongCornerVariant
枚举 :定义了龙身体拐角的四种变体:左上(TopLeft
)、右上(TopRight
)、左下(BottomLeft
)、右下(BottomRight
)。它还有一个方法reverse_y
,用于根据垂直轴反转拐角方向。LoongPartVariant
枚举 :定义了龙的部件类型,包括头部(Head
)带方向,尾部(Tail
)带方向,身体(Body
)表示是否垂直,以及拐角(Corner
)带具体的拐角类型。LoongPart
结构体 :表示龙的一个部分,包含一个点(Point
)表示位置和一个变体(variant
),说明这个部分是头部、尾部、身体还是拐角。LoongCtrlFullState
结构体 :表示龙的完整状态,包含一个龙的部件列表(loong
),食物的位置列表(food
),和当前龙的前进方向(direction
)。
逻辑实现
-
calc_full_state
函数 :根据当前的龙状态、食物状态、当前方向、维度和是否需要沿Y轴反转,计算并返回一个LoongCtrlFullState
实例,即龙的完整状态。其主要步骤如下:- 初始化 :创建一个空的
LoongPart
列表,准备存储计算后的龙的各个部分。 - 遍历龙的身体 :对于龙身体的每一个部分,根据它是头部、尾部还是身体的其他部分,分别计算并创建一个新的
LoongPart
实例,添加到结果列表中。头部直接使用当前方向,尾部需要计算方向,身体部分则需要判断是身体直线部分还是拐角,并计算相应的变体。 - 处理食物位置:如果需要,将食物位置根据Y轴进行反转。
- 处理龙的Y轴反转:如果指定了沿Y轴反转,则对每一个龙的部分进行Y轴的反转,包括位置和拐角方向的反转。
- 初始化 :创建一个空的
-
get_body_part_variant
函数:根据前一个部分、当前部分和下一个部分的位置,计算当前部分应该是直线部分还是拐角部分,以及相应的变体。逻辑上,它通过比较当前部分与前后部分的方向关系来确定。
开始game部分
游戏逻辑有了,剩下的就是如何将这条龙展现出来,所以有关颜色,速度,绘制模块放在游戏部分,例如 在view部分,我们要定义颜色,以及将龙绘制出来。
游戏贴图
这是将游戏中用到的龙的身各部分,以及食物等东西,放在一个图片文件中,然后再将对应的图片中的身体,拐角,头部尾部,食物与游戏一一对应起来,这样后边就可以使用该图片中的贴图资源了。
rust
static STEP: f64 = 64.0;
pub struct Sprites {
// texture: Rc<Texture>,
apple: Sprite<Texture>,
head_top: Sprite<Texture>,
head_right: Sprite<Texture>,
head_bottom: Sprite<Texture>,
head_left: Sprite<Texture>,
tail_top: Sprite<Texture>,
tail_right: Sprite<Texture>,
tail_bottom: Sprite<Texture>,
tail_left: Sprite<Texture>,
body_hor: Sprite<Texture>,
body_vert: Sprite<Texture>,
corner_top_left: Sprite<Texture>,
corner_top_right: Sprite<Texture>,
corner_bottom_left: Sprite<Texture>,
corner_bottom_right: Sprite<Texture>,
}
fn take_sprite_at_pos(tex: &Rc<Texture>, x: u8, y: u8) -> Sprite<Texture> {
let mut s = Sprite::from_texture(tex.clone());
s.set_src_rect([f64::from(x) * STEP, f64::from(y) * STEP, STEP, STEP]);
s.set_scale(0.25, 0.25);
s
}
impl Sprites {
pub fn init() -> Self {
let img_src = include_bytes!("../../sprites.png");
let img = image::load_from_memory(img_src).unwrap().into_rgba();
let texture = Rc::new(Texture::from_image(&img, &TextureSettings::new()));
let apple = take_sprite_at_pos(&texture, 0, 3);
let head_top = take_sprite_at_pos(&texture, 3, 0);
let head_right = take_sprite_at_pos(&texture, 4, 0);
let head_bottom = take_sprite_at_pos(&texture, 4, 1);
let head_left = take_sprite_at_pos(&texture, 3, 1);
let tail_top = take_sprite_at_pos(&texture, 4, 3);
let tail_right = take_sprite_at_pos(&texture, 3, 3);
let tail_bottom = take_sprite_at_pos(&texture, 3, 2);
let tail_left = take_sprite_at_pos(&texture, 4, 2);
let body_hor = take_sprite_at_pos(&texture, 1, 0);
let body_vert = take_sprite_at_pos(&texture, 2, 1);
let corner_top_left = take_sprite_at_pos(&texture, 0, 0);
let corner_top_right = take_sprite_at_pos(&texture, 2, 0);
let corner_bottom_left = take_sprite_at_pos(&texture, 0, 1);
let corner_bottom_right = take_sprite_at_pos(&texture, 2, 2);
Sprites {
// texture,
apple,
head_top,
head_right,
head_bottom,
head_left,
tail_top,
tail_right,
tail_bottom,
tail_left,
body_hor,
body_vert,
corner_top_left,
corner_top_right,
corner_bottom_left,
corner_bottom_right,
}
}
pub fn apple(&self) -> &Sprite<Texture> {
&self.apple
}
pub fn head(&self, direction: Direction) -> &Sprite<Texture> {
match direction {
Direction::Top => &self.head_top,
Direction::Right => &self.head_right,
Direction::Bottom => &self.head_bottom,
Direction::Left => &self.head_left,
}
}
pub fn tail(&self, direction: Direction) -> &Sprite<Texture> {
match direction {
Direction::Top => &self.tail_top,
Direction::Right => &self.tail_right,
Direction::Bottom => &self.tail_bottom,
Direction::Left => &self.tail_left,
}
}
pub fn body(&self, is_vertical: bool) -> &Sprite<Texture> {
if is_vertical {
&self.body_vert
} else {
&self.body_hor
}
}
pub fn corner(&self, variant: LoongCornerVariant) -> &Sprite<Texture> {
match variant {
LoongCornerVariant::TopLeft => &self.corner_top_left,
LoongCornerVariant::TopRight => &self.corner_top_right,
LoongCornerVariant::BottomLeft => &self.corner_bottom_left,
LoongCornerVariant::BottomRight => &self.corner_bottom_right,
}
}
}
颜色定义
下面是定义的游戏中需要用到的一些颜色:
rust
fn from_rgb_to_ratio<T: Into<Rgb>>(val: T) -> [f32; 4] {
let rgb: Rgb = val.into();
rgb.as_ratio().into()
}
lazy_static! {
pub static ref WHITE: [f32; 4] = from_rgb_to_ratio((255, 255, 255));
pub static ref LIME: [f32; 4] = from_rgb_to_ratio((205, 220, 57));
pub static ref DARK_GREEN: [f32; 4] = from_rgb_to_ratio((130, 119, 23));
pub static ref BLACK: [f32; 4] = from_rgb_to_ratio((33, 33, 33));
pub static ref BLACK_OP: [f32; 4] =
from_rgb_to_ratio((33.0, 33.0, 33.0, 0.9));
pub static ref ORANGE: [f32; 4] = from_rgb_to_ratio((224, 93, 31));
pub static ref GREY: [f32; 4] = from_rgb_to_ratio((50, 50, 50));
}
绘制龙的身体
下面这段代码是贪吃龙游戏中绘制龙("loong")到屏幕上的函数。它使用GlGraphics
库来进行图形绘制,并且依据龙的当前状态来决定如何在游戏窗口中展示每一部分的龙。下面是对这个函数每个部分的详细解释:
参数解释
gl
:一个可变引用到GlGraphics
对象,它是用来进行OpenGL绘制的工具。vp
:表示当前视图的Viewport
,用来定义绘图区域的大小和位置。state
:一个引用到LoongCtrlFullState
结构体,包含了当前需要绘制的龙的全部状态信息,如龙的各个部分的位置和类型。sprites
:一个引用到Sprites
,它是一个自定义的结构或类型,包含了绘制龙各个部分所需的精灵图(或者图像资源)。offset
:一个元组(f64, f64)
,表示绘制时的偏移量,用来调整整个龙在屏幕上的位置。
绘制逻辑
- 绘制开始 :通过
gl.draw
方法开始一个绘制会话。这个方法接受一个视图窗口vp
和一个闭包。闭包提供了一个上下文c
和一个gl
用于实际的绘制命令。 - 遍历龙的部件 :通过
state.loong
访问龙的当前状态,遍历其中的每个LoongPart
,这代表了龙的各个部分,如头部、尾部、身体和拐角。 - 计算位置 :对于每个
LoongPart
,根据其点point
计算出实际的屏幕坐标。使用STEP
和HALF_STEP
(这些值在代码中没有给出,但通常代表绘制单位的大小和一半大小)来将龙的逻辑位置(通常是格子坐标)转换为屏幕上的像素位置,并应用了偏移量offset
。 - 选择精灵图 :根据
LoongPart
的variant
(变体),选择对应的精灵图进行绘制。例如,头部可能根据不同的方向选择不同的头部图像,尾部也是如此。身体和拐角部分则根据是否垂直或是拐角的具体类型来选择图像。 - 绘制部件 :最后,使用精灵的
draw
方法,结合计算出的变换transform
(这里通过c.transform.trans(x, y)
创建,将绘图上下文移动到正确的位置),在OpenGL上下文gl
上绘制每个部分。
rust
pub fn draw_loong(
gl: &mut GlGraphics,
vp: Viewport,
state: &LoongCtrlFullState,
sprites: &Sprites,
offset: (f64, f64),
) {
gl.draw(vp, |c, gl| {
for loong_part in &state.loong {
let x = loong_part.point.0 as f64 * STEP - HALF_STEP + offset.0;
let y = loong_part.point.1 as f64 * STEP - HALF_STEP + offset.1;
let transform = c.transform.trans(x, y);
let sprite = match loong_part.variant {
LoongPartVariant::Head(dir) => sprites.head(dir),
LoongPartVariant::Tail(dir) => sprites.tail(dir),
LoongPartVariant::Body(is_vertical) => sprites.body(is_vertical),
LoongPartVariant::Corner(var) => sprites.corner(var),
};
sprite.draw(transform, gl);
}
});
}
计时器,帧率
这段代码定义了一个Timer
结构体,用于管理和跟踪时间相关的操作,如计时、暂停/恢复计时器以及调整计时器的速度。这种计时器可以在游戏或任何需要精确时间管理的应用中非常有用。
rust
pub struct Timer {
start: Instant,
last_update: Instant,
tick_millis: u128,
count_of_decreasing: u32,
is_paused: bool,
}
impl Timer {
pub fn new(tick_millis: u128) -> Self {
Timer {
last_update: Instant::now(),
start: Instant::now(),
tick_millis,
count_of_decreasing: 0,
is_paused: false,
}
}
pub fn is_ready(&mut self) -> bool {
if !self.is_paused
&& (self.last_update.elapsed().as_millis() >= self.tick_millis)
{
self.last_update = Instant::now();
return true;
}
return false;
}
pub fn decrease_tick_millis(&mut self) {
self.count_of_decreasing += 1;
self.tick_millis = ((self.tick_millis as f64) * 0.99999) as u128;
}
pub fn pause(&mut self) {
self.is_paused = true;
}
pub fn resume(&mut self) {
self.is_paused = false;
}
pub fn toggle_pause(&mut self) {
if self.is_paused {
self.resume()
} else {
self.pause()
}
}
pub fn get_speed(&self) -> u128 {
self.tick_millis
}
pub fn reset_last_update(&mut self) {
self.last_update = self.start;
}
}
游戏得分
这段代码定义了一个名为Record
的结构体,用于管理和记录游戏(如贪吃龙游戏)的得分数据。它使用Rust的文件系统(std::fs
)和输入输出(std::io
)库来存储和读取得分记录。
rust
static CFG_NAME: &str = "loong_rs_game_data";
pub struct Record {
pub score: u64,
current_score: u64,
}
impl Record {
pub fn init() -> Self {
let mut score = 0;
if let Some(dir) = dirs::data_dir() {
fs::read_to_string(dir.join(CFG_NAME))
.iter()
.for_each(|data| {
data.trim().parse::<u64>().iter().for_each(|num| {
score = *num;
});
});
};
Record {
score,
current_score: 0,
}
}
pub fn set_current_score(&mut self, val: u64) {
self.current_score = val;
}
pub fn write(&mut self) {
if self.current_score < self.score {
return;
}
self.score = self.current_score;
if let Some(dir) = dirs::data_dir() {
let file = fs::OpenOptions::new()
.write(true)
.create(true)
.open(dir.join(CFG_NAME));
if let Ok(mut f) = file {
f.write_all(self.current_score.to_string().as_bytes())
.map_err(|e| println!("{:?}", e));
}
};
}
}
impl Drop for Record {
fn drop(&mut self) {
self.write();
}
}
下面是对代码的详细解释:
Record
包含两个公开的字段:score
和current_score
。score
用于存储游戏的最高分,而current_score
用于记录当前游戏的得分。
init
方法
init
方法用于初始化Record
实例。它首先尝试读取存储在特定目录下的得分记录文件(文件名由静态变量CFG_NAME
定义)。- 使用
dirs::data_dir()
来获取操作系统特定的数据目录路径,然后尝试读取该目录下名为CFG_NAME
的文件。 - 如果文件存在且成功读取,它会解析文件内容为
u64
类型的得分并存储到score
字段中。 - 方法返回一个
Record
实例,其中score
字段设置为文件中读取的得分,current_score
初始化为0。
set_current_score
方法
set_current_score
方法允许设置Record
实例的current_score
字段。这个方法在游戏中用于更新当前得分。
write
方法
write
方法用于将当前得分写入文件。如果当前得分小于已记录的最高分,方法会提前返回,不执行任何操作。- 如果当前得分大于或等于最高分,它会更新
score
字段为current_score
的值,并尝试写入得分记录文件。 - 使用
fs::OpenOptions
来打开(或创建)得分记录文件,并用write_all
方法写入得分。如果写入过程出现错误,它会打印错误信息。
Drop
特性实现
Record
实现了Rust的Drop
特性,这意味着当Record
实例离开作用域或被销毁时,drop
方法会被自动调用。- 在
drop
方法中,它调用write
方法,确保最新的得分数据被写入文件。这是一种自动保存数据的机制,减少数据丢失的风险。
最后让他动起来吧
在main 函数中,我们需要 游戏初始化 :在main
函数中,设置了OpenGL版本、创建了游戏窗口、初始化了贪吃龙控制器的配置、加载了字体缓存,以及创建了App
实例,准备游戏运行所需的各种资源和状态。
-
事件处理和游戏循环 :通过Piston的事件循环来处理渲染(
render
)、键盘输入(handle_key_press
)和游戏状态更新(update
)。这包括处理用户输入(方向键改变贪吃龙的方向,空格键暂停/恢复游戏或重启游戏)、根据计时器触发游戏逻辑更新,以及渲染游戏画面。 -
键盘输入处理 (
handle_key_press
):根据用户按键输入改变贪吃龙的移动方向或处理游戏暂停/恢复、重启。 -
游戏重启 (
restart_game
):重置得分、计时器、贪吃龙控制器状态和游戏结束标志,以开始新游戏。 -
渲染逻辑 (
render
):绘制游戏得分、检查是否刷新记录、绘制游戏界面(包括贪吃龙、食物和边界),以及在游戏结束时显示结束画面和重启提示。 -
状态更新 (
update
):在计时器准备好时(即达到更新间隔),调用贪吃龙控制器的next_tick
方法来更新游戏状态,处理贪吃龙的移动、吃食和检测碰撞,同时更新得分和检查游戏结束条件。
rust
fn main() {
let opengl = OpenGL::V3_2;
let mut window: Window = WindowSettings::new(
"(Loong game).rs",
[
(BOARD_DIM_X * STEP as u16 + 70) as u32,
(BOARD_DIM_Y * STEP as u16 + 130) as u32,
],
)
.resizable(false)
.graphics_api(opengl)
.exit_on_esc(true)
.build()
.unwrap();
let loong_ctrl_options = LoongCtrlOptions::default()
.dimension_x(BOARD_DIM_X)
.dimension_y(BOARD_DIM_Y)
.initial_loong_size(10);
let glyph_cache = GlyphCache::from_bytes(
include_bytes!("../FiraSans-Regular.ttf"),
(),
TextureSettings::new(),
)
.unwrap();
let mut app = App {
gl: GlGraphics::new(opengl),
score: 0,
loong_ctrl: LoongCtrl::new(&loong_ctrl_options).unwrap(),
glyph_cache,
sprites: view::Sprites::init(),
timer: timer::Timer::new(100),
record: record::Record::init(),
is_game_over: false,
def_draw_state: DrawState::default(),
};
let mut events = Events::new(EventSettings::new().max_fps(30));
while let Some(e) = events.next(&mut window) {
if let Some(args) = e.render_args() {
app.render(&args);
}
if let Some(Button::Keyboard(key)) = e.press_args() {
app.handle_key_press(key);
};
if let Some(args) = e.update_args() {
if app.timer.is_ready() {
app.update(&args);
}
}
}
}
效果展示
图抠的有点丑,但是可以的话,只需要修改贴图,你也可以做个好看的哦
源码位置 from刘金,转载请注明原文链接。感谢!