一个非常简单的小项目。
看到了杨旭大佬的教学视频,自己跟着实现了一下,完善了一下游戏逻辑。
通过空格键进行控制。
游戏中可按 P 键 暂停/恢复 游戏
项目结构
| · |
| ├── Cargo.lock |
| ├── Cargo.toml |
| ├── src/ |
| │ ├── main.rs |
| │ ├──bird/ |
| │ │ ├── bird.rs |
| │ │ └── mod.rs |
| │ ├──game/ |
| │ │ ├── game.rs |
| │ │ └── mod.rs |
| │ └──obstacles/ |
| │ ├──obstacle.rs |
| │ └── mod.rs |
- game.rs 负责游戏的逻辑、控制、等内容。
- bird.rs 负责小鸟本身的实现。
- obstacle.rs 负责障碍物的实现。
三个mod.rs 文件
| |
| pub mod game; |
| |
| |
| pub mod bird; |
| |
| |
| pub mod obstacle; |
| |
main.rs
| use bracket_lib::prelude::{main_loop, BError, BTermBuilder}; |
| |
| pub mod bird; |
| pub mod game; |
| pub mod obstacles; |
| |
| fn main() -> BError { |
| |
| let ctx = BTermBuilder::simple80x50() |
| .with_title("Flappy Bird") |
| .build() |
| .unwrap(); |
| |
| |
| let game = game::game::State::new(); |
| |
| |
| main_loop(ctx, game) |
| } |
| |
game.rs
| use crate::bird::bird; |
| use crate::obstacles::obstacle; |
| use bracket_lib::{ |
| prelude::{BTerm, GameState}, |
| terminal::{VirtualKeyCode, LIGHT_BLUE}, |
| }; |
| use std::collections::LinkedList; |
| |
| |
| const GAME_WIDTH: u32 = 80; |
| |
| const GAME_HEIGHT: u32 = 50; |
| |
| const PERIOD: f32 = 120.0; |
| |
| |
| enum GameMode { |
| |
| Playing, |
| |
| End, |
| |
| Paused, |
| |
| Menu, |
| } |
| |
| |
| pub struct State { |
| |
| mode: GameMode, |
| |
| score: u32, |
| |
| bird: bird::Bird, |
| |
| waite_time: f32, |
| |
| obstacle_list: LinkedList<obstacle::OBstacle>, |
| } |
| |
| |
| impl GameState for State { |
| fn tick(&mut self, ctx: &mut BTerm) { |
| match self.mode { |
| GameMode::Playing => self.paly_control(ctx), |
| GameMode::End => self.end_control(ctx), |
| GameMode::Menu => self.menu_control(ctx), |
| GameMode::Paused => self.paused_control(ctx), |
| } |
| } |
| } |
| |
| impl State { |
| |
| pub fn new() -> Self { |
| let mut obstacle_list = LinkedList::new(); |
| obstacle_list.push_back(obstacle::OBstacle::new(0, 35, GAME_HEIGHT)); |
| obstacle_list.push_back(obstacle::OBstacle::new(0, 50, GAME_HEIGHT)); |
| obstacle_list.push_back(obstacle::OBstacle::new(0, 65, GAME_HEIGHT)); |
| obstacle_list.push_back(obstacle::OBstacle::new(0, 80, GAME_HEIGHT)); |
| Self { |
| mode: GameMode::Menu, |
| score: 0, |
| bird: bird::Bird::new(GAME_WIDTH, GAME_HEIGHT), |
| waite_time: 0.0, |
| obstacle_list, |
| } |
| } |
| |
| |
| fn reset(&mut self) { |
| self.mode = GameMode::Playing; |
| self.waite_time = 0.0; |
| self.bird = bird::Bird::new(GAME_WIDTH, GAME_HEIGHT); |
| self.score = 0; |
| let mut obstacle_list = LinkedList::new(); |
| obstacle_list.push_back(obstacle::OBstacle::new(0, 35, GAME_HEIGHT)); |
| obstacle_list.push_back(obstacle::OBstacle::new(0, 50, GAME_HEIGHT)); |
| obstacle_list.push_back(obstacle::OBstacle::new(0, 65, GAME_HEIGHT)); |
| obstacle_list.push_back(obstacle::OBstacle::new(0, 80, GAME_HEIGHT)); |
| self.obstacle_list = obstacle_list; |
| } |
| |
| |
| fn paly_control(&mut self, ctx: &mut BTerm) { |
| ctx.cls(); |
| ctx.set_bg(GAME_WIDTH, GAME_HEIGHT, LIGHT_BLUE); |
| |
| |
| self.waite_time += ctx.frame_time_ms; |
| |
| |
| if self.waite_time >= PERIOD { |
| self.bird.gravity_and_move(); |
| self.move_obstacles(); |
| self.pass_obstacles(); |
| self.waite_time = 0.0; |
| } |
| |
| if let Some(key) = ctx.key { |
| match key { |
| VirtualKeyCode::P => self.mode = GameMode::Paused, |
| VirtualKeyCode::Space => { |
| self.bird.flap(); |
| self.waite_time = 0.0; |
| } |
| VirtualKeyCode::Q | VirtualKeyCode::Escape => ctx.quit(), |
| _ => (), |
| } |
| } |
| self.bird.draw(ctx); |
| |
| for obt in &self.obstacle_list { |
| obt.draw(GAME_HEIGHT, ctx) |
| } |
| ctx.print(0, 0, format!("Score :{}", self.score)); |
| ctx.print(0, 1, "Space to flap"); |
| ctx.print(0, 2, "(Q/Esc) Quit game)"); |
| |
| if self.bird.bird_out(GAME_HEIGHT) { |
| self.mode = GameMode::End |
| } |
| } |
| |
| |
| fn move_obstacles(&mut self) { |
| for obt in &mut self.obstacle_list { |
| obt.move_forward(1); |
| } |
| } |
| |
| |
| fn pass_obstacles(&mut self) { |
| let first_obt = self.obstacle_list.front().unwrap(); |
| if first_obt.obstacle_x() <= self.bird.bird_x() as u32 { |
| let half_sie = first_obt.obstacle_size() / 2; |
| let first_top = first_obt.obstacle_gap_y() - half_sie; |
| let first_bottom = first_obt.obstacle_gap_y() + half_sie; |
| let dragon_y = self.bird.bird_y() as u32; |
| |
| if dragon_y > first_top && dragon_y < first_bottom { |
| self.score += 1; |
| } else { |
| self.mode = GameMode::End; |
| } |
| |
| self.obstacle_list.pop_front(); |
| self.obstacle_list |
| .push_back(obstacle::OBstacle::new(self.score, 80, GAME_HEIGHT)); |
| } |
| } |
| |
| |
| fn end_control(&mut self, ctx: &mut BTerm) { |
| ctx.cls(); |
| ctx.print_centered(6, format!("You are DEAD ! with {} score. ", self.score)); |
| ctx.print_centered(8, " (R) Restart game"); |
| ctx.print_centered(9, " (Q/Esc) Exit "); |
| |
| if let Some(key) = ctx.key { |
| match key { |
| VirtualKeyCode::R => { |
| self.reset(); |
| self.mode = GameMode::Playing; |
| } |
| VirtualKeyCode::Q | VirtualKeyCode::Escape => ctx.quitting = true, |
| _ => (), |
| } |
| } |
| } |
| |
| |
| fn menu_control(&mut self, ctx: &mut BTerm) { |
| ctx.cls(); |
| ctx.print_centered(6, "Welcome to flappy dragon "); |
| ctx.print_centered(8, " (S) Start game"); |
| ctx.print_centered(9, " (P) Paused game"); |
| ctx.print_centered(10, " (Q/Esc) Exit "); |
| |
| if let Some(key) = ctx.key { |
| match key { |
| VirtualKeyCode::S => self.mode = GameMode::Playing, |
| VirtualKeyCode::Q | VirtualKeyCode::Escape => ctx.quit(), |
| _ => (), |
| } |
| } |
| } |
| |
| |
| fn paused_control(&mut self, ctx: &mut BTerm) { |
| ctx.print_centered(0, format!("Score :{}", self.score)); |
| ctx.print_centered(7, "Game Paused, (P) to return game!"); |
| self.bird.draw(ctx); |
| if let Some(key) = ctx.key { |
| match key { |
| VirtualKeyCode::P => self.mode = GameMode::Playing, |
| VirtualKeyCode::Q | VirtualKeyCode::Escape => ctx.quit(), |
| _ => (), |
| } |
| } |
| } |
| } |
bird.rs
| use bracket_lib::prelude::BTerm; |
| use bracket_lib::terminal::{to_cp437, BLACK, YELLOW_GREEN}; |
| |
| |
| pub struct Bird { |
| |
| x: f64, |
| |
| y: f64, |
| |
| velocity: f64, |
| } |
| |
| impl Bird { |
| |
| pub fn new(game_width: u32, game_height: u32) -> Self { |
| Self { |
| x: (game_width / 5) as f64, |
| y: (game_height / 2) as f64, |
| velocity: 0.1, |
| } |
| } |
| |
| |
| pub fn draw(&self, ctx: &mut BTerm) { |
| ctx.set( |
| self.x as u32, |
| self.y as u32, |
| YELLOW_GREEN, |
| BLACK, |
| to_cp437('&'), |
| ); |
| } |
| |
| |
| pub fn bird_x(&self) -> f64 { |
| self.x |
| } |
| |
| |
| pub fn bird_y(&self) -> f64 { |
| self.y |
| } |
| |
| |
| pub fn gravity_and_move(&mut self) { |
| self.velocity += 0.1; |
| self.y += self.get_velocity(); |
| } |
| |
| |
| pub fn flap(&mut self) { |
| self.y -= 2.0; |
| self.velocity = 0.1; |
| } |
| |
| |
| pub fn bird_out(&self, max_y: u32) -> bool { |
| self.y as u32 > max_y || self.y < 0.0 |
| } |
| |
| |
| fn get_velocity(&mut self) -> f64 { |
| if self.velocity > 2.0 { |
| self.velocity = 2.0 |
| } |
| self.velocity |
| } |
| } |
obstacle.rs
| use bracket_lib::{ |
| prelude::BTerm, |
| random::RandomNumberGenerator, |
| terminal::{to_cp437, BLACK, RED}, |
| }; |
| |
| |
| pub struct OBstacle { |
| |
| x: u32, |
| |
| size: u32, |
| |
| gap_y: u32, |
| } |
| |
| impl OBstacle { |
| |
| |
| |
| |
| |
| |
| |
| pub fn new(score: u32, start_x: u32, screen_height: u32) -> Self { |
| let mut random = RandomNumberGenerator::new(); |
| |
| let size; |
| if score > 60 { |
| size = 2 |
| } else { |
| size = u32::max(2, 20 - score / 3); |
| } |
| |
| let half_size = size / 2 + 1; |
| Self { |
| x: start_x, |
| size, |
| gap_y: random.range(half_size, screen_height - half_size), |
| } |
| } |
| |
| |
| |
| |
| pub fn move_forward(&mut self, distance: u32) { |
| self.x -= distance |
| } |
| |
| pub fn obstacle_x(&self) -> u32 { |
| self.x |
| } |
| |
| pub fn obstacle_size(&self) -> u32 { |
| self.size |
| } |
| |
| pub fn obstacle_gap_y(&self) -> u32 { |
| self.gap_y |
| } |
| |
| |
| pub fn draw(&self, screen_height: u32, ctx: &mut BTerm) { |
| let screen_x = self.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('#')); |
| } |
| } |
| } |
Cargo.toml
| [package] |
| name = "flappy_bird" |
| version = "0.1.0" |
| edition = "2021" |
| |
| |
| |
| [dependencies] |
| bracket-lib = "0.8.7" |
Rust官网
Rust 中文社区
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现