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)
}