想用Rust开发游戏?这份超详细的入门教程请收好!“Talkischeap,showmethecode.”学习一门新语言,最快的方式莫过于动手做一个有趣的项目。你是否曾对游戏开发充满好奇,却又被复杂的概念和庞大的游戏引擎劝退?今天,就让我们以一种轻松有趣的方式,走进Rust
“Talk is cheap, show me the code.” 学习一门新语言,最快的方式莫过于动手做一个有趣的项目。你是否曾对游戏开发充满好奇,却又被复杂的概念和庞大的游戏引擎劝退?
今天,就让我们以一种轻松有趣的方式,走进 Rust 游戏开发的世界。本文将带你使用对新手极其友好的 Rust 游戏库 bracket-lib,一步步构建一个经典的微型游戏——“Flappy Dragon”。
我们将从零开始,搭建项目、理解核心的“游戏循环(Game Loop)”、设计不同的游戏模式,并逐步添加玩家、障碍物和计分系统。无论你是 Rust 新手,还是对游戏开发感兴趣的开发者,都能通过这个项目,快速掌握游戏开发的基本脉络和 Rust 的实践应用。
准备好了吗?让我们一起敲下代码,召唤出属于自己的第一条“小龙”吧!
使用 Rust 构建微型游戏 | 轻松理解游戏开发核心
为了让游戏流畅、顺滑的运行,需要使用 Game loop
Game loop:
开始 -> 配置 App、Window 和图形 -> Poll (轮询 OS 监听输入状态 -> 调用 tick() 函数 -> 更新屏幕 -> 停止? -> 退出
Bracket-Lib 是一个 Rust 游戏编程库:
Bracket-Lib 包括很多库:
bracket-terminal 是 Bracket-Lib 中负责显示部分
Codepage 437:
~ 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
➜
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 {})
}
我们这个游戏需要 3 种模式:
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)登场了!
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())
}
小龙能在世界里飞行了,但还缺少挑战。接下来,我们将为它添加障碍物和计分系统,让游戏变得完整。
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 库,一步步实现了:
通过这个项目,我们不仅实践了 Rust 的基础语法和结构体(struct)的使用,更重要的是,我们亲手触摸到了游戏开发的脉搏。bracket-lib 为我们抽象了底层渲染的复杂性,让我们能聚焦于游戏逻辑本身,这对于学习和理解游戏开发概念至关重要。
当然,这个游戏还很简单,但它为你打开了一扇门。接下来,你可以尝试:
希望这篇教程能点燃你对 Rust 和游戏开发的兴趣。编程的乐趣,正在于创造。继续探索,继续创造吧!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!