想用 Rust 开发游戏?这份超详细的入门教程请收好!

想用 Rust 开发游戏?这份超详细的入门教程请收好!

"Talk is cheap, show me the code." 学习一门新语言,最快的方式莫过于动手做一个有趣的项目。你是否曾对游戏开发充满好奇,却又被复杂的概念和庞大的游戏引擎劝退?

今天,就让我们以一种轻松有趣的方式,走进 Rust 游戏开发的世界。本文将带你使用对新手极其友好的 Rust 游戏库 bracket-lib,一步步构建一个经典的微型游戏------"Flappy Dragon"。

我们将从零开始,搭建项目、理解核心的"游戏循环(Game Loop)"、设计不同的游戏模式,并逐步添加玩家、障碍物和计分系统。无论你是 Rust 新手,还是对游戏开发感兴趣的开发者,都能通过这个项目,快速掌握游戏开发的基本脉络和 Rust 的实践应用。

准备好了吗?让我们一起敲下代码,召唤出属于自己的第一条"小龙"吧!

使用 Rust 构建微型游戏 | 轻松理解游戏开发核心

一、 创建游戏

Agenda

  • 建立项目
  • 实现 Game loop
  • 不同的游戏模式
  • 添加玩家
  • 添加障碍和计分
  • 汇总

理解 Game loop

为了让游戏流畅、顺滑的运行,需要使用 Game loop

Game loop:

  • 初始化窗口、图形和其它资源
  • 每当屏幕刷新(通常是每秒 30、60或更多次),它都会运行
  • 每次通过循环,它都会调用游戏的 tick() 函数

Game loop

开始 -> 配置 App、Window 和图形 -> Poll (轮询 OS 监听输入状态 -> 调用 tick() 函数 -> 更新屏幕 -> 停止? -> 退出

游戏引擎

  • 游戏引擎用来处理平台特定的部分
  • 以便开发者专心开发游戏

Bracket-Lib (Amethyst Foundation)

Bracket-Lib 是一个 Rust 游戏编程库:

  • 作为简单的教学工具
  • 抽象了游戏开发很多复杂的东西
  • 但保留了相关的概念

Bracket-Lib 包括很多库:

  • 随机数生成、几何、路径寻找、颜色处理、常用算法等

Bracket-terminal

bracket-terminal 是 Bracket-Lib 中负责显示部分

  • 它提供了模拟控制台
  • 可与多种渲染平台配合:
    • 从文本控制台到 Web Assembly
    • 例如:OpenGL、Vulkan、Metal
  • 支持 sprites 和原生 OpenGL 开发

Codepage 437:IBM 扩展 ASCII 字符集

Codepage 437:

  • 来自 Dos PC 上的字符,用于终端输出,除了字母和数字,还提供了一些符号
  • Bracket-lib 会把字符翻译成图形 sprites 并提供一个有限的字符集,字符所展示的是相应的图片
bash 复制代码
~ via 🅒 base
➜ cd rust

~/rust via 🅒 base
➜ cargo new flappy
     Created binary (application) `flappy` package

~/rust via 🅒 base
➜ cd flappy

flappy on  master [?] via 🦀 1.67.1 via 🅒 base
➜ c

flappy on  master [?] via 🦀 1.67.1 via 🅒 base
➜

main.rs

rust 复制代码
use bracket_lib::prelude::*;

struct State {}

impl GameState for State {
    fn tick(&mut self, ctx: &mut BTerm) {
        ctx.cls();
        ctx.print(1, 1, "Hello, Bracket Terminal!");
    }
}

fn main() -> BError {
    let context = BTermBuilder::simple80x50()
        .with_title("Flappy Dragon")
        .build()?;

    main_loop(context, State {})
}

游戏的模式

  • 游戏通常在不同的模式中运行
  • 每种模式会明确游戏在当前的 tick() 中应该做什么

我们这个游戏需要 3 种模式:

  • 菜单
  • 游戏中
  • 结束
rust 复制代码
use bracket_lib::prelude::*;

enum GameMode {
    Menu,
    Playing,
    End,
}

struct State {
    mode: GameMode,
}

impl State {
    fn new() -> Self {
        State {
            mode: GameMode::Menu,
        }
    }

    fn play(&mut self, ctx: &mut BTerm) {
        // TODO
        self.mode = GameMode::End;
    }

    fn restart(&mut self) {
        self.mode = GameMode::Playing;
    }

    fn main_menu(&mut self, ctx: &mut BTerm) {
        ctx.cls();
        ctx.print_centered(5, "Welcome to Flappy Dragon");
        ctx.print_centered(8, "(P) Play Game");
        ctx.print_centered(9, "(Q) Quit Game");

        if let Some(key) = ctx.key {
            match key {
                VirtualKeyCode::P => self.restart(),
                VirtualKeyCode::Q => ctx.quitting = true,
                _ => {}
            }
        }
    }

    fn dead(&mut self, ctx: &mut BTerm) {
        ctx.cls();
        ctx.print_centered(5, "You are dead!");
        ctx.print_centered(8, "(P) Play Game");
        ctx.print_centered(9, "(Q) Quit Game");

        if let Some(key) = ctx.key {
            match key {
                VirtualKeyCode::P => self.restart(),
                VirtualKeyCode::Q => ctx.quitting = true,
                _ => {}
            }
        }
    }
}

impl GameState for State {
    fn tick(&mut self, ctx: &mut BTerm) {
        match self.mode {
            GameMode::Menu => self.main_menu(ctx),
            GameMode::End => self.dead(ctx),
            GameMode::Playing => self.play(ctx),
        }
    }
}

fn main() -> BError {
    let context = BTermBuilder::simple80x50()
        .with_title("Flappy Dragon")
        .build()?;

    main_loop(context, State::new())
}

现在我们有了基本的游戏框架和模式切换,是时候让主角------我们的小龙(Player)登场了!

二、添加 Player

main.rs

rust 复制代码
use bracket_lib::prelude::*;

enum GameMode {
    Menu,
    Playing,
    End,
}

const SCREEN_WIDTH: i32 = 80;
const SCREEN_HEIGHT: i32 = 50;
const FRAME_DURATION: f32 = 75.0;

struct Player {
    x: i32,
    y: i32,
    velocity: f32,
}

impl Player {
    // fn new(x: i32, y: i32) -> Self {
    //   Player {
    //        x: 0,
    //        y: 0,
         //   velocity: 0.0,
       // }
    // }
    
    fn new(x: i32, y: i32) -> Self {
        Player {
            x, // Correctly uses the x from the function argument
            y, // Correctly uses the y from the function argument
            velocity: 0.0,
        }
    }

    fn render(&mut self, ctx: &mut BTerm) {
        ctx.set(0, self.y, YELLOW, BLACK, to_cp437('@'))
    }

    fn gravity_and_move(&mut self) {
        if self.velocity < 2.0 {
            self.velocity += 0.2;
        }
        self.y += self.velocity as i32;
        self.x += 1;

        if self.y < 0 {
            self.y = 0;
        }
    }

    fn flap(&mut self) {
        self.velocity = -2.0; // 往上飞是负的
    }
}

struct State {
    player: Player,
    frame_time: f32,
    mode: GameMode,
}

impl State {
    fn new() -> Self {
        State {
            player: Player::new(5, 25),
            frame_time: 0.0,
            mode: GameMode::Menu,
        }
    }

    fn play(&mut self, ctx: &mut BTerm) {
        ctx.cls_bg(NAVY);
        self.frame_time += ctx.frame_time_ms;

        if self.frame_time > FRAME_DURATION {
            self.frame_time = 0.0;
            self.player.gravity_and_move();
        }

        if let Some(VirtualKeyCode::Space) = ctx.key {
            self.player.flap();
        }

        self.player.render(ctx);
        ctx.print(0, 0, "Press Space to Flap");

        if self.player.y > SCREEN_HEIGHT {
            self.mode = GameMode::End;
        }
    }

    fn restart(&mut self) {
        self.player = Player::new(5, 25);
        self.frame_time = 0.0;
        self.mode = GameMode::Playing;
    }

    fn main_menu(&mut self, ctx: &mut BTerm) {
        ctx.cls();
        ctx.print_centered(5, "Welcome to Flappy Dragon");
        ctx.print_centered(8, "(P) Play Game");
        ctx.print_centered(9, "(Q) Quit Game");

        if let Some(key) = ctx.key {
            match key {
                VirtualKeyCode::P => self.restart(),
                VirtualKeyCode::Q => ctx.quitting = true,
                _ => {}
            }
        }
    }

    fn dead(&mut self, ctx: &mut BTerm) {
        ctx.cls();
        ctx.print_centered(5, "You are dead!");
        ctx.print_centered(8, "(P) Play Game");
        ctx.print_centered(9, "(Q) Quit Game");

        if let Some(key) = ctx.key {
            match key {
                VirtualKeyCode::P => self.restart(),
                VirtualKeyCode::Q => ctx.quitting = true,
                _ => {}
            }
        }
    }
}

impl GameState for State {
    fn tick(&mut self, ctx: &mut BTerm) {
        match self.mode {
            GameMode::Menu => self.main_menu(ctx),
            GameMode::End => self.dead(ctx),
            GameMode::Playing => self.play(ctx),
        }
    }
}

fn main() -> BError {
    let context = BTermBuilder::simple80x50()
        .with_title("Flappy Dragon")
        .build()?;

    main_loop(context, State::new())
}

小龙能在世界里飞行了,但还缺少挑战。接下来,我们将为它添加障碍物和计分系统,让游戏变得完整。

三、添加障碍

rust 复制代码
use bracket_lib::prelude::*;

enum GameMode {
    Menu,
    Playing,
    End,
}

const SCREEN_WIDTH: i32 = 80;
const SCREEN_HEIGHT: i32 = 50;
const FRAME_DURATION: f32 = 75.0;

struct Player {
    x: i32, // 世界空间
    y: i32,
    velocity: f32,
}

impl Player {
    fn new(x: i32, y: i32) -> Self {
        Player {
            x,
            y,
            velocity: 0.0,
        }
    }

    fn render(&mut self, ctx: &mut BTerm) {
        ctx.set(0, self.y, YELLOW, BLACK, to_cp437('@'))
    }

    fn gravity_and_move(&mut self) {
        if self.velocity < 2.0 {
            self.velocity += 0.2;
        }
        self.y += self.velocity as i32;
        self.x += 1;

        if self.y < 0 {
            self.y = 0;
        }
    }

    fn flap(&mut self) {
        self.velocity = -2.0; // 往上飞是负的
    }
}

struct State {
    player: Player,
    frame_time: f32,
    mode: GameMode,
    obstacle: Obstacle,
    score: i32,
}

impl State {
    fn new() -> Self {
        State {
            player: Player::new(5, 25),
            frame_time: 0.0,
            mode: GameMode::Menu,
            obstacle: Obstacle::new(SCREEN_WIDTH, 0),
            score: 0,
        }
    }

    fn play(&mut self, ctx: &mut BTerm) {
        ctx.cls_bg(NAVY);
        self.frame_time += ctx.frame_time_ms;

        if self.frame_time > FRAME_DURATION {
            self.frame_time = 0.0;
            self.player.gravity_and_move();
        }

        if let Some(VirtualKeyCode::Space) = ctx.key {
            self.player.flap();
        }

        self.player.render(ctx);
        ctx.print(0, 0, "Press Space to Flap");
        ctx.print(0, 1, &format!("Score: {}", self.score));

        self.obstacle.render(ctx, self.player.x);
        if self.player.x > self.obstacle.x {
            self.score += 1;
            self.obstacle = Obstacle::new(self.player.x + SCREEN_WIDTH, self.score);
        }

        if self.player.y > SCREEN_HEIGHT || self.obstacle.hit_obstacle(&self.player) {
            self.mode = GameMode::End;
        }
    }

    fn restart(&mut self) {
        self.player = Player::new(5, 25);
        self.frame_time = 0.0;
        self.mode = GameMode::Playing;
        self.obstacle = Obstacle::new(SCREEN_WIDTH, 0);
        self.score = 0;
    }

    fn main_menu(&mut self, ctx: &mut BTerm) {
        ctx.cls();
        ctx.print_centered(5, "Welcome to Flappy Dragon");
        ctx.print_centered(8, "(P) Play Game");
        ctx.print_centered(9, "(Q) Quit Game");

        if let Some(key) = ctx.key {
            match key {
                VirtualKeyCode::P => self.restart(),
                VirtualKeyCode::Q => ctx.quitting = true,
                _ => {}
            }
        }
    }

    fn dead(&mut self, ctx: &mut BTerm) {
        ctx.cls();
        ctx.print_centered(5, "You are dead!");
        ctx.print_centered(6, &format!("You earned {} points", self.score));
        ctx.print_centered(8, "(P) Play Game");
        ctx.print_centered(9, "(Q) Quit Game");

        if let Some(key) = ctx.key {
            match key {
                VirtualKeyCode::P => self.restart(),
                VirtualKeyCode::Q => ctx.quitting = true,
                _ => {}
            }
        }
    }
}

impl GameState for State {
    fn tick(&mut self, ctx: &mut BTerm) {
        match self.mode {
            GameMode::Menu => self.main_menu(ctx),
            GameMode::End => self.dead(ctx),
            GameMode::Playing => self.play(ctx),
        }
    }
}

struct Obstacle {
    x: i32, // 世界空间
    gap_y: i32,
    size: i32,
}

impl Obstacle {
    fn new(x: i32, score: i32) -> Self {
        let mut random = RandomNumberGenerator::new();
        Obstacle {
            x,
            gap_y: random.range(10, 40),
            size: i32::max(2, 20 - score),
        }
    }

    fn render(&mut self, ctx: &mut BTerm, player_x: i32) {
        let screen_x = self.x - player_x; // 屏幕空间
        let half_size = self.size / 2;

        for y in 0..self.gap_y - half_size {
            ctx.set(screen_x, y, RED, BLACK, to_cp437('|'));
        }

        for y in self.gap_y + half_size..SCREEN_HEIGHT {
            ctx.set(screen_x, y, RED, BLACK, to_cp437('|'))
        }
    }

    fn hit_obstacle(&self, player: &Player) -> bool {
        let half_size = self.size / 2;
        let does_x_match = player.x == self.x;
        let player_above_gap = player.y < self.gap_y - half_size;
        let player_below_gap = player.y > self.gap_y + half_size;
        does_x_match && (player_above_gap || player_below_gap)
    }
}

fn main() -> BError {
    let context = BTermBuilder::simple80x50()
        .with_title("Flappy Dragon")
        .build()?;

    main_loop(context, State::new())
}

这里的 player_x 代表了玩家在整个游戏世界中的坐标(世界空间),而 screen_x 则是障碍物相对于窗口左侧的坐标(屏幕空间)。通过这种转换,我们实现了摄像机跟随玩家移动的效果。

总结

恭喜你,坚持看到了这里!现在,你已经拥有了一个麻雀虽小五脏俱全的"Flappy Dragon"游戏。

回顾整个过程,我们从一个空的 main.rs 文件开始,借助强大的 bracket-lib 库,一步步实现了:

  1. 游戏框架: 搭建了项目,并理解了游戏引擎的核心------Game Loop。
  2. 状态管理: 使用 enum 为游戏设计了菜单、进行中和结束三种清晰的模式。
  3. 玩家和物理: 创建了玩家 Player 结构体,并模拟了重力和跳跃(flap)等简单物理效果。
  4. 游戏核心玩法: 设计并实现了动态生成的障碍物、碰撞检测和计分逻辑。

通过这个项目,我们不仅实践了 Rust 的基础语法和结构体(struct)的使用,更重要的是,我们亲手触摸到了游戏开发的脉搏。bracket-lib 为我们抽象了底层渲染的复杂性,让我们能聚焦于游戏逻辑本身,这对于学习和理解游戏开发概念至关重要。

当然,这个游戏还很简单,但它为你打开了一扇门。接下来,你可以尝试:

  • 美化界面:用 to_cp437() 尝试更多有趣的字符来代表小龙和障碍。
  • 增加难度:随着分数增加,让小龙飞得更快,或者让障碍的间隙更小。
  • 添加音效:探索 Rust 的音频库,为跳跃和得分增加反馈。

希望这篇教程能点燃你对 Rust 和游戏开发的兴趣。编程的乐趣,正在于创造。继续探索,继续创造吧!

参考

相关推荐
大学生资源网16 分钟前
基于springboot的万亩助农网站的设计与实现源代码(源码+文档)
java·spring boot·后端·mysql·毕业设计·源码
苏三的开发日记25 分钟前
linux端进行kafka集群服务的搭建
后端
苏三的开发日记43 分钟前
windows系统搭建kafka环境
后端
爬山算法1 小时前
Netty(19)Netty的性能优化手段有哪些?
java·后端
Tony Bai1 小时前
Cloudflare 2025 年度报告发布——Go 语言再次“屠榜”API 领域,AI 流量激增!
开发语言·人工智能·后端·golang
想用offer打牌1 小时前
虚拟内存与寻址方式解析(面试版)
java·后端·面试·系统架构
無量1 小时前
AQS抽象队列同步器原理与应用
后端
9号达人2 小时前
支付成功订单却没了?MyBatis连接池的坑我踩了
java·后端·面试
用户497357337982 小时前
【轻松掌握通信协议】C#的通信过程与协议实操 | 2024全新
后端
草莓熊Lotso2 小时前
C++11 核心精髓:类新功能、lambda与包装器实战
开发语言·c++·人工智能·经验分享·后端·nginx·asp.net