🐉年大吉:带你用Rust从零写一条贪吃龙

大家新年好呀

在迎接辉煌的龙年之际,我打算用一种别具一格的方式来庆祝这个特别的时刻:我用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.rsdraw_snake.rssprites.rs,用于处理游戏的视觉呈现。

游戏开发流程

  • 开始阶段: 设置项目结构,定义基本的游戏元素和规则。
  • 开发阶段: 实现 ctrlgame 模块的具体功能,逐步构建游戏逻辑和用户界面。
  • 测试与调试: 在开发过程中不断测试游戏,确保功能正确,同时修复发现的任何问题。
  • 优化与完善: 根据测试反馈优化游戏性能和用户体验。

从ctrl模块开始吧

编写的贪吃龙游戏的控制模块

该模块包含三部分,龙,食物,和背景板以及需要的方法

Board 结构体

Board 结构体代表游戏板,包含游戏配置、蛇和食物的位置等信息。方法如下:

  • new: 初始化游戏板,包括蛇和食物的初始位置。
  • restart: 重启游戏,重置蛇和食物。
  • move_loong: 移动蛇,并检查是否吃到食物。
  • generate_food: 生成新的食物位置。
  • center_of: 计算板中心点,辅助蛇的初始放置。
  • clone_loongclone_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)
      }
    }
  }
}

枚举和结构体定义

  1. LoongCornerVariant 枚举 :定义了龙身体拐角的四种变体:左上(TopLeft)、右上(TopRight)、左下(BottomLeft)、右下(BottomRight)。它还有一个方法 reverse_y,用于根据垂直轴反转拐角方向。
  2. LoongPartVariant 枚举 :定义了龙的部件类型,包括头部(Head)带方向,尾部(Tail)带方向,身体(Body)表示是否垂直,以及拐角(Corner)带具体的拐角类型。
  3. LoongPart 结构体 :表示龙的一个部分,包含一个点(Point)表示位置和一个变体(variant),说明这个部分是头部、尾部、身体还是拐角。
  4. LoongCtrlFullState 结构体 :表示龙的完整状态,包含一个龙的部件列表(loong),食物的位置列表(food),和当前龙的前进方向(direction)。

逻辑实现

  • calc_full_state 函数 :根据当前的龙状态、食物状态、当前方向、维度和是否需要沿Y轴反转,计算并返回一个LoongCtrlFullState实例,即龙的完整状态。其主要步骤如下:

    1. 初始化 :创建一个空的LoongPart列表,准备存储计算后的龙的各个部分。
    2. 遍历龙的身体 :对于龙身体的每一个部分,根据它是头部、尾部还是身体的其他部分,分别计算并创建一个新的LoongPart实例,添加到结果列表中。头部直接使用当前方向,尾部需要计算方向,身体部分则需要判断是身体直线部分还是拐角,并计算相应的变体。
    3. 处理食物位置:如果需要,将食物位置根据Y轴进行反转。
    4. 处理龙的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),表示绘制时的偏移量,用来调整整个龙在屏幕上的位置。

绘制逻辑

  1. 绘制开始 :通过gl.draw方法开始一个绘制会话。这个方法接受一个视图窗口vp和一个闭包。闭包提供了一个上下文c和一个gl用于实际的绘制命令。
  2. 遍历龙的部件 :通过state.loong访问龙的当前状态,遍历其中的每个LoongPart,这代表了龙的各个部分,如头部、尾部、身体和拐角。
  3. 计算位置 :对于每个LoongPart,根据其点point计算出实际的屏幕坐标。使用STEPHALF_STEP(这些值在代码中没有给出,但通常代表绘制单位的大小和一半大小)来将龙的逻辑位置(通常是格子坐标)转换为屏幕上的像素位置,并应用了偏移量offset
  4. 选择精灵图 :根据LoongPartvariant(变体),选择对应的精灵图进行绘制。例如,头部可能根据不同的方向选择不同的头部图像,尾部也是如此。身体和拐角部分则根据是否垂直或是拐角的具体类型来选择图像。
  5. 绘制部件 :最后,使用精灵的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包含两个公开的字段:scorecurrent_scorescore用于存储游戏的最高分,而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刘金,转载请注明原文链接。感谢!

相关推荐
胡玉洋10 分钟前
Spring Boot 项目配置文件密码加密解决方案 —— Jasypt 实战指南
java·spring boot·后端·安全·加密·配置文件·jasypt
小坏讲微服务17 分钟前
Spring Boot4.0 集成 Redis 实现看门狗 Lua 脚本分布式锁完整使用
java·spring boot·redis·分布式·后端·lua
程序员大辉24 分钟前
Rust使用IDE,除了vscode还有RustRover非商业用户可以免费使用
ide·vscode·rust
IT_陈寒42 分钟前
Vue3性能优化实战:这5个技巧让我的应用加载速度提升了40%
前端·人工智能·后端
长征coder43 分钟前
SpringCloud服务优雅下线LoadBalancer 缓存配置方案
java·后端·spring
ForteScarlet1 小时前
Kotlin 2.3.0 现已发布!又有什么好东西?
android·开发语言·后端·ios·kotlin
Json____1 小时前
springboot框架对接物联网,配置TCP协议依赖,与设备通信,让TCP变的如此简单
java·spring boot·后端·tcp/ip
程序员阿明1 小时前
spring boot 3集成spring security6
spring boot·后端·spring
后端小张1 小时前
【JAVA 进阶】深入拆解SpringBoot自动配置:从原理到实战的完整指南
java·开发语言·spring boot·后端·spring·spring cloud·springboot
草莓熊Lotso1 小时前
C++11 核心进阶:引用折叠、完美转发与可变参数模板实战
开发语言·c++·人工智能·经验分享·后端·visualstudio·gitee