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