用ggez游戏框架学Rust:游戏开发实战指南

下面讲解的是一份非常经典的 Rust + ggez 2D 射击游戏代码。我将按照代码的逻辑顺序,从上到下逐行拆解游戏代码的“前世今生”。
代码实现了一个简单的“打飞机”游戏,包含玩家控制、射击、敌人生成、碰撞检测和游戏状态管理。
第一部分:依赖引入与常量定义
这部分代码告诉编译器我们需要用到哪些外部库,并定义了游戏的“物理规则”。
//引入event模块和EventHandler
use ggez::event::{self, EventHandler};
use ggez::graphics::{self, Color, DrawMode, Mesh, Rect, Text, TextFragment};
use ggez::input::keyboard::KeyCode;
use ggez::timer;
use ggez::{Context, ContextBuilder, GameResult};
use ggez::glam::Vec2;
use rand::Rng;
- 解释:引入了
ggez框架的核心模块(事件处理、图形绘制、键盘输入、时间管理)和rand随机数库。Vec2用于处理二维坐标。
// 游戏常量
const SCREEN_WIDTH: f32 = 800.0; //屏幕宽度
const SCREEN_HEIGHT: f32 = 600.0;
const PLAYER_SPEED: f32 = 300.0; //玩家移动速度
const BULLET_SPEED: f32 = 500.0; //子弹速度
const ENEMY_SPEED: f32 = 100.0; //敌人速度
const SPAWN_INTERVAL: f32 = 1.5; // 敌人生成间隔(秒)
- 解释:定义了游戏窗口大小和各种移动速度。这些是游戏的“平衡性参数”。
第二部分:数据结构定义 (Structs)
这里定义了游戏中所有物体的“蓝图”。
// 游戏实体
#[derive(Clone)]
struct Player {
pos: Vec2,
size: f32,
hp: i32,
}
- 解释:定义了玩家飞船,包含位置 (
pos)、大小 (size) 和血量 (hp)。
#[derive(Clone)]
struct Bullet {
pos: Vec2,
velocity: Vec2,
active: bool,
}
- 解释:定义了子弹,包含位置、速度向量 (
velocity) 和一个活跃状态标记。
#[derive(Clone)]
struct Enemy {
pos: Vec2,
size: f32,
speed: f32,
hp: i32,
active: bool,
}
- 解释:定义了敌人,结构与玩家类似,但多了独立的速度属性(让每一个敌人有自己的速度)
// 游戏状态
struct MainState {
player: Player,
bullets: Vec<Bullet>,//字段集合
enemies: Vec<Enemy>, //敌人集合
score: u32,
spawn_timer: f32, //生成敌人间隔计时器
game_over: bool,
stars: Vec<Vec2>, // 背景星星
paused: bool, // 游戏是否暂停
fire_counts:u32, // 射击计数器(用于演示)
}
- 解释:这是游戏的“大脑”。它持有了所有当前存在的对象(玩家、子弹列表、敌人列表)以及全局状态(分数、计时器、游戏是否结束)。
第三部分:核心逻辑实现 (Impl MainState)
这里定义了游戏对象的行为方法。
impl MainState {
fn new() -> GameResult<MainState> {
//mut关键字让变量stars在后面的代码可改变
let mut stars = Vec::new();
let mut rng = rand::rng(); // 用来随机生成背景星星
for _ in 0..50 {
stars.push(Vec2::new(
rng.random_range(0.0..SCREEN_WIDTH),
rng.random_range(0.0..SCREEN_HEIGHT),
));
}
// 初始化 MainState 结构体
let s = MainState {
player: Player {
pos: Vec2::new(SCREEN_WIDTH / 2.0, SCREEN_HEIGHT - 50.0),
size: 20.0,
hp: 30,
},
bullets: Vec::new(),
enemies: Vec::new(),
score: 0,
spawn_timer: 0.0,
game_over: false,
stars,
paused: false,
fire_counts: 0,
};
Ok(s)
}
// 以下是函数定义...
}
- 解释:
new方法是游戏的“出生点”。它创建了 50 颗随机位置的背景星星,并初始化玩家位置。左上角为原点(0,0)坐标。
// 重置游戏
fn reset(&mut self) {
self.player = Player {
pos: Vec2::new(SCREEN_WIDTH / 2.0, SCREEN_HEIGHT - 50.0),
size: 20.0,
hp: 3,
};
self.bullets.clear();
self.enemies.clear();
self.score = 0;
self.spawn_timer = 0.0;
self.game_over = false;
self.paused = false; // 重置时游戏直接执行
self.fire_counts=0;
}
// 生成敌人
fn spawn_enemy(&mut self) {
let mut rng = rand::rng();
let x = rng.random_range(30.0..SCREEN_WIDTH - 30.0);
self.enemies.push(Enemy {
pos: Vec2::new(x, -30.0),//敌人初始化在屏幕外
size: 25.0,
speed: ENEMY_SPEED + rng.random_range(0.0..100.0),
hp: 2,
active: true,
});
}
// 射击
fn shoot(&mut self) {
self.fire_counts+=2;
self.bullets.push(Bullet {
pos: self.player.pos + Vec2::new(0.0, -20.0),
velocity: Vec2::new(0.0, -BULLET_SPEED),
active: true,
});
self.fire_counts-=1;
}
// 碰撞检测
fn check_collision(a_pos: Vec2, a_size: f32, b_pos: Vec2, b_size: f32) -> bool {
let distance = a_pos.distance(b_pos);
distance < (a_size + b_size) / 2.0
}
// 增加分数
fn add_score(&mut self, points: u32) {
self.score += points;
}
-
解释:
&T(不可变引用)允许多个“读者”同时读取数据,但不能修改。&mut T(可变引用)允许“写者”修改数据,但同一时间只能有一个可变引用,且不能有其他不可变引用共存。这保证了数据在修改时不会被其他代码意外读取(即“数据竞争”)。shoot方法中:fire_counts先加 2 再减 1,最终结果是加 1。这是一段故意设计的控制代码,方便研究汇编代码。碰撞逻辑,这是最基础的圆形碰撞检测。计算两个物体中心点的距离,如果距离小于它们半径之和,则判定为碰撞。
可以把MainState看作一个类,其中定义了属性字段和方法(fn)
第四部分:游戏主循环 (EventHandler)
这是游戏运行的核心,继承自 ggez 的 EventHandler trait。游戏会不停地循环执行这update、draw两个方法。
impl EventHandler for MainState {
fn update(&mut self, ctx: &mut Context) -> GameResult {}
fn draw(&mut self, ctx: &mut Context) -> GameResult {}
}
1. update 方法:处理逻辑
相当于准备好数据,给draw绘制。
fn update(&mut self, ctx: &mut Context) -> GameResult {
// 暂停逻辑:按 P 键切换暂停
if ctx.keyboard.is_key_just_pressed(KeyCode::P) {
self.paused = !self.paused;
}
if self.game_over {
// 按 R 重新开始
if ctx.keyboard.is_key_pressed(KeyCode::R) {
self.reset();
}
return Ok(());
}
if self.paused {
return Ok(());
}
let dt = ctx.time.delta().as_secs_f32(); // 获取上一帧到这一帧的时间差
- 解释:
dt(Delta Time) 非常重要,乘以它能让游戏速度在不同电脑上保持一致。
// ===== 玩家移动 =====
let mut movement = Vec2::ZERO;
// 检测键盘方向键或 WASD
if ctx.keyboard.is_key_pressed(KeyCode::Left) || ctx.keyboard.is_key_pressed(KeyCode::A) {
movement.x -= 1.0;
}
if ctx.keyboard.is_key_pressed(KeyCode::Right) || ctx.keyboard.is_key_pressed(KeyCode::D) {
movement.x += 1.0;
}
if ctx.keyboard.is_key_pressed(KeyCode::Up) || ctx.keyboard.is_key_pressed(KeyCode::W) {
movement.y -= 1.0;
}
if ctx.keyboard.is_key_pressed(KeyCode::Down) || ctx.keyboard.is_key_pressed(KeyCode::S) {
movement.y += 1.0;
}
// 归一化对角线移动,防止斜向移动过快
if movement.length() > 0.0 { movement = movement.normalize(); }
self.player.pos += movement * PLAYER_SPEED * dt;
// 边界限制,不让玩家飞出屏幕
self.player.pos.x = self.player.pos.x.clamp(20.0, SCREEN_WIDTH - 20.0);
self.player.pos.y = self.player.pos.y.clamp(20.0, SCREEN_HEIGHT - 20.0);
- 解释:这是标准的向量移动逻辑,包含防对角线加速的归一化处理。
// ===== 射击(空格键)=====
// 简单的射击冷却,仅仅响应按下空格次数,一致按着无效
if ctx.keyboard.is_key_just_pressed(KeyCode::Space) {
self.shoot();//调用shoot方法
}
// ===== 每一帧都更新子弹数据,实现子弹移动效果 =====
for bullet in &mut self.bullets {
bullet.pos += bullet.velocity * dt;
// 超出屏幕
if bullet.pos.y < -10.0 {
bullet.active = false;
}
}
// ===== 敌人生成 =====
self.spawn_timer += dt;
if self.spawn_timer >= SPAWN_INTERVAL {
self.spawn_enemy();
self.spawn_timer = 0.0;
}
// ===== 更新敌人 =====
for enemy in &mut self.enemies {
enemy.pos.y += enemy.speed * dt;
// 超出屏幕底部
if enemy.pos.y > SCREEN_HEIGHT + 30.0 {
enemy.active = false;
// 敌人逃脱,玩家是否扣血
// self.player.hp -= 1;
// if self.player.hp <= 0 {
// self.game_over = true;
// }
}
}
- 解释:计时器控制敌人生成频率;循环更新所有子弹和敌人的位置。
// ===== 碰撞检测逻辑 =====
// 打中敌人的子弹索引集合
let mut bullets_to_deactivate = Vec::new();
// 被打中的敌人集合, 临时保存为三元组(idx, hp_change, deactivate)
let mut enemy_changes = Vec::new(); //
let mut score_to_add = 0;
//遍历子弹
for (bullet_idx, bullet) in self.bullets.iter().enumerate() {
if !bullet.active { continue; }
//遍历敌人
for (enemy_idx, enemy) in self.enemies.iter().enumerate() {
if !enemy.active { continue; }
if Self::check_collision(bullet.pos, 5.0, enemy.pos, enemy.size) {
bullets_to_deactivate.push(bullet_idx);
enemy_changes.push((enemy_idx, -1, false));
break;
}
}
}
// 应用变化
for &idx in &bullets_to_deactivate {
if idx < self.bullets.len() {
self.bullets[idx].active = false;
}
}
for (idx, hp_change, _) in enemy_changes {
if idx < self.enemies.len() {
self.enemies[idx].hp += hp_change;
if self.enemies[idx].hp <= 0 {
self.enemies[idx].active = false;
score_to_add += 10;
}
}
}
self.add_score(score_to_add);
// 敌人撞玩家
for enemy in &mut self.enemies {
if enemy.active
&& Self::check_collision(self.player.pos, self.player.size, enemy.pos, enemy.size)
{
enemy.active = false;
self.player.hp -= 1;
if self.player.hp <= 0 {
self.game_over = true;
}
}
}
// 移除无效敌人
self.enemies.retain(|e| e.active);
// 移除无效子弹
self.bullets.retain(|b| b.active);
// ===== 更新星星背景 =====
for star in &mut self.stars {
star.y += 50.0 * dt; // 星星下落
if star.y > SCREEN_HEIGHT {
star.y = 0.0;
star.x = rand::rng().random_range(0.0..SCREEN_WIDTH);
}
}
Ok(())
}//fn update结束
-
解释:这是游戏的“战斗系统”。检测到碰撞后,标记对象为非活跃,然后通过
retain方法从列表中物理删除。 -
闭包:|b| …
这是 Rust 的闭包(Closure)语法,类似于匿名函数。
|b|:这是参数列表。b代表bullets列表中的每一个子弹对象。b.active:这是闭包的主体。它返回一个布尔值。
借用(&)和消耗
for (idx, hp_change, _) in enemy_changes { if idx < self.enemies.len() { self.enemies[idx].hp += hp_change; if self.enemies[idx].hp <= 0 { self.enemies[idx].active = false; score_to_add += 10; } } } //enemy_changes.clear();编译器提示错误! -
不用像C++那样主动销毁临时指针delete bullets_to_deactivate、enemy_changes
2. draw 方法:绘制画面
fn draw(&mut self, ctx: &mut Context) -> GameResult {
let mut canvas = graphics::Canvas::from_frame(ctx, Color::new(0.1, 0.1, 0.2, 1.0));
- 解释:创建一个画布,背景色是深蓝色(模拟太空)。
// 绘制背景星星
for star in &self.stars {
let star_mesh = Mesh::new_circle(ctx, DrawMode::fill(), *star, 1.5, 0.1, Color::WHITE)?;
canvas.draw(&star_mesh, graphics::DrawParam::default());
}
- 解释:画出白色的圆点作为背景。 *star:你需要用
*解引用操作符来获取具体的坐标值传给Mesh::new_circle
// ===== 绘制暂停提示 =====
if self.paused {
let paused_text = Text::new(TextFragment {
text: "PAUSED".to_string(),
color: Some(Color::YELLOW),
font: Some("LiberationMono-Regular".into()),
scale: Some(graphics::PxScale::from(50.0)),
..Default::default()
});
canvas.draw(
&paused_text,
Vec2::new(SCREEN_WIDTH / 2.0 - 100.0, SCREEN_HEIGHT / 2.0 - 30.0),
);
}
if self.game_over {
// ===== 游戏结束画面 =====
let game_over_text = Text::new(TextFragment {
text: "GAME OVER".to_string(),
color: Some(Color::RED),
font: Some("LiberationMono-Regular".into()),
scale: Some(graphics::PxScale::from(60.0)),
..Default::default()
});
let restart_text = Text::new(TextFragment {
text: "Press R to restart".to_string(),
color: Some(Color::WHITE),
font: Some("LiberationMono-Regular".into()),
scale: Some(graphics::PxScale::from(30.0)),
..Default::default()
});
canvas.draw(
&game_over_text,
Vec2::new(SCREEN_WIDTH / 2.0 - 180.0, SCREEN_HEIGHT / 2.0 - 80.0),
);
canvas.draw(
&restart_text,
Vec2::new(SCREEN_WIDTH / 2.0 - 160.0, SCREEN_HEIGHT / 2.0 + 80.0),
);
} else {
// ===== 绘制玩家(多边形-绿色三角形)=====
let player_mesh = Mesh::new_polygon(
ctx,
DrawMode::fill(),
&[
Vec2::new(self.player.pos.x, self.player.pos.y - self.player.size), // 顶点
Vec2::new(self.player.pos.x - self.player.size / 2.0,
self.player.pos.y + self.player.size / 2.0,
), // 左下
Vec2::new(self.player.pos.x + self.player.size / 2.0,
self.player.pos.y + self.player.size / 2.0,
), // 右下
],
Color::GREEN,
)?;
canvas.draw(&player_mesh, graphics::DrawParam::default());
// 绘制血条(红色矩形)
for i in 0..self.player.hp {
let hp_mesh = Mesh::new_rectangle(
ctx,
DrawMode::fill(),
Rect::new(10.0 + i as f32 * 25.0, 10.0, 20.0, 5.0),
Color::RED,
)?;
canvas.draw(&hp_mesh, graphics::DrawParam::default());
}
// ===== 绘制子弹(黄色小矩形)=====
for bullet in &self.bullets {
let bullet_mesh = Mesh::new_rectangle(
ctx,
DrawMode::fill(),
Rect::new(bullet.pos.x - 3.0, bullet.pos.y - 8.0, 6.0, 16.0),
Color::YELLOW,
)?;
canvas.draw(&bullet_mesh, graphics::DrawParam::default());
}
for enemy in &self.enemies {
// ===== 绘制敌人(红色方块)=====
// 敌人血条
}
// 绘制分数等
}
canvas.finish(ctx)?; // 提交绘制指令
timer::yield_now(); // 让出 CPU 时间片
Ok(())
}//fn draw结束
}//impl EventHandler for MainState 结束
- 解释:根据游戏状态绘制不同的 UI。正常游戏中绘制飞船、血条和分数;游戏结束时绘制大大的 GAME OVER 提示。
- 代码中 ? 是 Rust 中一个非常强大和常用的错误传播操作符。
它的作用可以理解为:“如果前面的操作成功了,就拿到结果;如果失败了,就立刻把错误返回给调用者。” - 调用
yield_now()可以告诉操作系统:“我这帧没事干了,你可以去调度其他程序,或者让我的 CPU 核心休息一下。” 这可以显著降低程序的功耗和发热。
第五部分:程序入口 (main)
fn main() -> GameResult {
let cb = ContextBuilder::new("space_shooter", "奥利顶")
.window_setup(ggez::conf::WindowSetup::default().title("Space Shooter - 标题"))
.window_mode(
ggez::conf::WindowMode::default()
.dimensions(SCREEN_WIDTH, SCREEN_HEIGHT)
.resizable(false),
);
let (ctx, event_loop) = cb.build()?;
let state = MainState::new()?;
event::run(ctx, event_loop, state)
}
- 解释:这是程序的起点。它创建了游戏窗口,设置了标题和大小,初始化了
MainState,最后启动了ggez的事件循环。游戏从此刻开始正式运行。
总结
这段代码虽然不长,但麻雀虽小五脏俱全。它展示了 ggez 框架最标准的项目结构:
- 定义数据 (Structs)
- 实现逻辑 (update - 处理输入和移动)
- 实现渲染 (draw - 绘制图形)
- 启动循环 (main)
这是一个非常完美的 2D 游戏开发入门范例,这种结构很清爽。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)