Rust 项目实战:Flappy Bird 游戏

Rust 项目实战:Flappy Bird 游戏

  • Rust 项目实战:Flappy Bird 游戏
    • 理解 Game loop
    • 开发库:bracket-lib
      • bracket-terminal
      • Codepage 437
      • 导入 bracket-lib
    • 创建游戏
    • 游戏的模式
    • 添加玩家
    • 添加障碍
    • 最终效果
    • 项目源码

Rust 项目实战:Flappy Bird 游戏

在这里插入图片描述

参考视频:https://www.bilibili.com/video/BV1vM411J74S

理解 Game loop

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

Game loop:

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

在这里插入图片描述

开发库:bracket-lib

bracket-lib 是一个 Rust 游戏编程库,其中包含了随机数生成、几何、路径寻找、颜色处理、常用算法等库。

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

bracket-terminal

bracket-terminal 是 bracket-lib 中负责显示的部分。它提供了模拟控制台,可与多种渲染平台(OpenGL、Vulkan、Metal、Web Assembly)配合,还支持 Sprites 和原生 OpenGL 开发。

在游戏开发当中,Sprites 一般指一个实体,但是跟我们平常开发软件说的那种“实体”不一样,它是直接让玩家看到、控制甚至接触的物体,称之为“精灵”,传统意义上是指可见操作物体,当然现在也有触发精灵之类的不可见操作物体。

Codepage 437

Codepage 437 是 IBM 扩展 ASCII 字符集。

  • 来自 Dos PC 上的字符,用于终端输出,除了字母和数字,还提供了一些符号。
  • bracket-lib 会把字符翻译成图形 Sprites 并提供一个有限的字符集,字符所展示的是相应的图片。

导入 bracket-lib

项目的 Cargo.toml 中的 [dependencies] 部分插入:

bracket-lib = "~0.8.7"

创建游戏

main.rs:

use bracket_lib::prelude::*;struct State {}impl GameState for State {fn tick(&mut self, ctx: &mut BTerm) {ctx.cls();ctx.print(1, 1, "Hello, Brackets Terminal!");}
}fn main() -> BError {let context = BTermBuilder::simple80x50().with_title("Flappy Bird").build()?;main_loop(context, State {})
}

结构体 State 保存游戏的状态。为 State 实现来自 bracket_lib 的 GameState trait。

需要实现一个 tick 方法,ctx 是上下文,指的是游戏窗口。tick 方法内,先使用 cls() 方法清理屏幕,再使用 print 方法,在屏幕的 (1, 1) 坐标上打印文本 Hello, Brackets Terminal!。

游戏运行有可能出错,bracket_lib 提供了一个叫做 BError 的 Rusult 用来表示错误。

在 main 函数中,我们使用构建者模式创建一个 80 * 50 的窗口,标题为 Flappy Bird。因为构建可能出错,在 build() 函数后使用 ? 进行处理。

main_loop 就是游戏的主循环,需要传入 context 和状态 State。因为 main_loop 是需要返回的,所以后面没有分号。

运行程序:

在这里插入图片描述

游戏的模式

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

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

  • 菜单
  • 游戏中
  • 结束

我们建立一个枚举 GameMode 表示这 3 种模式。

enum GameMode {Menu,Playing,End,
}

我们需要把模式存储在状态中,修改 State 的定义:

struct State {mode: GameMode,
}

再为 State 实现一个关联函数 new(),游戏的初始状态是菜单。

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

修改 tick() 方法,根据状态的当前模式,执行不同的函数,这里使用 match 表达式。

    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),}}

这里的 main_menu、play、dead 函数都还没有在 State 中实现。

这里直接给出 State 中 4 个新函数的实现代码:

impl State {fn new() -> Self {State {mode: GameMode::Menu,}}fn main_menu(&mut self, ctx: &mut BTerm) {ctx.cls();ctx.print_centered(5, "Welcome to Flappy Bird");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 play(&mut self, ctx: &mut BTerm) {// TODOself.mode = GameMode::End;}fn dead(&mut self, ctx: &mut BTerm) {ctx.cls();ctx.print_centered(5, "You are dead!");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.restart(),VirtualKeyCode::Q => ctx.quitting = true,_ => {}}}}fn restart(&mut self) {self.mode = GameMode::Playing;}
}

因为 State 有了新成员变量,之前的 main_loop 中的 State {} 不行了,修改 main 函数:

	main_loop(context, State::new())

运行程序,游戏状态首先是 GameMode::Menu。在 tick() 函数中进行匹配,执行 main_menu 函数,显示菜单:

在这里插入图片描述

按下 P 开始游戏,执行 restart 函数,将游戏状态变为 GameMode::Playing,在 tick() 函数中进行匹配,执行 play 函数。

因为我们还没有实现 play 函数中应该有的内容,play 函数只是将游戏状态变为 GameMode::End。

再次在 tick() 函数中进行匹配,执行 dead 函数,显示新的界面:

在这里插入图片描述

如果在结束界面按下 P,还是显示相同的界面。

如果在结束界面按下 Q,则退出游戏,窗口被关闭。

添加玩家

创建一个结构体 Player 表示玩家:

struct Player {x: i32,y: i32,velocity: f32, // 垂直方向的加速度,向下为正
}

x、y 是玩家在窗口中的坐标。

Player 相关方法:

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.x += 1;self.y += self.velocity as i32;if self.y < 0 {self.y = 0;}}fn flap(&mut self) {self.velocity = -2.0;}
}

render 方法用于渲染。设置背景色为黑色,在 (0, y) 坐标渲染一个黄色的 @ 字符。

gravity_and_move 方法用于模拟玩家的重力,当 velocity < 2.0 时,velocity 会越来越大,每次增加 0.2。然后,玩家的坐标 (x, y) 发生改变,在水平方向上每次前进 1 格,在竖直方向上相当于 y = y + a * t。特殊的,要判断 y < 0 的情况,也就是说,玩家不能超出窗口的上边缘。

flap 方法用于模拟按下空格的情况,每次按下空格,玩家都会向上扑腾一下。实际上就是将玩家的 velocity 修改为 -2.0,负数表示向上的加速度。

我们需要在状态中添加 Player 成员变量:

struct State {player: Player,frame_time: f32, // 游戏累计时间mode: GameMode,
}

另外增加了一个 frame_time 成员变量,表示游戏运行的累计时间。

对应的,State 的 new 函数和 restart 函数都需要做修改:

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

我们默认玩家出生在 (5, 25) 坐标。

我们向程序中添加一些常量:

/// 游戏屏幕宽度
const SCREEN_WIDTH: i32 = 80;
/// 游戏屏幕高度
const SCREEN_HEIGHT: i32 = 50;
/// 游戏单位时间,每隔 75 ms 做一些事情
const FRAME_DURATION: f32 = 75.0;

接下来完善 play 函数的逻辑:

    fn play(&mut self, ctx: &mut BTerm) {// 清空屏幕,并设置屏幕的背景颜色ctx.cls_bg(NAVY);// frame_time_ms 记录了每次调用 tick() 所经过的时间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");// 如果 y 大于游戏屏幕高度,视为坠地,游戏结束if self.player.y > SCREEN_HEIGHT {self.mode = GameMode::End;}}

运行程序,进入游戏:

在这里插入图片描述

当我们按下空格时,小鸟会向上移动。否则,小鸟受重力作用加速向下移。若小鸟触碰到窗口底部,则游戏失败。

添加障碍

结构体 Obstacle 表示障碍:

struct Obstacle {x: i32,     // 横坐标gap_y: i32, // 中间的空隙坐标size: i32,  // 空隙大小
}

游戏中的障碍如下图所示:

在这里插入图片描述

Obstacle 的 new 函数:

    pub fn new(x: i32, score: i32) -> Self {let mut random = RandomNumberGenerator::new();Obstacle {x,gap_y: random.range(10, 40), // [10, 40)size: i32::max(2, 20 - score), // 积分越多,洞越窄}}

障碍中间的空隙坐标使用随机数生成。障碍空隙大小取决于玩家积分,积分越多,洞越窄。

Obstacle 的 render 函数:

    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('|'))}}

障碍的横坐标是 self.x,玩家的横坐标是 player_x,它们都是世界空间下的横坐标,可以是无限大。而屏幕空间是有限的,所以计算 self.x - player_x 得到的相对坐标才是障碍在屏幕中的横坐标。

Obstacle 的 hit_obstacle 函数:

    fn hit_obstacle(&self, player: &Player) -> bool {let half_size = self.size / 2;// 玩家的 x 坐标和障碍的坐标是否一样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)}

只有当玩家的 x 坐标和障碍物的坐标一样,且玩家在障碍的上半部分或下半部分,才视为玩家撞到了障碍物。

再向 State 中添加 2 个成员变量:

struct State {player: Player,frame_time: f32, // 游戏累计时间mode: GameMode,obstacle: Obstacle,score: i32,
}

对应的也要修改其构造函数、dead 函数和 restart 函数:

    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 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 Again");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 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;}

再修改 play 函数,添加障碍和积分的相关代码:

    fn play(&mut self, ctx: &mut BTerm) {// 清空屏幕,并设置屏幕的背景颜色ctx.cls_bg(NAVY);// frame_time_ms 记录了每次调用 tick() 所经过的时间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);}// 如果 y 大于游戏屏幕高度,视为坠地,或者撞到障碍物,则游戏结束if self.player.y > SCREEN_HEIGHT || self.obstacle.hit_obstacle(&self.player) {self.mode = GameMode::End;}}

最终效果

开始菜单:

在这里插入图片描述

游戏界面:

在这里插入图片描述

结束界面:

在这里插入图片描述

项目源码

GitHub:https://github.com/UestcXiye/Flappy-Bird

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/87332.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/87332.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Gin 中间件详解与实践

一、中间件的核心概念 定义 中间件是Web开发中非常重要的概念&#xff0c;它可以在请求到达最终处理函数之前或响应返回客户端之前执行一系列操作。Gin 框架支持自定义和使用内置的中间件&#xff0c;让你在请求到达路由处理函数前进行一系列预处理操作。 它是介于请求与响应处…

非接触式DIC测量系统:助力汽车研发与测试的创新技术应用

近年来&#xff0c;随着新能源汽车品牌的快速崛起&#xff0c;新车发布的节奏加快&#xff0c;层出不穷的新产品&#xff0c;给消费者带来了全新的使用体验。与此同时&#xff0c;变革的产品体验也让一些过往的汽车测试和评价标准变得不再适用&#xff0c;尤其是与过往燃油车型…

FOC学习笔记(7)锁相环(PLL)原理及其在电机控制中的应用

1. 锁相环(PLL)概述 锁相环&#xff08;Phase-Locked Loop, PLL&#xff09;是一种闭环控制系统&#xff0c;用于使输出信号的相位与输入参考信号的相位同步。PLL广泛应用于通信、电机控制、频率合成、时钟恢复等领域。在电机无传感器控制&#xff08;Sensorless Control&…

鸿蒙自定义相机的拍照页面

1、权限申请 "requestPermissions": [{"name": "ohos.permission.CAMERA","reason": "$string:reason_camera","usedScene": {"abilities": ["EntryAbility"]}},{"name": "oh…

greenplum7.2并行备份及恢复

1.并行备份 pg_dump -Fd --gp-syntax -U gpadmin -p 5432 -h 172.19.0.2 -d postgres -j 4 -f /opt/greenplum/data/postgres_backup_$(date %Y-%m-%d) 参数 含义 -Fd 使用 directory 格式&#xff08;支持并行&#xff09; --gp-syntax 使用 Greenplum 特定语法&#xff08;…

备赛2025年初中古诗文大会:练习历年真题,吃透知识点(0703)

初中古诗文大会的比赛内容古诗词、文言文各占比50%左右&#xff0c;从历年的比赛来看&#xff0c;中考语文的古诗文部分&#xff08;35分&#xff09;涉及到的古诗词、文言文知识点都在初中古诗文大会中考过。这些知识点掌握了&#xff0c;对于将来高中、高考也有直接的帮助。 …

BRAKER:真核微生物cds和蛋白注释

https://github.com/Gaius-Augustus/BRAKER 安装 # 第一次打开会pull这个docker docker run --user 1000:100 --rm -it teambraker/braker3:latest bash bash /opt/BRAKER/example/docker-tests/test3.sh braker.gtf&#xff1a;BRAKER 的最终基因集。 braker.codingseq&am…

基于 Three.js 与 WebGL 的商场全景 VR 导航系统源码级解析

本文面向Web前端开发者、WebGL/Three.js 爱好者、对VR/AR应用开发感兴趣的技术人员、智慧商场解决方案开发者。详细介绍如何利用 WebGL (Three.js框架) 构建高性能的商场全景VR环境&#xff0c;并实现精准的室内定位与3D路径规划导航功能。 如需获取商场全景VR导航系统解决方案…

AWS CloudFormation部署双可用区VPC网络架构 - 完整指南

一、模板概述 本CloudFormation模板用于在AWS上快速部署一个高可用的双可用区VPC网络架构,包含公有子网和私有子网。该架构是构建云原生应用的基础,特别适合生产环境使用。 二、完整模板代码 AWSTemplateFormatVersion: 2010-09-09 Description: Customizable dual-AZ VPC…

2025汽车声学升级:高透音汽车喇叭网成高端车型新标配

随着消费者对车载音质和静谧性要求的提升&#xff0c;高透音汽车喇叭网正成为高端车型的差异化配置。传统冲压金属网因声学损耗大、设计单一逐渐被淘汰&#xff0c;而新一代蚀刻工艺通过微孔结构优化&#xff0c;实现了声学性能与美学设计的双重突破。以下是技术趋势与市场前景…

决策树(Decision tree)算法详解(ID3、C4.5、CART)

文章目录 一、决策树介绍1.1 决策树的结构特征1.2 决策树的构建三步骤1.3 决策树构建例子 二、ID3决策树&#xff1a;基于信息增益的决策模型2.1 信息增益的公式与符号解析2.2 信息增益的意义2.3 ID3决策树案例演示&#xff1a;贷款申请分类2.4 ID3决策树缺陷 三、C4.5决策树&a…

python基础-网络的TCP、UDP协议操作

1.tcp基本语法 # ### TCP协议 客户端 import socket # 1.创建一个socket对象 sk socket.socket() # 2.与服务端建立连接 sk.connect( ("127.0.0.1" , 9000) ) # 3.收发数据的逻辑 """发送的数据类型是二进制字节流""" ""&q…

基于spark的航班价格分析预测及可视化

基于spark的航班价格分析预测及可视化 项目概况 [&#x1f447;&#x1f447;&#x1f447;&#x1f447;&#x1f447;&#x1f447;&#x1f447;&#x1f447;] 点这里,查看所有项目 [&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&…

每日算法刷题Day41 6.28:leetcode前缀和2道题,用时1h20min(要加快)

5. 523.连续的子数组和(中等,学习) 523. 连续的子数组和 - 力扣&#xff08;LeetCode&#xff09; 思想 1.给你一个整数数组 nums 和一个整数 k &#xff0c;如果 nums 有一个 好的子数组 返回 true &#xff0c;否则返回 false&#xff1a; 一个 好的子数组 是&#xff1a;…

拉取vue-element-admin

这个错误表明 npm 在尝试通过 SSH 克隆 GitHub 仓库时遇到了权限问题&#xff0c;根本原因是系统无法正确处理中文用户名路径下的 SSH 配置。以下是详细的解决方案&#xff1a; 解决方案 1&#xff1a;使用 HTTPS 代替 SSH&#xff08;推荐&#xff09; 修改 Git 全局配置&…

c语言的数组注意事项

在C语言中&#xff0c;int()[5]和int是两种完全不同的指针类型&#xff0c;理解它们的区别对于正确处理数组和多维数组至关重要。下面详细解释&#xff1a; 1&#xff1a;int*&#xff08;指向整型的指针&#xff09; 含义&#xff1a;指向单个int类型数据的指针典型用法&…

在 NestJS 中优雅使用 TypeORM 进行事务管理

事务管理是数据库操作中至关重要的部分&#xff0c;它能确保一系列操作要么全部成功&#xff0c;要么全部失败。本文将详细介绍在 NestJS 框架中使用 TypeORM 进行事务管理的多种方法。 为什么需要事务管理&#xff1f; 想象一下银行转账场景&#xff1a;从一个账户扣款后&am…

给任意apk内容添加水印

1 有源码给app添加水印 使用java可以适配更多的apk&#xff0c;如果使用koltin一些老的apk就会有适配问题 通过registerActivityLifecycleCallbacks拿到activity对象设置水印 在application里面registerActivityLifecycleCallbacks就行 static class MyActivityLifecycleCallb…

扩展的Fortran在高性能计算(HPC)中助力有限元分析(FEA)、流体力学(CFD)、结构力学、复合材料和增材制造仿真的详细指南【附完整示例代码实现】

Fortran 在高性能计算(HPC)中的仿真应用 本指南深入探讨 Fortran 语言如何在高性能计算(HPC)中助力有限元分析(FEA)、流体力学(CFD)、结构力学、复合材料和增材制造仿真。每部分详细介绍,分析 Fortran 的优势、应用场景和实现细节,并附带完整的 Fortran 模拟代码(含…

Java 大视界 -- Java 大数据机器学习模型在自然语言处理中的跨语言信息检索与知识融合(331)

Java 大视界 -- Java 大数据机器学习模型在自然语言处理中的跨语言信息检索与知识融合&#xff08;331&#xff09; 引言&#xff1a;正文&#xff1a;一、Java 驱动的多语言数据处理平台1.1 分布式多语言语料智能清洗系统1.2 多语言文本分布式存储与索引优化1.3 低资源语言数据…