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
英文地址,請注明出處:http://en.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 低資源語言數據…