Ggez游戏开发 学习笔记

bevy劝退了. api变动太快,导致很多网上的样例/AI 都是过时的,连一个小demo都得跑好几天. 转头去用ggez了.API相当稳定(仓库两年没更新了hh),AI 跑一遍都能生成一个demo.

话不多说,看例子: 01_super_simple.rs

//! The simplest possible example that does something.
//unnecessary_wraps是clippy中的一条具体规则,用于检测代码中不必要的Result或Option类型的包装。
//比如,当函数返回值已经是Result类型,却又在调用处将其包裹在Ok或Err中,这种情况就会被该规则检测到。
#![allow(clippy::unnecessary_wraps)]

use ggez::{
    event,
    glam::*,
    graphics::{self, Color},
    Context, GameResult,
};
//State是表示我们游戏当前状态所需的所有数据和信息。这些可以是玩家位置、分数、手中的牌等等。
//你的状态中包含的内容很大程度上取决于你正在制作的游戏。
//这里graphics::Mesh是ggez图形库中的一个类型,用于表示图形网格,
//在代码中circle字段用于存储一个圆形的图形数据,以便后续在绘制过程中使用。
struct MainState {
    pos_x: f32,
    circle: graphics::Mesh,
}

impl MainState {
    fn new(ctx: &mut Context) -> GameResult<MainState> {
        let circle = graphics::Mesh::new_circle(
            ctx,
//指定绘制的时候采取填充的策略
            graphics::DrawMode::fill(),
//vec2(0., 0.):这是使用glam库中的vec2函数创建的一个二维向量,向量的两个分量分别为0.0。
//在graphics::Mesh::new_circle函数里,这个向量用于指定圆形的圆心位置。
//这里表示圆心位于坐标(0, 0)处,即窗口的左上角
//(在ggez的默认坐标系下,左上角为原点(0, 0) ,x 轴向右为正方向,y 轴向下为正方向)。
            vec2(0., 0.),
//代表圆形的半径。在创建圆形Mesh时,这个值决定了圆形的大小,当前设置下,圆形的半径为100.0个单位长度。
            100.0,
//2.0:是圆形的细分程度参数。在构建圆形的图形网格时,该值控制圆形边缘的平滑程度。
数值越小,圆形边缘的分段数越多,看起来就越平滑,但同时也会增加计算量和内存开销。
这里设置为2.0,可以理解为一个相对适中的细分程度。
            2.0,
            Color::WHITE,
        )?;

        Ok(MainState { pos_x: 0.0, circle })
    }
}
//你可能已经注意到EventHandler是一个 Rust Trait。这意味着它旨在在一个结构体上实现。
//这个 Trait 上定义了相当多的回调函数,但只需要2 个:update 和 draw。
impl event::EventHandler<ggez::GameError> for MainState {
//可以发现这个update是更新MainState 用的
    fn update(&mut self, _ctx: &mut Context) -> GameResult {
//其实也就是向右移动
        self.pos_x = self.pos_x % 800.0 + 1.0;
        Ok(())
    }

    fn draw(&mut self, ctx: &mut Context) -> GameResult {
//ctx是Context类型的可变引用,包含了游戏的上下文信息,例如窗口的相关设置等。
//Color::from方法将一个包含四个浮点数的数组[0.1, 0.2, 0.3, 1.0]转换为Color类型,
//这四个浮点数分别表示红色、绿色、蓝色和透明度(RGBA)的值,所以背景颜色是一种深灰色
//(因为RGB值都比较低且透明度为 1 表示不透明)
        let mut canvas =
            graphics::Canvas::from_frame(ctx, graphics::Color::from([0.1, 0.2, 0.3, 1.0]));
//Vec2::new是glam库中用于创建二维向量的函数,self.pos_x是MainState结构体中的一个字段,
//表示圆形在 x 轴方向上的位置(会不断更新变化),380.0是在 y 轴方向上的位置,
//所以圆形会绘制在 y 坐标为 380 的水平线上,x 坐标则由self.pos_x决定。
        canvas.draw(&self.circle, Vec2::new(self.pos_x, 380.0));

        canvas.finish(ctx)?;

        Ok(())
    }
}
//GameResult是由ggez提供的一个实用工具,用于表示是否存在错误。
//在内部,它只是一个Result<(), GameError>,这就是为什么我们用GameError实现EventHandler,以便on_error知道会发生什么。
//但是,我们不会写任何错误,对吧?😉
pub fn main() -> GameResult {
//Context是ggez如何让你访问硬件的方式,例如鼠标、键盘、定时器、图形、声音等。
//这将创建一个带有游戏 ID“super_simple”和作者“ggez”的Context。
//它还将创建一个EventLoop。我们很快就会需要它来调用run。可以随意将作者替换为你自己。
    let cb = ggez::ContextBuilder::new("super_simple", "ggez");
    let (mut ctx, event_loop) = cb.build()?;
    let state = MainState::new(&mut ctx)?;
//现在你准备好启动循环了!
    event::run(ctx, event_loop, state)
}

最后的效果大概是: 840090099c428022af366907eb3eabf1_MD5 这个球会从左往右跑.


02_hello_world

//! Basic hello world example.

use ggez::{event, graphics, Context, GameResult};
use std::{env, path};

// First we make a structure to contain the game's state
struct MainState {
//用于记录当前帧数
    frames: usize,
}

impl MainState {
    fn new(ctx: &mut Context) -> GameResult<MainState> {
//gfx: 是Context中的一个字段,它提供了图形相关的操作方法。
//gfx的类型是Graphics结构体(在ggez库中定义),通过这个字段可以进行图形绘制、设置图形属性、管理图形资源(如字体、纹理等)等操作。
        ctx.gfx.add_font(
            "LiberationMono",
            graphics::FontData::from_path(ctx, "/LiberationMono-Regular.ttf")?,
        );

        let s = MainState { frames: 0 };
        Ok(s)
    }
}

// Then we implement the `ggez:event::EventHandler` trait on it, which
// requires callbacks for updating and drawing the game state each frame.
//
// The `EventHandler` trait also contains callbacks for event handling
// that you can override if you wish, but the defaults are fine.
impl event::EventHandler<ggez::GameError> for MainState {
//不需要更新MainState 
    fn update(&mut self, _ctx: &mut Context) -> GameResult {
        Ok(())
    }

    fn draw(&mut self, ctx: &mut Context) -> GameResult {
//这是设置背景的颜色
        let mut canvas =
            graphics::Canvas::from_frame(ctx, graphics::Color::from([0.1, 0.2, 0.3, 1.0]));

        // Text is drawn from the top-left corner.
//
        let offset = self.frames as f32 / 10.0;
//创建一个二维向量dest_point
        let dest_point = ggez::glam::Vec2::new(offset, offset);
        canvas.draw(
//设置字体属性的方法
            graphics::Text::new("Hello, world!")
                .set_font("LiberationMono")
//.set_scale(48.) 设置文本的缩放比例为 48,即文本会以较大的尺寸显示。
                .set_scale(48.),
//从dest_point这个位置开始画
            dest_point,
        );

        canvas.finish(ctx)?;

        self.frames += 1;
        if (self.frames % 100) == 0 {
            println!("FPS: {}", ctx.time.fps());
        }

        Ok(())
    }
}

// Now our main function, which does three things:
//
// * First, create a new `ggez::ContextBuilder`
// object which contains configuration info on things such
// as screen resolution and window title.
// * Second, create a `ggez::game::Game` object which will
// do the work of creating our MainState and running our game.
// * Then, just call `game.run()` which runs the `Game` mainloop.
pub fn main() -> GameResult {
    // We add the CARGO_MANIFEST_DIR/resources to the resource paths
    // so that ggez will look in our cargo project directory for files.
//env::var("CARGO_MANIFEST_DIR")用于获取CARGO_MANIFEST_DIR环境变量的值,
//这个变量在cargo构建时会被设置为当前项目的根目录。
//if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR")尝试获取该环境变量,
//如果获取成功,将路径追加resources子目录作为资源目录;若获取失败,则使用默认的./resources作为资源目录
    let resource_dir = if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
        let mut path = path::PathBuf::from(manifest_dir);
        path.push("resources");
        path
    } else {
        path::PathBuf::from("./resources")
    };

    let cb = ggez::ContextBuilder::new("helloworld", "ggez").add_resource_path(resource_dir);
    let (mut ctx, event_loop) = cb.build()?;
//?用于处理创建过程中可能出现的错误。
    let state = MainState::new(&mut ctx)?;
    event::run(ctx, event_loop, state)
}
最终效果: 316022da43c506cb57f2b14c1c6ca17c_MD5 这个字体会从左上移动到右下.


03_drawing

//! A collection of semi-random shape and image drawing examples.

use ggez::{
    event,
    glam::*,
    graphics::{self, Color},
    Context, GameResult,
};
use std::{env, path};

struct MainState {
//首次提到Image内容.
    image1: graphics::Image,
    image2: graphics::Image,
//Option<graphics::Image>:这是一个可选的图像类型。Option 类型表示该值可能存在(Some 变体),也可能不存在(None 变体)。
//在代码中,用于表示某些 Mesh 是否关联有对应的图像
//graphics::Mesh:这是 ggez 库中的网格类型,用于定义复杂的图形形状,包含顶点、索引等数据,可用于绘制图形。
//在代码中,meshes 向量存储了多个带有或不带有关联图像的网格,在绘制时会根据是否有图像来选择不同的绘制方式,
//如 draw_textured_mesh 或 draw。
    meshes: Vec<(Option<graphics::Image>, graphics::Mesh)>,
//rect:这是一个类型为 graphics::Mesh 的属性。它代表一个矩形网格,在代码中通过 MeshBuilder 构建,用于绘制矩形图形。
//在绘制部分,使用 canvas.draw(&self.rect, graphics::DrawParam::default()); 来绘制这个矩形,DrawParam::default() 使用默认的绘制参数。
    rect: graphics::Mesh,
    rotation: f32,
}

impl MainState {
    /// Load images and create meshes.
    fn new(ctx: &mut Context) -> GameResult<MainState> {
        let image1 = graphics::Image::from_path(ctx, "/dragon1.png")?;
        let image2 = graphics::Image::from_path(ctx, "/shot.png")?;

        let mb = &mut graphics::MeshBuilder::new();
        mb.rectangle(
//graphics::DrawMode::stroke(1.0):该参数用于指定图形的绘制模式为描边模式,1.0表示描边的宽度。
//在绘制图形时,描边模式只绘制图形的轮廓。相比填充模式(fill),它不会填充图形内部,仅沿着图形的边界绘制线条。
//比如在绘制矩形时,使用stroke模式可以创建一个空心矩形,通过设置不同的描边宽度,可以调整线条的粗细,从而改变图形的外观。
            graphics::DrawMode::stroke(1.0),
//graphics::Rect::new(450.0, 450.0, 50.0, 50.0):这个参数定义了一个矩形的属性。Rect::new是创建矩形对象的方法,四个参数分别代表矩形的起始横坐标(x)、起始纵坐标(y)、宽度(width)和高度(height)。
//在代码中,这个矩形的左上角位于坐标(450.0, 450.0)的位置,宽度和高度均为50.0。在绘制图形时,这个矩形的位置和尺寸决定了图形在屏幕上的显示位置和大小。
            graphics::Rect::new(450.0, 450.0, 50.0, 50.0),
            graphics::Color::new(1.0, 0.0, 0.0, 1.0),
        )?;

        let rock = graphics::Image::from_path(ctx, "/rock.png")?;
// 这行代码创建了一个类型为Vec<(Option<graphics::Image>, graphics::Mesh)>的向量meshes。
//向量中包含两个元素,每个元素都是一个元组
        let meshes = vec![
//这个元组的第一个元素为None,表示该图形网格没有关联的图像。
//build_mesh(ctx)?调用了build_mesh函数,该函数构建了一个包含多种形状(线段、椭圆、圆形)的复合网格
            (None, build_mesh(ctx)?),
//元组的第一个元素为Some(rock),表示该图形网格关联了之前加载的rock图像。
//build_textured_triangle(ctx)调用了build_textured_triangle函数,该函数构建了一个带有纹理坐标和颜色的三角形网格。
//在后续绘制过程中,这个三角形网格会使用rock图像进行纹理映射,从而绘制出带有纹理的三角形。
            (Some(rock), build_textured_triangle(ctx)),
        ];

        let rect = graphics::Mesh::from_data(ctx, mb.build());

        let s = MainState {
            image1,
            image2,
//实际上meshes就是左上角那个半圆的彩虹,rect就是白的那一块
            meshes,
            rect,
//1.0 这个初始值决定了在程序启动时,依赖于该旋转值的图形(如 image2 )的起始旋转状态。
            rotation: 1.0,
        };

        Ok(s)
    }
}

fn build_mesh(ctx: &mut Context) -> GameResult<graphics::Mesh> {
    //MeshBuilder是 ggez 库中用于构建Mesh对象的工具,通过它可以方便地添加各种图形元素。
    let mb = &mut graphics::MeshBuilder::new();

    mb.line(
//mb.line:这是MeshBuilder的方法,用于添加线段到构建的网格中。
Vec2是glam库中的二维向量类型,每个点表示线段上的一个位置。这里定义的点构成了一条复杂的折线。
        &[
            Vec2::new(200.0, 200.0),
            Vec2::new(400.0, 200.0),
            Vec2::new(400.0, 400.0),
            Vec2::new(200.0, 400.0),
            Vec2::new(200.0, 300.0),
        ],
//4.0:表示线段的宽度为 4.0。
        4.0,
        Color::new(1.0, 0.0, 0.0, 1.0),
    )?;

    mb.ellipse(
//mb.ellipse:用于添加椭圆到网格。
//graphics::DrawMode::fill():指定椭圆的绘制模式为填充模式,即绘制一个实心椭圆。
//Vec2::new(600.0, 200.0):表示椭圆的中心位置在坐标 (600.0, 200.0)。
//1.0:表示椭圆的旋转角度为 1.0 弧度(这里的旋转角度可能不是直观的视觉旋转效果,具体取决于渲染管线)。
        graphics::DrawMode::fill(),
        Vec2::new(600.0, 200.0),
        50.0,
        120.0,
        1.0,
        Color::new(1.0, 1.0, 0.0, 1.0),
    )?;
//mb.circle:用于添加圆形到网格。
//graphics::DrawMode::fill():绘制模式为填充,绘制实心圆形。
//Vec2::new(600.0, 380.0):圆形的圆心位置在坐标 (600.0, 380.0)。
//40.0:圆形的半径为 40.0。
//1.0:圆形的旋转角度为 1.0 弧度。
//Color::new(1.0, 0.0, 1.0, 1.0):定义圆形的颜色为品红色(不透明)。
//?:用于错误处理。
    mb.circle(
        graphics::DrawMode::fill(),
        Vec2::new(600.0, 380.0),
        40.0,
        1.0,
        Color::new(1.0, 0.0, 1.0, 1.0),
    )?;

    Ok(graphics::Mesh::from_data(ctx, mb.build()))
}
//其实就是画那个半圆彩虹的
fn build_textured_triangle(ctx: &mut Context) -> graphics::Mesh {
    let triangle_verts = vec![
        graphics::Vertex {
//graphics::Vertex是ggez库中定义的顶点结构体,每个顶点包含以下信息:
//position:表示顶点在二维空间中的位置,这里分别定义了三个顶点的坐标[100.0, 100.0]、[0.0, 100.0]和[0.0, 0.0] 。这些坐标决定了三角形在屏幕上的形状和位置。
//uv:纹理坐标,用于指定在纹理图像上的采样位置。[1.0, 1.0]、[0.0, 1.0]和[0.0, 0.0]这些值决定了如何将纹理映射到三角形表面。
//例如,[0.0, 0.0]对应纹理图像的左上角,[1.0, 1.0]对应右下角。
            position: [100.0, 100.0],
            uv: [1.0, 1.0],
            color: [1.0, 0.0, 0.0, 1.0],
        },
        graphics::Vertex {
            position: [0.0, 100.0],
            uv: [0.0, 1.0],
            color: [0.0, 1.0, 0.0, 1.0],
        },
        graphics::Vertex {
            position: [0.0, 0.0],
            uv: [0.0, 0.0],
            color: [0.0, 0.0, 1.0, 1.0],
        },
    ];
//triangle_indices是一个Vec<u32>类型的向量,存储了三角形顶点的索引。
//在这个例子中,[0, 1, 2]表示按照triangle_verts向量中索引为 0、1、2 的顶点顺序来构建三角形。
//索引的使用可以提高渲染效率,特别是在处理复杂模型时,通过复用顶点数据来减少内存占用。
    let triangle_indices = vec![0, 1, 2];

    graphics::Mesh::from_data(
        ctx,
        graphics::MeshData {
            vertices: &triangle_verts,
            indices: &triangle_indices,
        },
    )
}

impl event::EventHandler<ggez::GameError> for MainState {
    fn update(&mut self, ctx: &mut Context) -> GameResult {
//帧率设置成60
        const DESIRED_FPS: u32 = 60;
//check_update_time方法用于检查距离上次更新是否达到了期望帧率所要求的时间间隔
//。如果达到了,该方法返回true,表示可以进行游戏状态更新;如果未达到,则返回false。
//在这个while循环中,只要check_update_time返回true,就会执行循环体。
//这两句结合起来意味着每隔 1/60 秒rotation就会增加 0.01 。
        while ctx.time.check_update_time(DESIRED_FPS) {
//在每次循环中,rotation的值会增加 0.01。这意味着随着时间的推移,与rotation相关的图形(例如在draw方法中使用rotation参数绘制的图像)会逐渐旋转,
//每次更新旋转角度增加 0.01 弧度。由于while循环会在满足帧率要求的时间间隔内不断执行,所以图形会以稳定的速度旋转。
            self.rotation += 0.01;
        }

        Ok(())
    }

    fn draw(&mut self, ctx: &mut Context) -> GameResult {
//创建画布.
//为什么要设置颜色?因为原本的图片里的有透明的部分,这样就能把底下的画布展示出来.
        let mut canvas =
            graphics::Canvas::from_frame(ctx, graphics::Color::from([0.1, 0.2, 0.3, 1.0]));

        // Draw an image.
        let dst = glam::Vec2::new(20.0, 20.0);
//graphics::DrawParam::new().dest(dst):创建一个绘制参数对象DrawParam,
//并使用dest方法设置图像的绘制目标位置为dst。DrawParam可以用于设置多种绘制参数,如位置、旋转、缩放等
        canvas.draw(&self.image1, graphics::DrawParam::new().dest(dst));

        // Draw an image with some options, and different filter modes.
        let dst = glam::Vec2::new(200.0, 100.0);
        let dst2 = glam::Vec2::new(400.0, 400.0);
        let scale = glam::Vec2::new(10.0, 10.0);

        canvas.draw(
            &self.image2,
            graphics::DrawParam::new()
                .dest(dst)
//旋转角度self.rotation(rotation在update方法中会不断变化)
                .rotation(self.rotation)
//缩放因子scale 。
                .scale(scale),
        );
//canvas.set_sampler(graphics::Sampler::nearest_clamp()):设置图像采样器为nearest_clamp模式,
//这种模式在采样时会使用最接近的像素,并且会将纹理坐标限制在0 - 1的范围内,防止纹理坐标超出范围导致的错误。
        canvas.set_sampler(graphics::Sampler::nearest_clamp());
        canvas.draw(
            &self.image2,
            graphics::DrawParam::new()
                .dest(dst2)
                .rotation(self.rotation)
                .scale(scale)
//offset方法设置了绘制偏移量vec2(0.5, 0.5) ,这会使图像在绘制时相对于目标位置有一个偏移
                .offset(vec2(0.5, 0.5)),
        );
//canvas.set_default_sampler():恢复默认的采样器设置,确保后续绘制操作不受之前设置的采样器影响。
        canvas.set_default_sampler();

        // Draw a filled rectangle mesh.
//let rect = graphics::Rect::new(450.0, 450.0, 50.0, 50.0):
//创建一个矩形对象rect,表示矩形的位置和大小,左上角坐标为(450.0, 450.0) ,宽度和高度均为50.0 。
        let rect = graphics::Rect::new(450.0, 450.0, 50.0, 50.0);
        canvas.draw(
    canvas.draw:绘制操作。&graphics::Quad表示绘制一个四边形,这里用于绘制矩形。
            &graphics::Quad,
            graphics::DrawParam::new()
                .dest(rect.point())
                .scale(rect.size())
                .color(Color::WHITE),
        );

        // Draw a stroked rectangle mesh.
//&self.rect:要绘制的描边矩形网格,self.rect是MainState结构体中创建的矩形网格对象
        canvas.draw(&self.rect, graphics::DrawParam::default());

        // Draw some pre-made meshes
//for (image, mesh) in &self.meshes:
//遍历MainState结构体中的meshes向量,meshes中存储了多个包含图像(可选)和网格的元组
        for (image, mesh) in &self.meshes {
//f let Some(image) = image:判断当前元组中的图像是否存在。
//如果存在,则使用canvas.draw_textured_mesh方法绘制带纹理的网格,需要传入网格的克隆对象mesh.clone()、
//图像的克隆对象image.clone()和默认的绘制参数DrawParam::new() 。
            if let Some(image) = image {
                canvas.draw_textured_mesh(mesh.clone(), image.clone(), graphics::DrawParam::new());
//如果图像不存在,则使用canvas.draw方法绘制普通网格,传入网格对象mesh和默认绘制参数DrawParam::new() 。
            } else {
                canvas.draw(mesh, graphics::DrawParam::new());
            }
        }

        // Finished drawing, show it all on the screen!
        canvas.finish(ctx)?;

        Ok(())
    }
}

pub fn main() -> GameResult {
    let resource_dir = if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
        let mut path = path::PathBuf::from(manifest_dir);
        path.push("resources");
        path
    } else {
        path::PathBuf::from("./resources")
    };

    let cb = ggez::ContextBuilder::new("drawing", "ggez").add_resource_path(resource_dir);

    let (mut ctx, events_loop) = cb.build()?;

    let state = MainState::new(&mut ctx).unwrap();
    event::run(ctx, events_loop, state)
}
最后的图: 7742f346aeeb3c60ee0fc4d3966d99dd_MD5


这篇完结,主要是下一个game太长了