想用 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 和游戏开发的兴趣。编程的乐趣,正在于创造。继续探索,继续创造吧!

参考

相关推荐
free-9d2 小时前
NodeJs后端常用三方库汇总
后端·node.js
写不出来就跑路4 小时前
WebClient与HTTPInterface远程调用对比
java·开发语言·后端·spring·springboot
天上掉下来个程小白5 小时前
MybatisPlus-06.核心功能-自定义SQL
java·spring boot·后端·sql·微服务·mybatisplus
知了一笑5 小时前
独立开发第二周:构建、执行、规划
java·前端·后端
晴空月明5 小时前
分布式系统高可用性设计 - 缓存策略与数据同步机制
后端
Real_man6 小时前
新物种与新法则:AI重塑开发与产品未来
前端·后端·面试
小马爱打代码7 小时前
Spring Boot:将应用部署到Kubernetes的完整指南
spring boot·后端·kubernetes
卜锦元7 小时前
Go中使用wire进行统一依赖注入管理
开发语言·后端·golang
SoFlu软件机器人7 小时前
Cursor、飞算JavaAI、GitHub Copilot、Gemini CLI 等热门 AI 开发工具合集
人工智能·github·copilot