游戏理解入门:Rust+Bracket开发一个小游戏

1. Game loop

使用game loop可以使得游戏运行更加流畅和顺滑,它可以:

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

大致的原理流程如下:


2. 游戏引擎/库

这里选择使用一款名为bracket-Lib的游戏编程库,这是基于rust

  • 抽象了游戏开发中很多复杂的东西,但是保留了相关的概念,可以作为简单的教学工具。
  • 包括了随机数生成、几何、寻路、颜色处理、常用算法等。

2.1 Bracket-terminal

这个终端主要负责Bracket-Lib中的显示部分。

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

2.2 Codepage437

  • 这是IBM扩展的ACSLL字符集。来自Dos PC上得到字符,用于终端输出,除了字母和数字,还提供一些符号。
  • Bracket-lib会把字符翻译为图形sprites并提供一个有限的字符集,字符所展示的是相应的图片;

3. 开始编码

3.1 游戏窗口初始化

使用cargo new创建游戏项目并导入Gracket-lib依赖。下面是第一部分代码实现,创建了游戏终窗口并打印一条简单的输出:

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

// 保留帧状态

struct State {

}

// 状态怎么和哟游戏帧关联上呢?,这就用到了一个名为GaemState的trait
impl GameState for State {
    // 实现tick函数
    fn tick(&mut self, ctx: &mut BTerm) {
        // 清屏
        ctx.cls();
        // 在屏幕上打印输出,坐标系x,y从屏幕左上角开始计算(0,0)
        ctx.print(1, 1, "Hello,Bracket Terminall!");


    }
}
fn main() -> BError {
    // 创建一个80x50的简单窗口,标题为游戏名称,?表示这个build可能会出错,出错就捕获返回,否则成功
    let context = BTermBuilder::simple80x50()
    .with_title("Flappy Dragon")
    .build()?;
    
    main_loop (context,State{})
}

运行结果:


3.2 游戏模式

一般情况下,游戏都是有一些明确的游戏模式,每种模式会明确游戏在当前的tick()中应该作的任务。

这个游戏也不例外,主要涉及三种模式:

  • 菜单
  • 游戏中
  • 结束

下面先将大致的框架构建好。

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

// 保留帧状态
struct State {
    mode:GameMode,

}
// 为游戏状态实现一个叫new的关联函数
impl State {
    fn new() ->Self {
        State {
            mode:GameMode::Menu, // 设置游戏初始状态为菜单模式
        }
    }

    // 实现play方法
    fn play(&mut self,ctx:&mut BTerm) {
        //TODO
		self.mode = GameMode::End;
    }
    
  	// restart
    fn resatrt(&mut self) {
        self.mode = GameMode::Playing;
    }
    
    fn main_menu(&mut self, ctx: &mut BTerm) {
        // TODO

    }
    
    // 实现end方法
    fn dead(&mut self, ctx: &mut BTerm) {
        
    }
    
    // 实现menu方法
    
}
// 游戏模式枚举并存储到游戏状态中
enum GameMode{
    Menu,
    Playing,
    End,
}


// 状态怎么和哟游戏帧关联上呢?,这就用到了一个名为GaemState的trait
impl GameState for State {
    // 实现tick函数
    fn tick(&mut self, ctx: &mut BTerm) {
        // 根据游戏状态选择方向
        match self.mode {
            GameMode::Menu =>self.main_menu(ctx),
            GameMode::Playing => self.dead(ctx),
            GameMode::End => self.play(ctx),
        }
        
    }
}
fn main() -> BError {
    // 创建一个80x50的简单窗口,标题为游戏名称,?表示这个build可能会出错,出错就捕获返回,否则成功
    let context = BTermBuilder::simple80x50()
    .with_title("Flappy Dragon")
    .build()?;
    
    main_loop (context,State::new())
}

3.2.1 游戏菜单实现

游戏菜单的实现逻辑比较简单,主要是提供一个游戏操作的入口以供玩家进行选择操作:

  • 清理屏幕
  • 打印欢迎语
  • 开始游戏§
  • 离开游戏(Q)
rust 复制代码
fn main_menu(&self, ctx: &mut BTerm) {
    // TODO
    ctx.cls();
    // print_centered会在屏幕水平中间位置进行打印
    ctx.print_centered( 5,"欢迎来到Flappy Dragon!");
    ctx.print_centered( 8, "(P) 开始游戏");
    ctx.print_centered(9, " (Q) 离开游戏");

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

3.2.2 游戏结束的实现

这块代码和游戏菜单差不多,把提示词换一下

rust 复制代码
// 实现end方法
fn dead(&mut self, ctx: &mut BTerm) {
    ctx.cls();
    // print_centered会在屏幕水平中间位置进行打印
    ctx.print_centered( 5,"小菜鸡,你已经嘎了!");
    ctx.print_centered( 8, "(P) 不服,再战");
    ctx.print_centered(9, " (Q) 离开游戏" );

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

3.3.3 第一阶段效果

下面是该阶段全部代码,实现了游戏基本窗口以及三个基本模式的逻辑。

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

// 保留帧状态
struct State {
    mode:GameMode,

}
// 为游戏状态实现一个叫new的关联函数
impl State {
    fn new() ->Self {
        State {
            mode:GameMode::Menu, // 设置游戏初始状态为菜单模式
        }
    }

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

    // menu方法
    fn main_menu(&mut self, ctx: &mut BTerm) {
        // TODO
        ctx.cls();
        // print_centered会在屏幕水平中间位置进行打印
        ctx.print_centered( 5,"Welcome to Flappy Dragon!");
        ctx.print_centered( 8, "(P) Start play");
        ctx.print_centered(9, " (Q) Quit game");
        
        if let Some(key) =ctx.key {
            match key {
                VirtualKeyCode::P => self.resatrt(),
                VirtualKeyCode::Q => ctx.quitting = true,
                _ => {}
            }
        }
    }

    // restart
    fn resatrt(&mut self) {
        self.mode = GameMode::Playing;
    }
    
    // 实现end方法
    fn dead(&mut self, ctx: &mut BTerm) {
        ctx.cls();
        // print_centered会在屏幕水平中间位置进行打印
        ctx.print_centered( 5,"You are dead!");
        ctx.print_centered( 8, "(P) replay");
        ctx.print_centered(9,  "(Q) quit game");
        
        if let Some(key) =ctx.key {
            match key {
                VirtualKeyCode::P => self.resatrt(),
                VirtualKeyCode::Q => ctx.quitting = true,
                _ => {}
            }
        }
    }
    
    
}
// 游戏模式枚举并存储到游戏状态中
enum GameMode{
    Menu,
    Playing,
    End,
}


// 状态怎么和哟游戏帧关联上呢?,这就用到了一个名为GaemState的trait
impl GameState for State {
    // 实现tick函数
    fn tick(&mut self, ctx: &mut BTerm) {
        // 根据游戏状态选择方向
        match self.mode {
            GameMode::Menu =>self.main_menu(ctx),
            GameMode::Playing => self.dead(ctx),
            GameMode::End => self.play(ctx),
        }
        
    }
}
fn main() -> BError {
    // 创建一个80x50的简单窗口,标题为游戏名称,?表示这个build可能会出错,出错就捕获返回,否则成功
    let context = BTermBuilder::simple80x50()
    .with_title("Flappy Dragon")
    .build()?;
    
    main_loop (context,State::new())
}
  • 运行效果:

3.3 添加play

这部分主要在游戏窗口添加一个玩家角色,这里以字符@作为龙,实现玩家通过空格键控制该角色的上下移动:

  • 一定时间不按空格,角色会下落,当下落碰到屏幕时游戏失败并结束游戏;
  • 按下空格时,龙会网上移动。
rust 复制代码
use bracket_lib::prelude::*;

// 保留帧状态
struct State {
    player:Player,
    frame_time:f32,// 结果多少帧后累计的时间
    mode:GameMode,

}

const SCREEN_WIDTH:i32 = 80; // 屏幕宽度
const SCREEN_HEIGHT:i32 = 50; // 屏幕高度
const FRAME_DURATION:f32 = 75.0; //

struct Player {
    x:i32,
    y:i32,
    velocity:f32,// 纵向速度 > 0 玩家就会往下掉
}
// 游戏模式枚举并存储到游戏状态中
enum GameMode{
    Menu,
    Playing,
    End,
}
impl Player {
    fn new(x:i32,y:i32)-> Self {
        Player {
            x:0,
            y:0,
            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) {
        // 当下降速度小于2.0时让它的重力加速度每次增加0.2
        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;
    }

}

// 为游戏状态实现一个叫new的关联函数
impl State {
    fn new() ->Self {
        State {
            player:Player::new(5,25),
            frame_time:0.0,
            mode:GameMode::Menu, // 设置游戏初始状态为菜单模式
        }
    }

    // 实现play方法
    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;
        }
    }

    // menu方法
    fn main_menu(&mut self, ctx: &mut BTerm) {
        ctx.cls();
        // print_centered会在屏幕水平中间位置进行打印
        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.mode = GameMode::Playing,
                VirtualKeyCode::Q => ctx.quitting = true,
                _ => {}
            }
        }
    }

    // restart
    fn resatrt(&mut self) {
        self.player = Player::new(5,25);
        self.frame_time = 0.0;
        self.mode = GameMode::Menu;
    }
    
    // 实现end方法
    fn dead(&mut self, ctx: &mut BTerm) {
        ctx.cls();
        // print_centered会在屏幕水平中间位置进行打印
        ctx.print_centered( 5,"You are dead!");
        ctx.print_centered( 8, "(P) replay");
        ctx.print_centered(9,  "(Q) quit game");
        
        if let Some(key) =ctx.key {
            match key {
                VirtualKeyCode::P => self.resatrt(),
                VirtualKeyCode::Q => ctx.quitting = true,
                _ => {}
            }
        }
    }
    
    
}



// 状态怎么和哟游戏帧关联上呢?,这就用到了一个名为GaemState的trait
impl GameState for State {
    // 实现tick函数
    fn tick(&mut self, ctx: &mut BTerm) {
        // 根据游戏状态选择方向
        match self.mode {
            GameMode::Menu =>self.main_menu(ctx),
            GameMode::Playing => self.play(ctx),
            GameMode::End => self.dead(ctx),
        }
    }
}
fn main() -> BError {
    // 创建一个80x50的简单窗口,标题为游戏名称,?表示这个build可能会出错,出错就捕获返回,否则成功
    let context = BTermBuilder::simple80x50()
    .with_title("Flappy Dragon")
    .build()?;
    
    main_loop (context,State::new())
}

3.4 添加障碍物

rust 复制代码
use std::fmt::format;

use bracket_lib::{prelude::*, random};

// 保留帧状态
struct State {
    player:Player,
    frame_time:f32,// 结果多少帧后累计的时间
    mode:GameMode,
    obstacle:Obstacle,
    score:i32,

}

const SCREEN_WIDTH:i32 = 80; // 屏幕宽度
const SCREEN_HEIGHT:i32 = 50; // 屏幕高度
const FRAME_DURATION:f32 = 75.0; //

struct Player {
    x:i32,
    y:i32,
    velocity:f32,// 纵向速度 > 0 玩家就会往下掉
}
// 游戏模式枚举并存储到游戏状态中
enum GameMode{
    Menu,
    Playing,
    End,
}
impl Player {
    fn new(x:i32,y:i32)-> Self {
        Player {
            x:0,
            y:0,
            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) {
        // 当下降速度小于2.0时让它的重力加速度每次增加0.2
        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;
    }

}

// 为游戏状态实现一个叫new的关联函数
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,
        }
    }

    // 实现play方法
    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;
        }

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

    // menu方法
    fn main_menu(&mut self, ctx: &mut BTerm) {
        ctx.cls();
        // print_centered会在屏幕水平中间位置进行打印
        ctx.print_centered( 5,"Welcome to Flappy Dragon!");
        ctx.print_color_right(60, 7, WEB_GREEN, BLACK,"by:Gemini48");
        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.mode = GameMode::Playing,
                VirtualKeyCode::Q => ctx.quitting = true,
                _ => {}
            }
        }
    }

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

    }
    
    // 实现end方法
    fn dead(&mut self, ctx: &mut BTerm) {
        ctx.cls();
        // print_centered会在屏幕水平中间位置进行打印
        ctx.print_centered( 5,"You are dead!");
        ctx.print_centered(6,&format!("You earned {} points",self.score));
        ctx.print_centered( 8, "(P) Play Again");
        ctx.print_centered(9,  "(Q) Quit Game");
        
        if let Some(key) =ctx.key {
            match key {
                VirtualKeyCode::P => self.resatrt(),
                VirtualKeyCode::Q => ctx.quitting = true,
                _ => {}
            }
        }
    }
    
    
}



// 状态怎么和哟游戏帧关联上呢?,这就用到了一个名为GaemState的trait
impl GameState for State {
    // 实现tick函数
    fn tick(&mut self, ctx: &mut BTerm) {
        // 根据游戏状态选择方向
        match self.mode {
            GameMode::Menu =>self.main_menu(ctx),
            GameMode::Playing => self.play(ctx),
            GameMode::End => self.dead(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:i32  = 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; // 玩家x和障碍物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 {
    // 创建一个80x50的简单窗口,标题为游戏名称,?表示这个build可能会出错,出错就捕获返回,否则成功
    let context = BTermBuilder::simple80x50()
    .with_title("Flappy Dragon")
    .build()?;
    
    main_loop (context,State::new())
}

4. 效果截图

源码地址

参考&引用

相关推荐
a cool fish(无名)1 小时前
rust-参考与借用
java·前端·rust
叶 落4 小时前
[Rust 基础课程]猜数字游戏-获取用户输入并打印
rust·rust基础
UWA5 小时前
UWA DAY 2025 游戏开发者大会|全议程
游戏·unity·性能优化·游戏开发·uwa·unreal engine
RustFS6 小时前
RustFS 如何修改默认密码?
rust
景天科技苑8 小时前
【Rust线程池】如何构建Rust线程池、Rayon线程池用法详细解析
开发语言·后端·rust·线程池·rayon·rust线程池·rayon线程池
龚子亦1 天前
【Unity开发】数据存储——XML
xml·unity·游戏引擎·数据存储·游戏开发
该用户已不存在1 天前
Zig想要取代Go和Rust,它有资格吗
前端·后端·rust
侑虎科技1 天前
Unity中利用遗传算法训练MLP
性能优化·游戏开发
用户1774125612441 天前
不懂装懂的AI,折了程序员的阳寿
rust
量子位2 天前
vivo自研蓝河操作系统内核开源!Rust开发新机遇来了
rust·ai编程