🐉年大吉:带你用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刘金,转载请注明原文链接。感谢!

相关推荐
豌豆花下猫4 分钟前
Python 潮流周刊#78:async/await 是糟糕的设计(摘要)
后端·python·ai
YMWM_6 分钟前
第一章 Go语言简介
开发语言·后端·golang
码蜂窝编程官方23 分钟前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游
hummhumm41 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
J老熊1 小时前
JavaFX:简介、使用场景、常见问题及对比其他框架分析
java·开发语言·后端·面试·系统架构·软件工程
AuroraI'ncoding1 小时前
时间请求参数、响应
java·后端·spring
好奇的菜鸟1 小时前
Go语言中的引用类型:指针与传递机制
开发语言·后端·golang
Alive~o.01 小时前
Go语言进阶&依赖管理
开发语言·后端·golang
许苑向上1 小时前
Dubbo集成SpringBoot实现远程服务调用
spring boot·后端·dubbo
郑祎亦2 小时前
Spring Boot 项目 myblog 整理
spring boot·后端·java-ee·maven·mybatis