在这里插入图片描述

下面讲解的是一份非常经典的 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)

这是游戏运行的核心,继承自 ggezEventHandler 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 框架最标准的项目结构:

  1. 定义数据 (Structs)
  2. 实现逻辑 (update - 处理输入和移动)
  3. 实现渲染 (draw - 绘制图形)
  4. 启动循环 (main)

这是一个非常完美的 2D 游戏开发入门范例,这种结构很清爽。


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐