一个简单的 rust 项目 使用 bevy 引擎 复刻 Flappy Bird 小游戏 ...

锦通  金牌会员 | 2023-4-24 17:12:56 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 925|帖子 925|积分 2775

Rust + Bevy 实现的 Flappy Bird 游戏

简介

一个使用 bevy 引擎复刻的 Flappy Bird 经典小游戏。
通过该项目我们可以学到:bevy 的自定义组件,自定义插件,自定义资源,sprite 的旋转,sprite 的移动,sprite sheet 动画的定义使用,状态管理,等内容…
简单介绍一下包含的内容:

  • 游戏状态管理 Menu、InGame、Paused、GameOver。
  • 小鸟碰撞检测。
  • 地面移动。
  • 小鸟飞翔动画。
  • 小鸟飞行方向变化。
  • 小鸟重力系统。
  • 障碍物随机生成。
通过空格向上飞行。
按 P 暂停游戏,按 R 恢复游戏。
代码结构
  1. ·
  2. ├── assets/
  3. │   ├──audios/
  4. │   ├──fonts/
  5. │   └──images/
  6. ├── src/
  7. │   ├── components.rs
  8. │   ├── constants.rs
  9. │   ├── main.rs
  10. │   ├── obstacle.rs
  11. │   ├── player.rs
  12. │   ├── resource.rs
  13. │   └── state.rs
  14. ├── Cargo.lock
  15. └── Cargo.toml
复制代码

  • assets/audios 声音资源文件。
  • assets/fonts 字体资源文件。
  • assets/images 图片资源文件。
  • components.rs 游戏组件定义。
  • constants.rs 负责存储游戏中用到的常量。
  • main.rs 负责游戏的逻辑、插件交互、等内容。
  • obstacle.rs 障碍物生成、初始化。
  • player.rs 玩家角色插件,生成、移动、键盘处理的实现。
  • resource.rs 游戏资源定义。
  • state.rs 游戏状态管理。
components.rs
  1. use bevy::{
  2.     prelude::Component,
  3.     time::{Timer, TimerMode},
  4. };
  5. /// 玩家组件
  6. #[derive(Component)]
  7. pub struct Player;
  8. /// 玩家动画播放计时器
  9. #[derive(Component)]
  10. pub struct PlayerAnimationTimer(pub Timer);
  11. impl Default for PlayerAnimationTimer {
  12.     fn default() -> Self {
  13.         Self(Timer::from_seconds(0.1, TimerMode::Repeating))
  14.     }
  15. }
  16. /// 障碍物组件
  17. #[derive(Component)]
  18. pub struct Obstacle;
  19. /// 移动组件
  20. #[derive(Component)]
  21. pub struct Movable {
  22.     /// 移动时是否需要旋转
  23.     pub need_rotation: bool,
  24. }
  25. impl Default for Movable {
  26.     fn default() -> Self {
  27.         Self {
  28.             need_rotation: false,
  29.         }
  30.     }
  31. }
  32. /// 速度组件
  33. #[derive(Component)]
  34. pub struct Velocity {
  35.     pub x: f32,
  36.     pub y: f32,
  37. }
  38. impl Default for Velocity {
  39.     fn default() -> Self {
  40.         Self { x: 0., y: 0. }
  41.     }
  42. }
  43. /// 分数显示组件
  44. #[derive(Component)]
  45. pub struct DisplayScore;
  46. /// 菜单显示组件
  47. #[derive(Component)]
  48. pub struct DisplayMenu;
  49. /// 地面组件
  50. #[derive(Component)]
  51. pub struct Ground(pub f32);
  52. /// 游戏结束组件
  53. #[derive(Component)]
  54. pub struct DisplayGameOver;
复制代码
constants.rs
  1. /// 小鸟图片路径
  2. pub const BIRD_IMG_PATH: &str = "images/bird_columns.png";
  3. /// 小鸟图片大小
  4. pub const BIRD_IMG_SIZE: (f32, f32) = (34., 24.);
  5. /// 小鸟动画帧数
  6. pub const BIRD_ANIMATION_LEN: usize = 3;
  7. pub const WINDOW_WIDTH: f32 = 576.;
  8. pub const WINDOW_HEIGHT: f32 = 624.;
  9. /// 背景图片路径
  10. pub const BACKGROUND_IMG_PATH: &str = "images/background.png";
  11. /// 背景图片大小
  12. pub const BACKGROUND_IMG_SIZE: (f32, f32) = (288., 512.);
  13. /// 地面图片路径
  14. pub const GROUND_IMG_PATH: &str = "images/ground.png";
  15. /// 地面图片大小
  16. pub const GROUND_IMG_SIZE: (f32, f32) = (336., 112.);
  17. /// 一个单位地面的大小
  18. pub const GROUND_ITEM_SIZE: f32 = 48.;
  19. /// 管道图片路径
  20. pub const PIPE_IMG_PATH: &str = "images/pipe.png";
  21. /// 管道图片大小
  22. pub const PIPE_IMG_SIZE: (f32, f32) = (52., 320.);
  23. /// 飞翔声音路径
  24. pub const FLAY_AUDIO_PATH: &str = "audios/wing.ogg";
  25. /// 得分声音
  26. pub const POINT_AUDIO_PATH: &str = "audios/point.ogg";
  27. /// 死亡声音
  28. pub const DIE_AUDIO_PATH: &str = "audios/die.ogg";
  29. /// 被撞击声音
  30. pub const HIT_AUDIO_PATH: &str = "audios/hit.ogg";
  31. /// kenney future 字体路径
  32. pub const KENNEY_FUTURE_FONT_PATH: &str = "fonts/KenneyFuture.ttf";
  33. /// x 轴前进速度
  34. pub const SPAWN_OBSTACLE_TICK: f32 = 4.;
  35. /// x 轴前进速度
  36. pub const PLAYER_X_MAX_VELOCITY: f32 = 48.;
  37. /// y 轴最大上升速度
  38. pub const PLAYER_Y_MAX_UP_VELOCITY: f32 = 20.;
  39. /// y 轴每次上升像素
  40. pub const PLAYER_Y_UP_PIXEL: f32 = 10.;
  41. /// y 轴最大下落速度
  42. pub const PLAYER_Y_MAX_VELOCITY: f32 = 200.;
  43. /// y 轴下落加速度,每秒增加
  44. pub const GRAVITY_VELOCITY: f32 = 80.;
  45. /// 步长 (帧数)
  46. pub const TIME_STEP: f32 = 1. / 60.;
  47. /// 最大通过空间
  48. pub const GAP_MAX: f32 = 300.;
  49. /// 最小通过空间
  50. pub const GAP_MIN: f32 = 50.;
复制代码
main.rs
  1. use bevy::{
  2.     prelude::*,
  3.     sprite::collide_aabb::collide,
  4.     window::{Window, WindowPlugin, WindowPosition},
  5. };
  6. use obstacle::ObstaclePlugin;
  7. use components::{DisplayScore, Ground, Movable, Obstacle, Player, PlayerAnimationTimer, Velocity};
  8. use constants::*;
  9. use player::PlayerPlugin;
  10. use resource::{GameData, StaticAssets, WinSize};
  11. use state::{GameState, StatesPlugin};
  12. mod components;
  13. mod constants;
  14. mod obstacle;
  15. mod player;
  16. mod resource;
  17. mod state;
  18. fn main() {
  19.     App::new()
  20.         .add_state::<GameState>()
  21.         .insert_resource(ClearColor(Color::rgb_u8(205, 201, 201)))
  22.         .add_plugins(DefaultPlugins.set(WindowPlugin {
  23.             primary_window: Some(Window {
  24.                 title: "Flappy Bird".to_owned(),
  25.                 resolution: (WINDOW_WIDTH, WINDOW_HEIGHT).into(),
  26.                 position: WindowPosition::At(IVec2::new(2282, 0)),
  27.                 resizable: false,
  28.                 ..Default::default()
  29.             }),
  30.             ..Default::default()
  31.         }))
  32.         .add_system(system_startup.on_startup())
  33.         .add_plugin(StatesPlugin)
  34.         .add_plugin(PlayerPlugin)
  35.         .add_plugin(ObstaclePlugin)
  36.         .add_systems(
  37.             (
  38.                 score_display_update_system,
  39.                 player_animation_system,
  40.                 player_score_system,
  41.                 movable_system,
  42.                 ground_move_system,
  43.                 player_collision_check_system,
  44.             )
  45.                 .in_set(OnUpdate(GameState::InGame)),
  46.         )
  47.         .add_system(bevy::window::close_on_esc)
  48.         .run();
  49. }
  50. /// 玩家碰撞检测系统
  51. fn player_collision_check_system(
  52.     win_size: Res<WinSize>,
  53.     static_assets: Res<StaticAssets>,
  54.     audio_player: Res<Audio>,
  55.     mut next_state: ResMut<NextState<GameState>>,
  56.     obstacle_query: Query<(Entity, &Transform), With<Obstacle>>,
  57.     player_query: Query<(Entity, &Transform), With<Player>>,
  58. ) {
  59.     let player_result = player_query.get_single();
  60.     match player_result {
  61.         Ok((_, player_tf)) => {
  62.             let mut is_collision = false;
  63.             // 先进行边缘碰撞检测
  64.             if player_tf.translation.y >= win_size.height / 2.
  65.                 || player_tf.translation.y <= -(win_size.height / 2. - GROUND_IMG_SIZE.1)
  66.             {
  67.                 is_collision = true;
  68.             }
  69.             for (_, obstacle_tf) in obstacle_query.iter() {
  70.                 let collision = collide(
  71.                     player_tf.translation,
  72.                     Vec2 {
  73.                         x: BIRD_IMG_SIZE.0,
  74.                         y: BIRD_IMG_SIZE.1,
  75.                     },
  76.                     obstacle_tf.translation,
  77.                     Vec2 {
  78.                         x: PIPE_IMG_SIZE.0,
  79.                         y: PIPE_IMG_SIZE.1,
  80.                     },
  81.                 );
  82.                 if let Some(_) = collision {
  83.                     is_collision = true;
  84.                     break;
  85.                 }
  86.             }
  87.             // 判断是否已经发生碰撞
  88.             if is_collision {
  89.                 // 增加得分并播放声音
  90.                 audio_player.play(static_assets.hit_audio.clone());
  91.                 audio_player.play(static_assets.die_audio.clone());
  92.                 next_state.set(GameState::GameOver);
  93.             }
  94.         }
  95.         _ => (),
  96.     }
  97. }
  98. /// 玩家得分检测
  99. fn player_score_system(
  100.     mut commands: Commands,
  101.     mut game_data: ResMut<GameData>,
  102.     static_assets: Res<StaticAssets>,
  103.     audio_player: Res<Audio>,
  104.     obstacle_query: Query<(Entity, &Transform), With<Obstacle>>,
  105.     player_query: Query<(Entity, &Transform), With<Player>>,
  106. ) {
  107.     let player_result = player_query.get_single();
  108.     match player_result {
  109.         Ok((_, player_tf)) => {
  110.             let mut need_add_score = false;
  111.             for (entity, obstacle_tf) in obstacle_query.iter() {
  112.                 // 鸟的 尾巴通过管道的右边缘
  113.                 if player_tf.translation.x - BIRD_IMG_SIZE.0 / 2.
  114.                     > obstacle_tf.translation.x + PIPE_IMG_SIZE.0 / 2.
  115.                 {
  116.                     // 通过的话,将需要得分记为 true 并销毁管道
  117.                     need_add_score = true;
  118.                     commands.entity(entity).despawn();
  119.                 }
  120.             }
  121.             // 判断是否需要增加得分
  122.             if need_add_score {
  123.                 // 增加得分并播放声音
  124.                 game_data.add_score();
  125.                 audio_player.play(static_assets.point_audio.clone());
  126.                 game_data.call_obstacle_spawn();
  127.             }
  128.         }
  129.         _ => (),
  130.     }
  131. }
  132. /// 移动系统
  133. ///
  134. /// * 不考虑正负值,只做加法,需要具体的实体通过移动的方向自行考虑正负值
  135. fn movable_system(
  136.     mut query: Query<(&mut Transform, &Velocity, &Movable), (With<Movable>, With<Velocity>)>,
  137. ) {
  138.     for (mut transform, velocity, movable) in query.iter_mut() {
  139.         let x = velocity.x * TIME_STEP;
  140.         let y = velocity.y * TIME_STEP;
  141.         transform.translation.x += x;
  142.         transform.translation.y += y;
  143.         // 判断是否需要旋转
  144.         if movable.need_rotation {
  145.             if velocity.y > 0. {
  146.                 transform.rotation = Quat::from_rotation_z(velocity.y / PLAYER_Y_MAX_UP_VELOCITY);
  147.             } else {
  148.                 transform.rotation = Quat::from_rotation_z(velocity.y / PLAYER_Y_MAX_VELOCITY);
  149.             };
  150.         }
  151.     }
  152. }
  153. /// 地面移动组件
  154. fn ground_move_system(mut query: Query<(&mut Transform, &mut Ground)>) {
  155.     let result = query.get_single_mut();
  156.     match result {
  157.         Ok((mut transform, mut ground)) => {
  158.             ground.0 += 1.;
  159.             transform.translation.x = -ground.0;
  160.             ground.0 = ground.0 % GROUND_ITEM_SIZE;
  161.         }
  162.         _ => (),
  163.     }
  164. }
  165. /// 角色动画系统
  166. fn player_animation_system(
  167.     time: Res<Time>,
  168.     mut query: Query<(&mut PlayerAnimationTimer, &mut TextureAtlasSprite)>,
  169. ) {
  170.     for (mut timer, mut texture_atlas_sprite) in query.iter_mut() {
  171.         timer.0.tick(time.delta());
  172.         if timer.0.just_finished() {
  173.             let next_index = (texture_atlas_sprite.index + 1) % BIRD_ANIMATION_LEN;
  174.             texture_atlas_sprite.index = next_index;
  175.         }
  176.     }
  177. }
  178. /// 分数更新系统
  179. fn score_display_update_system(
  180.     game_data: Res<GameData>,
  181.     mut query: Query<&mut Text, With<DisplayScore>>,
  182. ) {
  183.     for mut text in &mut query {
  184.         text.sections[1].value = game_data.get_score().to_string();
  185.     }
  186. }
  187. fn system_startup(
  188.     mut commands: Commands,
  189.     asset_server: Res<AssetServer>,
  190.     mut texture_atlases: ResMut<Assets<TextureAtlas>>,
  191.     windows: Query<&Window>,
  192. ) {
  193.     commands.spawn(Camera2dBundle::default());
  194.     let game_data = GameData::new();
  195.     commands.insert_resource(game_data);
  196.     let window = windows.single();
  197.     let (window_w, window_h) = (window.width(), window.height());
  198.     let win_size = WinSize {
  199.         width: window_w,
  200.         height: window_h,
  201.     };
  202.     commands.insert_resource(win_size);
  203.     let player_handle = asset_server.load(BIRD_IMG_PATH);
  204.     // 将 player_handle 加载的图片,用 BIRD_IMG_SIZE 的大小,按照 1 列,3 行,切图。
  205.     let texture_atlas =
  206.         TextureAtlas::from_grid(player_handle, Vec2::from(BIRD_IMG_SIZE), 1, 3, None, None);
  207.     let player = texture_atlases.add(texture_atlas);
  208.     let background = asset_server.load(BACKGROUND_IMG_PATH);
  209.     let pipe = asset_server.load(PIPE_IMG_PATH);
  210.     let ground = asset_server.load(GROUND_IMG_PATH);
  211.     let fly_audio = asset_server.load(FLAY_AUDIO_PATH);
  212.     let die_audio = asset_server.load(DIE_AUDIO_PATH);
  213.     let point_audio = asset_server.load(POINT_AUDIO_PATH);
  214.     let hit_audio = asset_server.load(HIT_AUDIO_PATH);
  215.     let kenney_future_font = asset_server.load(KENNEY_FUTURE_FONT_PATH);
  216.     let static_assets = StaticAssets {
  217.         player,
  218.         background,
  219.         pipe,
  220.         ground,
  221.         fly_audio,
  222.         die_audio,
  223.         point_audio,
  224.         hit_audio,
  225.         kenney_future_font,
  226.     };
  227.     commands.insert_resource(static_assets);
  228.     let (background_w, background_h) = BACKGROUND_IMG_SIZE;
  229.     let (ground_w, ground_h) = GROUND_IMG_SIZE;
  230.     commands.spawn(SpriteBundle {
  231.         texture: asset_server.load(BACKGROUND_IMG_PATH),
  232.         sprite: Sprite {
  233.             custom_size: Some(Vec2 {
  234.                 x: background_w * 2.,
  235.                 y: background_h,
  236.             }),
  237.             ..Default::default()
  238.         },
  239.         transform: Transform {
  240.             translation: Vec3 {
  241.                 x: 0.,
  242.                 y: ground_h / 2.,
  243.                 z: 1.,
  244.             },
  245.             ..Default::default()
  246.         },
  247.         ..Default::default()
  248.     });
  249.     commands.spawn((
  250.         SpriteBundle {
  251.             texture: asset_server.load(GROUND_IMG_PATH),
  252.             sprite: Sprite {
  253.                 custom_size: Some(Vec2 {
  254.                     x: ground_w * 2.,
  255.                     y: ground_h,
  256.                 }),
  257.                 ..Default::default()
  258.             },
  259.             transform: Transform {
  260.                 translation: Vec3 {
  261.                     x: 0.,
  262.                     y: window_h / 2. - background_h - ground_h / 2.,
  263.                     z: 4.,
  264.                 },
  265.                 ..Default::default()
  266.             },
  267.             ..Default::default()
  268.         },
  269.         Ground(GROUND_ITEM_SIZE),
  270.     ));
  271. }
复制代码
obstacle.rs
  1. use rand::{thread_rng, Rng};
  2. use std::time::Duration;
  3. use crate::{
  4.     components::{Movable, Obstacle, Velocity},
  5.     constants::{
  6.         BACKGROUND_IMG_SIZE, GAP_MAX, GAP_MIN, GROUND_IMG_SIZE, PIPE_IMG_SIZE,
  7.         PLAYER_X_MAX_VELOCITY, SPAWN_OBSTACLE_TICK,
  8.     },
  9.     resource::{GameData, StaticAssets, WinSize},
  10.     state::GameState,
  11. };
  12. use bevy::{
  13.     prelude::{
  14.         Commands, Entity, IntoSystemAppConfig, IntoSystemConfig, OnEnter, OnUpdate, Plugin, Query,
  15.         Res, ResMut, Transform, Vec3, With,
  16.     },
  17.     sprite::{Sprite, SpriteBundle},
  18.     time::common_conditions::on_timer,
  19. };
  20. /// 障碍物插件
  21. pub struct ObstaclePlugin;
  22. impl Plugin for ObstaclePlugin {
  23.     fn build(&self, app: &mut bevy::prelude::App) {
  24.         app.add_system(obstacle_init_system.in_schedule(OnEnter(GameState::InGame)))
  25.             .add_system(
  26.                 spawn_obstacle_system
  27.                     .run_if(on_timer(Duration::from_secs_f32(0.2)))
  28.                     .in_set(OnUpdate(GameState::InGame)),
  29.             );
  30.     }
  31. }
  32. /// 障碍物初始化
  33. fn obstacle_init_system(
  34.     mut commands: Commands,
  35.     static_assets: Res<StaticAssets>,
  36.     win_size: Res<WinSize>,
  37.     game_data: Res<GameData>,
  38.     query: Query<Entity, With<Obstacle>>,
  39. ) {
  40.     let count = query.iter().count();
  41.     if count >= 4 {
  42.         return;
  43.     }
  44.     let mut rng = thread_rng();
  45.     // 初始 x 坐标
  46.     let x = win_size.width / 2. + PIPE_IMG_SIZE.0 / 2.;
  47.     // 初始化 管道区域的中心点。因为要排除地面的高度
  48.     let center_y = (win_size.height - BACKGROUND_IMG_SIZE.1) / 2.;
  49.     // 定义合理范围
  50.     let reasonable_y_max = win_size.height / 2. - 100.;
  51.     let reasonable_y_min = -(win_size.height / 2. - 100. - GROUND_IMG_SIZE.1);
  52.     let size = SPAWN_OBSTACLE_TICK * PLAYER_X_MAX_VELOCITY;
  53.     for i in 0..2 {
  54.         let x = x - PIPE_IMG_SIZE.0 - size * i as f32;
  55.         // y轴 随机中心点
  56.         // 随机可通过区域的中心点
  57.         let point_y = rng.gen_range(reasonable_y_min..reasonable_y_max);
  58.         let half_distance = (center_y - point_y).abs() / 2.;
  59.         // 获取得分 , 并根据得分获取一个随机的可通过区域的大小
  60.         let score = game_data.get_score();
  61.         let max = GAP_MAX - score as f32 / 10.;
  62.         // 不让 max 小于最小值
  63.         // 这里也可以做些其他的判断。改变下别的数据。比如说 让管道的移动速度变快!
  64.         let max = max.max(GAP_MIN);
  65.         let min = GAP_MIN;
  66.         let gap = rng.gen_range(min.min(max)..min.max(max));
  67.         let rand_half_gap = gap * rng.gen_range(0.3..0.7);
  68.         // 通过中心点,可通过区域,以及管道的高来计算 上下两个管道各自中心点的 y 坐标
  69.         let half_pipe = PIPE_IMG_SIZE.1 / 2.;
  70.         let pipe_upper = center_y + half_distance + (rand_half_gap + half_pipe);
  71.         let pipe_down = center_y - half_distance - (gap - rand_half_gap + half_pipe);
  72.         // 下方水管
  73.         commands.spawn((
  74.             SpriteBundle {
  75.                 texture: static_assets.pipe.clone(),
  76.                 transform: Transform {
  77.                     translation: Vec3 {
  78.                         x,
  79.                         y: pipe_down,
  80.                         z: 2.,
  81.                     },
  82.                     ..Default::default()
  83.                 },
  84.                 ..Default::default()
  85.             },
  86.             Velocity {
  87.                 x: -PLAYER_X_MAX_VELOCITY,
  88.                 y: 0.,
  89.             },
  90.             Movable {
  91.                 need_rotation: false,
  92.             },
  93.             Obstacle,
  94.         ));
  95.         // 上方水管
  96.         commands.spawn((
  97.             SpriteBundle {
  98.                 texture: static_assets.pipe.clone(),
  99.                 transform: Transform {
  100.                     translation: Vec3 {
  101.                         x,
  102.                         y: pipe_upper,
  103.                         z: 2.,
  104.                     },
  105.                     ..Default::default()
  106.                 },
  107.                 sprite: Sprite {
  108.                     flip_y: true,
  109.                     ..Default::default()
  110.                 },
  111.                 ..Default::default()
  112.             },
  113.             Velocity {
  114.                 x: -PLAYER_X_MAX_VELOCITY,
  115.                 y: 0.,
  116.             },
  117.             Movable {
  118.                 need_rotation: false,
  119.             },
  120.             Obstacle,
  121.         ));
  122.     }
  123. }
  124. fn spawn_obstacle_system(
  125.     mut commands: Commands,
  126.     mut game_data: ResMut<GameData>,
  127.     static_assets: Res<StaticAssets>,
  128.     win_size: Res<WinSize>,
  129. ) {
  130.     if !game_data.need_spawn_obstacle() {
  131.         return;
  132.     }
  133.     game_data.obstacle_call_back();
  134.     let mut rng = thread_rng();
  135.     // 初始 x 坐标
  136.     let x = win_size.width / 2. + PIPE_IMG_SIZE.0 / 2.;
  137.     // 初始化 管道区域的中心点。因为要排除地面的高度
  138.     let center_y = (win_size.height - BACKGROUND_IMG_SIZE.1) / 2.;
  139.     // y轴 随机中心点
  140.     // 定义合理范围
  141.     let reasonable_y_max = win_size.height / 2. - 100.;
  142.     let reasonable_y_min = -(win_size.height / 2. - 100. - GROUND_IMG_SIZE.1);
  143.     // 随机可通过区域的中心点
  144.     let point_y = rng.gen_range(reasonable_y_min..reasonable_y_max);
  145.     let half_distance = (center_y - point_y).abs() / 2.;
  146.     // 获取得分 , 并根据得分获取一个随机的可通过区域的大小
  147.     let score = game_data.get_score();
  148.     let max = GAP_MAX - score as f32 / 10.;
  149.     // 不让 max 小于最小值
  150.     // 这里也可以做些其他的判断。改变下别的数据。比如说 让管道的移动速度变快!
  151.     let max = max.max(GAP_MIN);
  152.     let min = GAP_MIN;
  153.     let gap = rng.gen_range(min.min(max)..min.max(max));
  154.     let rand_half_gap = gap * rng.gen_range(0.3..0.7);
  155.     // 通过中心点,可通过区域,以及管道的高来计算 上下两个管道各自中心点的 y 坐标
  156.     let half_pipe = PIPE_IMG_SIZE.1 / 2.;
  157.     let pipe_upper = center_y + half_distance + (rand_half_gap + half_pipe);
  158.     let pipe_down = center_y - half_distance - (gap - rand_half_gap + half_pipe);
  159.     // 下方水管
  160.     commands.spawn((
  161.         SpriteBundle {
  162.             texture: static_assets.pipe.clone(),
  163.             transform: Transform {
  164.                 translation: Vec3 {
  165.                     x,
  166.                     y: pipe_down,
  167.                     z: 2.,
  168.                 },
  169.                 ..Default::default()
  170.             },
  171.             ..Default::default()
  172.         },
  173.         Velocity {
  174.             x: -PLAYER_X_MAX_VELOCITY,
  175.             y: 0.,
  176.         },
  177.         Movable {
  178.             need_rotation: false,
  179.         },
  180.         Obstacle,
  181.     ));
  182.     // 上方水管
  183.     commands.spawn((
  184.         SpriteBundle {
  185.             texture: static_assets.pipe.clone(),
  186.             transform: Transform {
  187.                 translation: Vec3 {
  188.                     x,
  189.                     y: pipe_upper,
  190.                     z: 2.,
  191.                 },
  192.                 ..Default::default()
  193.             },
  194.             sprite: Sprite {
  195.                 flip_y: true,
  196.                 ..Default::default()
  197.             },
  198.             ..Default::default()
  199.         },
  200.         Velocity {
  201.             x: -PLAYER_X_MAX_VELOCITY,
  202.             y: 0.,
  203.         },
  204.         Movable {
  205.             need_rotation: false,
  206.         },
  207.         Obstacle,
  208.     ));
  209. }
复制代码
player.rs
  1. use bevy::{
  2.     prelude::{
  3.         Audio, Commands, Input, IntoSystemAppConfig, IntoSystemConfigs, KeyCode, OnEnter, OnUpdate,
  4.         Plugin, Query, Res, ResMut, Transform, Vec3, With,
  5.     },
  6.     sprite::{SpriteSheetBundle, TextureAtlasSprite},
  7.     time::{Timer, TimerMode},
  8. };
  9. use crate::{
  10.     components::{Movable, Player, PlayerAnimationTimer, Velocity},
  11.     constants::{
  12.         GRAVITY_VELOCITY, PLAYER_Y_MAX_UP_VELOCITY, PLAYER_Y_MAX_VELOCITY, PLAYER_Y_UP_PIXEL,
  13.         TIME_STEP,
  14.     },
  15.     resource::{GameData, StaticAssets, WinSize},
  16.     state::GameState,
  17. };
  18. pub struct PlayerPlugin;
  19. impl Plugin for PlayerPlugin {
  20.     fn build(&self, app: &mut bevy::prelude::App) {
  21.         app.add_systems(
  22.             (input_key_system, bird_automatic_system).in_set(OnUpdate(GameState::InGame)),
  23.         )
  24.         .add_system(spawn_bird_system.in_schedule(OnEnter(GameState::InGame)));
  25.     }
  26. }
  27. /// 产生玩家
  28. fn spawn_bird_system(
  29.     mut commands: Commands,
  30.     win_size: Res<WinSize>,
  31.     static_assets: Res<StaticAssets>,
  32.     mut game_data: ResMut<GameData>,
  33. ) {
  34.     if !game_data.player_alive() {
  35.         let bird = static_assets.player.clone();
  36.         let (x, y) = (-win_size.width / 4. / 2., win_size.height / 2. / 3.);
  37.         commands.spawn((
  38.             SpriteSheetBundle {
  39.                 texture_atlas: bird,
  40.                 transform: Transform {
  41.                     translation: Vec3 { x, y, z: 2. },
  42.                     ..Default::default()
  43.                 },
  44.                 sprite: TextureAtlasSprite::new(0),
  45.                 ..Default::default()
  46.             },
  47.             Player,
  48.             Velocity { x: 0., y: 0. },
  49.             Movable {
  50.                 need_rotation: true,
  51.             },
  52.             PlayerAnimationTimer(Timer::from_seconds(0.2, TimerMode::Repeating)),
  53.         ));
  54.         game_data.alive();
  55.     }
  56. }
  57. /// 游戏中键盘事件系统
  58. fn input_key_system(
  59.     kb: Res<Input<KeyCode>>,
  60.     static_assets: Res<StaticAssets>,
  61.     audio_player: Res<Audio>,
  62.     mut query: Query<(&mut Velocity, &mut Transform), With<Player>>,
  63. ) {
  64.     if kb.just_released(KeyCode::Space) {
  65.         let vt = query.get_single_mut();
  66.         // 松开空格后,直接向上20像素,并且给一个向上的速度。
  67.         match vt {
  68.             Ok((mut velocity, mut transform)) => {
  69.                 transform.translation.y += PLAYER_Y_UP_PIXEL;
  70.                 velocity.y = PLAYER_Y_MAX_UP_VELOCITY;
  71.             }
  72.             _ => (),
  73.         }
  74.         audio_player.play(static_assets.fly_audio.clone());
  75.     }
  76. }
  77. /// 小鸟重力系统
  78. fn bird_automatic_system(mut query: Query<&mut Velocity, (With<Player>, With<Movable>)>) {
  79.     for mut velocity in query.iter_mut() {
  80.         velocity.y = velocity.y - GRAVITY_VELOCITY * TIME_STEP;
  81.         if velocity.y < -PLAYER_Y_MAX_VELOCITY {
  82.             velocity.y = -PLAYER_Y_MAX_VELOCITY;
  83.         }
  84.     }
  85. }
复制代码
resource.rs
  1. use bevy::{
  2.     prelude::{AudioSource, Handle, Image, Resource},
  3.     sprite::TextureAtlas,
  4.     text::Font,
  5. };
  6. /// 游戏数据资源
  7. #[derive(Resource)]
  8. pub struct GameData {
  9.     score: u8,
  10.     alive: bool,
  11.     need_add_obstacle: bool,
  12. }
  13. impl GameData {
  14.     pub fn new() -> Self {
  15.         Self {
  16.             score: 0,
  17.             alive: false,
  18.             need_add_obstacle: false,
  19.         }
  20.     }
  21.     pub fn need_spawn_obstacle(&self) -> bool {
  22.         self.need_add_obstacle
  23.     }
  24.     pub fn obstacle_call_back(&mut self) {
  25.         self.need_add_obstacle = false;
  26.     }
  27.     pub fn call_obstacle_spawn(&mut self) {
  28.         self.need_add_obstacle = true;
  29.     }
  30.     pub fn alive(&mut self) {
  31.         self.alive = true;
  32.     }
  33.     pub fn death(&mut self) {
  34.         self.alive = false;
  35.         self.score = 0;
  36.     }
  37.     pub fn get_score(&self) -> u8 {
  38.         self.score
  39.     }
  40.     pub fn add_score(&mut self) {
  41.         self.score += 1;
  42.     }
  43.     pub fn player_alive(&self) -> bool {
  44.         self.alive
  45.     }
  46. }
  47. /// 窗口大小资源
  48. #[derive(Resource)]
  49. pub struct WinSize {
  50.     pub width: f32,
  51.     pub height: f32,
  52. }
  53. /// 静态资源
  54. #[derive(Resource)]
  55. pub struct StaticAssets {
  56.     /* 图片 */
  57.     /// 玩家动画
  58.     pub player: Handle<TextureAtlas>,
  59.     /// 管道图片
  60.     pub pipe: Handle<Image>,
  61.     /// 背景图片
  62.     pub background: Handle<Image>,
  63.     /// 地面图片
  64.     pub ground: Handle<Image>,
  65.     /* 声音 */
  66.     /// 飞行声音
  67.     pub fly_audio: Handle<AudioSource>,
  68.     /// 死亡声音
  69.     pub die_audio: Handle<AudioSource>,
  70.     /// 得分声音
  71.     pub point_audio: Handle<AudioSource>,
  72.     /// 被撞击声音
  73.     pub hit_audio: Handle<AudioSource>,
  74.     /* 字体 */
  75.     /// 游戏字体
  76.     pub kenney_future_font: Handle<Font>,
  77. }
复制代码
state.rs
  1. use bevy::{
  2.     prelude::{
  3.         Color, Commands, Entity, Input, IntoSystemAppConfig, IntoSystemConfig, KeyCode, NextState,
  4.         OnEnter, OnExit, OnUpdate, Plugin, Query, Res, ResMut, States, Transform, Vec3, With,
  5.     },
  6.     text::{Text, Text2dBundle, TextAlignment, TextSection, TextStyle},
  7. };
  8. use crate::{
  9.     components::{DisplayGameOver, DisplayMenu, DisplayScore, Obstacle, Player},
  10.     constants::GROUND_IMG_SIZE,
  11.     resource::{GameData, StaticAssets, WinSize},
  12. };
  13. #[derive(Debug, Default, States, PartialEq, Eq, Clone, Hash)]
  14. pub enum GameState {
  15.     #[default]
  16.     Menu,
  17.     InGame,
  18.     Paused,
  19.     GameOver,
  20. }
  21. pub struct StatesPlugin;
  22. impl Plugin for StatesPlugin {
  23.     fn build(&self, app: &mut bevy::prelude::App) {
  24.         app
  25.             //菜单状态
  26.             .add_system(menu_display_system.in_schedule(OnEnter(GameState::Menu)))
  27.             .add_system(enter_game_system.in_set(OnUpdate(GameState::Menu)))
  28.             .add_system(exit_menu.in_schedule(OnExit(GameState::Menu)))
  29.             // 暂停状态
  30.             .add_system(enter_paused_system.in_schedule(OnEnter(GameState::Paused)))
  31.             .add_system(paused_input_system.in_set(OnUpdate(GameState::Paused)))
  32.             .add_system(paused_exit_system.in_schedule(OnExit(GameState::Paused)))
  33.             // 游戏中状态
  34.             .add_system(in_game_display_system.in_schedule(OnEnter(GameState::InGame)))
  35.             .add_system(in_game_input_system.in_set(OnUpdate(GameState::InGame)))
  36.             .add_system(exit_game_system.in_schedule(OnExit(GameState::InGame)))
  37.             // 游戏结束状态
  38.             .add_system(game_over_enter_system.in_schedule(OnEnter(GameState::GameOver)))
  39.             .add_system(in_game_over_system.in_set(OnUpdate(GameState::GameOver)))
  40.             .add_system(game_over_exit_system.in_schedule(OnExit(GameState::GameOver)));
  41.     }
  42. }
  43. //// 进入菜单页面
  44. fn menu_display_system(mut commands: Commands, static_assets: Res<StaticAssets>) {
  45.     let font = static_assets.kenney_future_font.clone();
  46.     let common_style = TextStyle {
  47.         font: font.clone(),
  48.         font_size: 32.,
  49.         color: Color::BLUE,
  50.         ..Default::default()
  51.     };
  52.     let special_style = TextStyle {
  53.         font: font.clone(),
  54.         font_size: 38.,
  55.         color: Color::RED,
  56.         ..Default::default()
  57.     };
  58.     let align = TextAlignment::Center;
  59.     commands.spawn((
  60.         Text2dBundle {
  61.             text: Text::from_sections(vec![
  62.                 TextSection::new("PRESS \r\n".to_owned(), common_style.clone()),
  63.                 TextSection::new(" SPACE \r\n".to_owned(), special_style.clone()),
  64.                 TextSection::new("START GAME!\r\n".to_owned(), common_style.clone()),
  65.                 TextSection::new(" P \r\n".to_owned(), special_style.clone()),
  66.                 TextSection::new("PAUSED GAME!\r\n".to_owned(), common_style.clone()),
  67.             ])
  68.             .with_alignment(align),
  69.             transform: Transform {
  70.                 translation: Vec3::new(0., 0., 4.),
  71.                 ..Default::default()
  72.             },
  73.             ..Default::default()
  74.         },
  75.         DisplayMenu,
  76.     ));
  77. }
  78. //// 进入游戏显示系统
  79. fn in_game_display_system(
  80.     mut commands: Commands,
  81.     win_size: Res<WinSize>,
  82.     static_assets: Res<StaticAssets>,
  83. ) {
  84.     let font = static_assets.kenney_future_font.clone();
  85.     let common_style = TextStyle {
  86.         font: font.clone(),
  87.         font_size: 32.,
  88.         color: Color::BLUE,
  89.         ..Default::default()
  90.     };
  91.     let special_style = TextStyle {
  92.         font: font.clone(),
  93.         font_size: 38.,
  94.         color: Color::RED,
  95.         ..Default::default()
  96.     };
  97.     let y = -(win_size.height / 2. - GROUND_IMG_SIZE.1 + special_style.font_size * 1.5);
  98.     let align = TextAlignment::Center;
  99.     commands.spawn((
  100.         Text2dBundle {
  101.             text: Text::from_sections(vec![
  102.                 TextSection::new("SCORE: ".to_owned(), common_style),
  103.                 TextSection::new("0".to_owned(), special_style),
  104.             ])
  105.             .with_alignment(align),
  106.             transform: Transform {
  107.                 translation: Vec3::new(0., y, 6.),
  108.                 ..Default::default()
  109.             },
  110.             ..Default::default()
  111.         },
  112.         DisplayScore,
  113.     ));
  114. }
  115. /// 进入游戏
  116. fn enter_game_system(kb: Res<Input<KeyCode>>, mut state: ResMut<NextState<GameState>>) {
  117.     if kb.just_released(KeyCode::Space) {
  118.         state.set(GameState::InGame)
  119.     }
  120. }
  121. /// 退出游戏
  122. fn exit_game_system(
  123.     mut commands: Commands,
  124.     query: Query<Entity, (With<Text>, With<DisplayScore>)>,
  125. ) {
  126.     for entity in query.iter() {
  127.         commands.entity(entity).despawn();
  128.     }
  129. }
  130. /// 退出菜单
  131. fn exit_menu(mut commands: Commands, query: Query<Entity, (With<Text>, With<DisplayMenu>)>) {
  132.     for entity in query.iter() {
  133.         commands.entity(entity).despawn();
  134.     }
  135. }
  136. /// 进入暂停状态下运行的系统
  137. pub fn enter_paused_system(mut commands: Commands, static_assets: Res<StaticAssets>) {
  138.     // 字体引入
  139.     let font = static_assets.kenney_future_font.clone();
  140.     let common_style = TextStyle {
  141.         font: font.clone(),
  142.         font_size: 32.,
  143.         color: Color::BLUE,
  144.         ..Default::default()
  145.     };
  146.     let special_style = TextStyle {
  147.         font: font.clone(),
  148.         font_size: 38.,
  149.         color: Color::RED,
  150.         ..Default::default()
  151.     };
  152.     let align = TextAlignment::Center;
  153.     commands.spawn((
  154.         Text2dBundle {
  155.             text: Text::from_sections(vec![
  156.                 TextSection::new("PAUSED  \r\n".to_owned(), common_style.clone()),
  157.                 TextSection::new(" R \r\n".to_owned(), special_style.clone()),
  158.                 TextSection::new("RETURN GAME!".to_owned(), common_style.clone()),
  159.             ])
  160.             .with_alignment(align),
  161.             transform: Transform {
  162.                 translation: Vec3::new(0., 0., 4.),
  163.                 ..Default::default()
  164.             },
  165.             ..Default::default()
  166.         },
  167.         DisplayMenu,
  168.     ));
  169. }
  170. /// 暂停状态状态下的键盘监听系统
  171. pub fn paused_input_system(kb: Res<Input<KeyCode>>, mut next_state: ResMut<NextState<GameState>>) {
  172.     if kb.pressed(KeyCode::R) {
  173.         next_state.set(GameState::InGame);
  174.     }
  175. }
  176. /// 退出暂停状态时执行的系统
  177. pub fn paused_exit_system(
  178.     mut commands: Commands,
  179.     query: Query<Entity, (With<Text>, With<DisplayMenu>)>,
  180. ) {
  181.     for entity in query.iter() {
  182.         commands.entity(entity).despawn();
  183.     }
  184. }
  185. /// 游戏中监听暂停
  186. pub fn in_game_input_system(kb: Res<Input<KeyCode>>, mut next_state: ResMut<NextState<GameState>>) {
  187.     if kb.pressed(KeyCode::P) {
  188.         next_state.set(GameState::Paused);
  189.     }
  190. }
  191. /// 游戏结束状态下运行的系统
  192. pub fn game_over_enter_system(
  193.     mut commands: Commands,
  194.     game_data: Res<GameData>,
  195.     static_assets: Res<StaticAssets>,
  196. ) {
  197.     // 字体引入
  198.     let font = static_assets.kenney_future_font.clone();
  199.     let common_style = TextStyle {
  200.         font: font.clone(),
  201.         font_size: 32.,
  202.         color: Color::BLUE,
  203.         ..Default::default()
  204.     };
  205.     let special_style = TextStyle {
  206.         font: font.clone(),
  207.         font_size: 38.,
  208.         color: Color::RED,
  209.         ..Default::default()
  210.     };
  211.     let align = TextAlignment::Center;
  212.     commands.spawn((
  213.         Text2dBundle {
  214.             text: Text::from_sections(vec![
  215.                 TextSection::new(
  216.                     "GAME OVER !  \r\n You got ".to_owned(),
  217.                     common_style.clone(),
  218.                 ),
  219.                 TextSection::new(game_data.get_score().to_string(), special_style.clone()),
  220.                 TextSection::new(" score. \r\n  ".to_owned(), common_style.clone()),
  221.                 TextSection::new("SPACE ".to_owned(), special_style.clone()),
  222.                 TextSection::new("RESTART GAME! \r\n".to_owned(), common_style.clone()),
  223.                 TextSection::new("M ".to_owned(), special_style.clone()),
  224.                 TextSection::new("TO MENU".to_owned(), common_style.clone()),
  225.             ])
  226.             .with_alignment(align),
  227.             transform: Transform {
  228.                 translation: Vec3::new(0., 80., 4.),
  229.                 ..Default::default()
  230.             },
  231.             ..Default::default()
  232.         },
  233.         DisplayGameOver,
  234.     ));
  235. }
  236. /// 退出游戏状态时执行的系统
  237. pub fn game_over_exit_system(
  238.     mut commands: Commands,
  239.     query: Query<Entity, (With<Text>, With<DisplayGameOver>)>,
  240.     obstacle_query: Query<Entity, With<Obstacle>>,
  241.     player_query: Query<Entity, With<Player>>,
  242. ) {
  243.     for entity in query.iter() {
  244.         commands.entity(entity).despawn();
  245.     }
  246.     for entity in obstacle_query.iter() {
  247.         commands.entity(entity).despawn();
  248.     }
  249.     for entity in player_query.iter() {
  250.         commands.entity(entity).despawn();
  251.     }
  252. }
  253. /// 退出游戏状态监听
  254. pub fn in_game_over_system(
  255.     kb: Res<Input<KeyCode>>,
  256.     mut game_data: ResMut<GameData>,
  257.     mut next_state: ResMut<NextState<GameState>>,
  258. ) {
  259.     game_data.death();
  260.     if kb.pressed(KeyCode::M) {
  261.         next_state.set(GameState::Menu);
  262.     } else if kb.pressed(KeyCode::Space) {
  263.         next_state.set(GameState::InGame);
  264.     }
  265. }
复制代码
Cargo.toml
  1. [package]
  2. name = "flappy_bird_bevy"
  3. version = "0.1.0"
  4. edition = "2021"
  5. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
  6. [dependencies]
  7. bevy = { version = "0.10.1" }
  8. rand = "0.8.5"
  9. [workspace]
  10. resolver = "2"
复制代码
about me

目前失业,在家学习 rust 。
我的 bilibili,我的 Github
Rust官网
Rust 中文社区

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

锦通

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表