Ggez游戏开发 贪吃蛇游戏
04_snake
use oorandom::Rand32;
use ggez::{
event, graphics,
input::keyboard::{KeyCode, KeyInput},
Context, GameResult,
};
use std::collections::VecDeque;
//格子的数量:30*20个
const GRID_SIZE: (i16, i16) = (30, 20);
// 定义每个格子占据的大小
const GRID_CELL_SIZE: (i16, i16) = (32, 32);
// 定义实际窗口的大小
const SCREEN_SIZE: (f32, f32) = (
GRID_SIZE.0 as f32 * GRID_CELL_SIZE.0 as f32,
GRID_SIZE.1 as f32 * GRID_CELL_SIZE.1 as f32,
);
// 我们定义了我们希望游戏更新的频率,这样我们的蛇就不会飞过屏幕,因为每一帧它完整地移动一次
const DESIRED_FPS: u32 = 8;
//现在我们定义一个结构体,它将在我们的游戏板(即我们上面定义的网格)上保存一个实体的位置。
//我们将使用有符号整数,因为我们只想存储整数,并且我们需要它们是有符号的,
//以便它们在后面与我们的模算术正确配合
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct GridPosition {
x: i16,
y: i16,
}
impl GridPosition {
/// 使用标准的构造函数.
pub fn new(x: i16, y: i16) -> Self {
GridPosition { x, y }
}
/// 随机生成一个position
pub fn random(rng: &mut Rand32, max_x: i16, max_y: i16) -> Self {
// We can use `.into()` to convert from `(i16, i16)` to a `GridPosition` since
// we implement `From<(i16, i16)>` for `GridPosition` below.
(
rng.rand_range(0..(max_x as u32)) as i16,
rng.rand_range(0..(max_y as u32)) as i16,
)
.into()
}
// 我们将创建另一个辅助函数,它接受一个网格位置并在之后返回一个新的网格位置
/// 向 'dir' 方向移动一步。
/// 当越过顶部 / 左侧边界时,我们使用rem_euclid() API,因为当左操作数为负时,标准余数函数(%)会返回负值。
/// 只有向上 / 向左的情况需要rem_euclid();为了保持一致性,所有情况都使用它。
pub fn new_from_move(pos: GridPosition, dir: Direction) -> Self {
match dir {
Direction::Up => GridPosition::new(pos.x, (pos.y - 1).rem_euclid(GRID_SIZE.1)),
Direction::Down => GridPosition::new(pos.x, (pos.y + 1).rem_euclid(GRID_SIZE.1)),
Direction::Left => GridPosition::new((pos.x - 1).rem_euclid(GRID_SIZE.0), pos.y),
Direction::Right => GridPosition::new((pos.x + 1).rem_euclid(GRID_SIZE.0), pos.y),
}
}
}
//我们实现了From特征,在这种情况下,它允许我们在GridPosition和填充该网格单元的 ggez graphics::Rect之间轻松转换。
//现在,在我们想要一个表示该网格单元的Rect的GridPosition上,我们可以调用.into()
impl From<GridPosition> for graphics::Rect {
fn from(pos: GridPosition) -> Self {
graphics::Rect::new_i32(
pos.x as i32 * GRID_CELL_SIZE.0 as i32,
pos.y as i32 * GRID_CELL_SIZE.1 as i32,
GRID_CELL_SIZE.0 as i32,
GRID_CELL_SIZE.1 as i32,
)
}
}
/// 这样我们可以轻松地完成两者之间的转换:
/// `(i16, i16)` and a `GridPosition`.
impl From<(i16, i16)> for GridPosition {
fn from(pos: (i16, i16)) -> Self {
GridPosition { x: pos.0, y: pos.1 }
}
}
/// 现在我们创造枚举,记录每个可能的移动目标
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Direction {
Up,
Down,
Left,
Right,
}
impl Direction {
/// 我们创建一个辅助函数,以便能够轻松地获取 “方向” 的反向,
///之后我们可以用它来检查玩家是否能够让蛇向特定方向移动。
pub fn inverse(self) -> Self {
match self {
Direction::Up => Direction::Down,
Direction::Down => Direction::Up,
Direction::Left => Direction::Right,
Direction::Right => Direction::Left,
}
}
/// 我们还创建了一个辅助函数,用于在ggez的Keycode和它所代表的Direction之间进行转换。
///当然,如果不是每个键码都代表一个方向,那么在这种情况下我们返回None。
pub fn from_keycode(key: KeyCode) -> Option<Direction> {
match key {
KeyCode::Up => Some(Direction::Up),
KeyCode::Down => Some(Direction::Down),
KeyCode::Left => Some(Direction::Left),
KeyCode::Right => Some(Direction::Right),
_ => None,
}
}
}
/// 这主要是对GridPosition的一种语义抽象,用于表示蛇的一部分。
///比如说,让每个部分都有自己的颜色或类似的东西可能会很有用。
#[derive(Clone, Copy, Debug)]
struct Segment {
pos: GridPosition,
}
impl Segment {
pub fn new(pos: GridPosition) -> Self {
Segment { pos }
}
}
/// 这又是对代表蛇可以吃的一块食物的 “网格位置” 的一种抽象。它可以自己绘制自身
struct Food {
pos: GridPosition,
}
impl Food {
pub fn new(pos: GridPosition) -> Self {
Food { pos }
}
/// 这种绘图方法不能扩展。如果你需要绘制大量的图形,使用InstanceArray。
//对于这个示例来说,这种方法是可行的,因为调用次数相当有限。
fn draw(&self, canvas: &mut graphics::Canvas) {
// First we set the color to draw with, in this case all food will be
// colored blue.
let color = [0.0, 0.0, 1.0, 1.0];
//我们使用填充绘制模式绘制一个矩形,并使用“.into()”将食物的位置转换为“ggez::Rect”
///由于我们之前为“Rect”实现了“From<GridPosition>”,所以我们可以这样做。
canvas.draw(
&graphics::Quad,
graphics::DrawParam::new()
.dest_rect(self.pos.into())
.color(color),
);
}
}
/// 在此,我们定义了一个枚举,用于表示蛇在游戏更新期间可能 “吃掉” 的东西。
///蛇可能吃掉一块 “食物”,或者如果蛇头撞到身体,它可能会吃掉 “自己”。
#[derive(Clone, Copy, Debug)]
enum Ate {
Itself,
Food,
}
/// 我们现在创造一个结构体,来描述蛇的状态
struct Snake {
/// 头的状态
head: Segment,
/// 现在蛇的移动方向,这是update时的重要信息
dir: Direction,
/// 描述身子的
body: VecDeque<Segment>,
/// 现在我们有一个属性,它代表上一次执行的更新结果。
///蛇可能什么都没吃到(无)、食物(Some (Ate::Food))或者自己(Some (Ate::Itself))。
ate: Option<Ate>,
/// 最后,我们存储上一次调用 “update” 时蛇的移动方向,
///我们将使用它来确定下一次调用 “update” 时蛇可以移动的有效方向。
/// 存储下一个 “update” 之后将在 “更新” 中使用的方向
/// 这是需要的,以便用户可以按两个方向(例如向左然后向上)在一个 “更新” 发生之前。
///它有点像按键输入排队
next_dir: Option<Direction>,
}
impl Snake {
pub fn new(pos: GridPosition) -> Self {
let mut body = VecDeque::new();
// 初始时向右移动,并且只有头+一个segment,
body.push_back(Segment::new((pos.x - 1, pos.y).into()));
Snake {
head: Segment::new(pos),
dir: Direction::Right,
last_update_dir: Direction::Right,
body,
ate: None,
next_dir: None,
}
}
/// 现在是否吃了食物了
fn eats(&self, food: &Food) -> bool {
self.head.pos == food.pos
}
/// 现在是否吃了自己了
fn eats_self(&self) -> bool {
for seg in &self.body {
if self.head.pos == seg.pos {
return true;
}
}
false
}
/// 每次update游戏状态时update蛇
fn update(&mut self, food: &Food) {
// 如果 `last_update_dir` 已经被更新的和 `dir`一样了
//并且我们有 `next_dir`, then set `dir` to `next_dir` and unset `next_dir`
if self.last_update_dir == self.dir && self.next_dir.is_some() {
self.dir = self.next_dir.unwrap();
self.next_dir = None;
}
// 首先,我们使用先前的 “new_from_move” 辅助函数获取新的头部位置。
//我们将头部朝着当前前进的方向移动。
let new_head_pos = GridPosition::new_from_move(self.head.pos, self.dir);
// 创建一个新的头
let new_head = Segment::new(new_head_pos);
// 把头push一下
self.body.push_front(self.head);
// 修改一下head参数
self.head = new_head;
// 处理一下是不是吃了
if self.eats_self() {
self.ate = Some(Ate::Itself);
} else if self.eats(food) {
self.ate = Some(Ate::Food);
} else {
self.ate = None;
}
// 如果这一回合我们没有吃任何东西,我们从身体中取出最后一部分,
// 这给人一种蛇在移动的错觉。实际上,所有的片段都保持不变
// 静止的,我们只是在前面添加一段,从后面删除一段。如果我们吃
// 一块食物,然后我们留下最后一段,这样我们就把身体延长了一个。
if self.ate.is_none() {
self.body.pop_back();
}
// And set our last_update_dir to the direction we just moved.
self.last_update_dir = self.dir;
}
// 这里我们有蛇画本身。这与我们看到食物的方式非常相似
/// 早点画自己。
///
/// 再次注意,这种绘图方法适用于有限的范围
/// 示例,但大型游戏可能需要更优化的渲染路径
/// 使用 'InstanceArray' 或类似的批量绘制调用。
fn draw(&self, canvas: &mut graphics::Canvas) {
// 我们首先遍历身体部分并绘制它们。
for seg in &self.body {
/ 我们再次设置颜色(在本例中为橙色)
// 然后绘制我们将该 Segment 的位置转换为的 Rect
canvas.draw(
&graphics::Quad,
graphics::DrawParam::new()
.dest_rect(seg.pos.into())
.color([0.3, 0.3, 0.0, 1.0]),
);
}
//画头
canvas.draw(
&graphics::Quad,
graphics::DrawParam::new()
.dest_rect(self.head.pos.into())
.color([1.0, 0.5, 0.0, 1.0]),
);
}
}
/// 主要的部分
struct GameState {
/// 蛇
snake: Snake,
/// 食物
food: Food,
/// 游戏状态
gameover: bool,
/// Our RNG state
rng: Rand32,
}
impl GameState {
/// 初始状态
pub fn new() -> Self {
// 首先,我们把蛇放在 x 轴网格的四分之一处
// 和 y 轴的一半。这很有效,因为我们开始向右移动。
let snake_pos = (GRID_SIZE.0 / 4, GRID_SIZE.1 / 2).into();
// 创造种子
let mut seed: [u8; 8] = [0; 8];
getrandom::getrandom(&mut seed[..]).expect("Could not create RNG seed");
let mut rng = Rand32::new(u64::from_ne_bytes(seed));
// 创建食物
let food_pos = GridPosition::random(&mut rng, GRID_SIZE.0, GRID_SIZE.1);
GameState {
snake: Snake::new(snake_pos),
food: Food::new(food_pos),
gameover: false,
rng,
}
}
}
impl event::EventHandler<ggez::GameError> for GameState {
/// 每一帧draw之前都会update
fn update(&mut self, ctx: &mut Context) -> GameResult {
// 依靠 ggez 的内置计时器来决定何时更新游戏,以及更新多少次。
// 如果更新早,就不会有循环,否则逻辑会为每个运行一次
// 自上次更新以来的时间帧拟合。
while ctx.time.check_update_time(DESIRED_FPS) {
// 检查是否gameover了,否则啥也不做
if !self.gameover {
self.snake.update(&self.food);
// 看看吃了什么
if let Some(ate) = self.snake.ate {
//处理吃了什么
match ate {
// 如果吃了食物,那么我们生成一个信达位置
Ate::Food => {
let new_food_pos =
GridPosition::random(&mut self.rng, GRID_SIZE.0, GRID_SIZE.1);
self.food.pos = new_food_pos;
}
// 如果吃了自己,那么设置为gameover
Ate::Itself => {
self.gameover = true;
}
}
}
}
}
Ok(())
}
fn draw(&mut self, ctx: &mut Context) -> GameResult {
// // 首先我们创建一个渲染到框架的画布,并将其清除为(某种)绿色
let mut canvas =
graphics::Canvas::from_frame(ctx, graphics::Color::from([0.0, 1.0, 0.0, 1.0]));
// 分别让他们draw
self.snake.draw(&mut canvas);
self.food.draw(&mut canvas);
// 最后,我们 “刷新” 绘制命令。
// 由于我们渲染到框架,我们不需要告诉 ggez 呈现其他任何东西,
// 除非另有说明,否则 ggez 将自动呈现帧图像。
canvas.finish(ctx)?;
// We yield the current thread until the next update
ggez::timer::yield_now();
Ok(())
}
/// `key_down_event` 键盘按下时会触发这个
fn key_down_event(&mut self, _ctx: &mut Context, input: KeyInput, _repeat: bool) -> GameResult {
// 转化一下键盘按键变成朝向
if let Some(dir) = input.keycode.and_then(Direction::from_keycode) {
// 如果成功,我们检查是否已经设置了新的方向
// 并确保新方向与'snake. dir' 不同
if self.snake.dir != self.snake.last_update_dir && dir.inverse() != self.snake.dir {
self.snake.next_dir = Some(dir);
} else if dir.inverse() != self.snake.last_update_dir {
//如果没有设置新方向并且方向不是逆方向
//的'last_update_dir',然后设置蛇的新方向为
//用户按下的方向。
self.snake.dir = dir;
}
}
Ok(())
}
}
fn main() -> GameResult {
let (ctx, events_loop) = ggez::ContextBuilder::new("snake", "Gray Olson")
.window_setup(ggez::conf::WindowSetup::default().title("Snake!"))
.window_mode(ggez::conf::WindowMode::default().dimensions(SCREEN_SIZE.0, SCREEN_SIZE.1))
.build()?;
event::run(ctx, events_loop, state)
}