Rust + Bevy 实现的 Flappy Bird 游戏
简介
一个使用 bevy 引擎复刻的 Flappy Bird 经典小游戏。
通过该项目我们可以学到:bevy 的自定义组件,自定义插件,自定义资源,sprite 的旋转,sprite 的移动,sprite sheet 动画的定义使用,状态管理,等内容…
简单介绍一下包含的内容:
- 游戏状态管理 Menu、InGame、Paused、GameOver。
- 小鸟碰撞检测。
- 地面移动。
- 小鸟飞翔动画。
- 小鸟飞行方向变化。
- 小鸟重力系统。
- 障碍物随机生成。
通过空格向上飞行。
按 P 暂停游戏,按 R 恢复游戏。
代码结构
- ·
- ├── assets/
- │ ├──audios/
- │ ├──fonts/
- │ └──images/
- ├── src/
- │ ├── components.rs
- │ ├── constants.rs
- │ ├── main.rs
- │ ├── obstacle.rs
- │ ├── player.rs
- │ ├── resource.rs
- │ └── state.rs
- ├── Cargo.lock
- └── Cargo.toml
复制代码
- assets/audios 声音资源文件。
- assets/fonts 字体资源文件。
- assets/images 图片资源文件。
- components.rs 游戏组件定义。
- constants.rs 负责存储游戏中用到的常量。
- main.rs 负责游戏的逻辑、插件交互、等内容。
- obstacle.rs 障碍物生成、初始化。
- player.rs 玩家角色插件,生成、移动、键盘处理的实现。
- resource.rs 游戏资源定义。
- state.rs 游戏状态管理。
components.rs
- use bevy::{
- prelude::Component,
- time::{Timer, TimerMode},
- };
- /// 玩家组件
- #[derive(Component)]
- pub struct Player;
- /// 玩家动画播放计时器
- #[derive(Component)]
- pub struct PlayerAnimationTimer(pub Timer);
- impl Default for PlayerAnimationTimer {
- fn default() -> Self {
- Self(Timer::from_seconds(0.1, TimerMode::Repeating))
- }
- }
- /// 障碍物组件
- #[derive(Component)]
- pub struct Obstacle;
- /// 移动组件
- #[derive(Component)]
- pub struct Movable {
- /// 移动时是否需要旋转
- pub need_rotation: bool,
- }
- impl Default for Movable {
- fn default() -> Self {
- Self {
- need_rotation: false,
- }
- }
- }
- /// 速度组件
- #[derive(Component)]
- pub struct Velocity {
- pub x: f32,
- pub y: f32,
- }
- impl Default for Velocity {
- fn default() -> Self {
- Self { x: 0., y: 0. }
- }
- }
- /// 分数显示组件
- #[derive(Component)]
- pub struct DisplayScore;
- /// 菜单显示组件
- #[derive(Component)]
- pub struct DisplayMenu;
- /// 地面组件
- #[derive(Component)]
- pub struct Ground(pub f32);
- /// 游戏结束组件
- #[derive(Component)]
- pub struct DisplayGameOver;
复制代码 constants.rs
- /// 小鸟图片路径
- pub const BIRD_IMG_PATH: &str = "images/bird_columns.png";
- /// 小鸟图片大小
- pub const BIRD_IMG_SIZE: (f32, f32) = (34., 24.);
- /// 小鸟动画帧数
- pub const BIRD_ANIMATION_LEN: usize = 3;
- pub const WINDOW_WIDTH: f32 = 576.;
- pub const WINDOW_HEIGHT: f32 = 624.;
- /// 背景图片路径
- pub const BACKGROUND_IMG_PATH: &str = "images/background.png";
- /// 背景图片大小
- pub const BACKGROUND_IMG_SIZE: (f32, f32) = (288., 512.);
- /// 地面图片路径
- pub const GROUND_IMG_PATH: &str = "images/ground.png";
- /// 地面图片大小
- pub const GROUND_IMG_SIZE: (f32, f32) = (336., 112.);
- /// 一个单位地面的大小
- pub const GROUND_ITEM_SIZE: f32 = 48.;
- /// 管道图片路径
- pub const PIPE_IMG_PATH: &str = "images/pipe.png";
- /// 管道图片大小
- pub const PIPE_IMG_SIZE: (f32, f32) = (52., 320.);
- /// 飞翔声音路径
- pub const FLAY_AUDIO_PATH: &str = "audios/wing.ogg";
- /// 得分声音
- pub const POINT_AUDIO_PATH: &str = "audios/point.ogg";
- /// 死亡声音
- pub const DIE_AUDIO_PATH: &str = "audios/die.ogg";
- /// 被撞击声音
- pub const HIT_AUDIO_PATH: &str = "audios/hit.ogg";
- /// kenney future 字体路径
- pub const KENNEY_FUTURE_FONT_PATH: &str = "fonts/KenneyFuture.ttf";
- /// x 轴前进速度
- pub const SPAWN_OBSTACLE_TICK: f32 = 4.;
- /// x 轴前进速度
- pub const PLAYER_X_MAX_VELOCITY: f32 = 48.;
- /// y 轴最大上升速度
- pub const PLAYER_Y_MAX_UP_VELOCITY: f32 = 20.;
- /// y 轴每次上升像素
- pub const PLAYER_Y_UP_PIXEL: f32 = 10.;
- /// y 轴最大下落速度
- pub const PLAYER_Y_MAX_VELOCITY: f32 = 200.;
- /// y 轴下落加速度,每秒增加
- pub const GRAVITY_VELOCITY: f32 = 80.;
- /// 步长 (帧数)
- pub const TIME_STEP: f32 = 1. / 60.;
- /// 最大通过空间
- pub const GAP_MAX: f32 = 300.;
- /// 最小通过空间
- pub const GAP_MIN: f32 = 50.;
复制代码 main.rs
- use bevy::{
- prelude::*,
- sprite::collide_aabb::collide,
- window::{Window, WindowPlugin, WindowPosition},
- };
- use obstacle::ObstaclePlugin;
- use components::{DisplayScore, Ground, Movable, Obstacle, Player, PlayerAnimationTimer, Velocity};
- use constants::*;
- use player::PlayerPlugin;
- use resource::{GameData, StaticAssets, WinSize};
- use state::{GameState, StatesPlugin};
- mod components;
- mod constants;
- mod obstacle;
- mod player;
- mod resource;
- mod state;
- fn main() {
- App::new()
- .add_state::<GameState>()
- .insert_resource(ClearColor(Color::rgb_u8(205, 201, 201)))
- .add_plugins(DefaultPlugins.set(WindowPlugin {
- primary_window: Some(Window {
- title: "Flappy Bird".to_owned(),
- resolution: (WINDOW_WIDTH, WINDOW_HEIGHT).into(),
- position: WindowPosition::At(IVec2::new(2282, 0)),
- resizable: false,
- ..Default::default()
- }),
- ..Default::default()
- }))
- .add_system(system_startup.on_startup())
- .add_plugin(StatesPlugin)
- .add_plugin(PlayerPlugin)
- .add_plugin(ObstaclePlugin)
- .add_systems(
- (
- score_display_update_system,
- player_animation_system,
- player_score_system,
- movable_system,
- ground_move_system,
- player_collision_check_system,
- )
- .in_set(OnUpdate(GameState::InGame)),
- )
- .add_system(bevy::window::close_on_esc)
- .run();
- }
- /// 玩家碰撞检测系统
- fn player_collision_check_system(
- win_size: Res<WinSize>,
- static_assets: Res<StaticAssets>,
- audio_player: Res<Audio>,
- mut next_state: ResMut<NextState<GameState>>,
- obstacle_query: Query<(Entity, &Transform), With<Obstacle>>,
- player_query: Query<(Entity, &Transform), With<Player>>,
- ) {
- let player_result = player_query.get_single();
- match player_result {
- Ok((_, player_tf)) => {
- let mut is_collision = false;
- // 先进行边缘碰撞检测
- if player_tf.translation.y >= win_size.height / 2.
- || player_tf.translation.y <= -(win_size.height / 2. - GROUND_IMG_SIZE.1)
- {
- is_collision = true;
- }
- for (_, obstacle_tf) in obstacle_query.iter() {
- let collision = collide(
- player_tf.translation,
- Vec2 {
- x: BIRD_IMG_SIZE.0,
- y: BIRD_IMG_SIZE.1,
- },
- obstacle_tf.translation,
- Vec2 {
- x: PIPE_IMG_SIZE.0,
- y: PIPE_IMG_SIZE.1,
- },
- );
- if let Some(_) = collision {
- is_collision = true;
- break;
- }
- }
- // 判断是否已经发生碰撞
- if is_collision {
- // 增加得分并播放声音
- audio_player.play(static_assets.hit_audio.clone());
- audio_player.play(static_assets.die_audio.clone());
- next_state.set(GameState::GameOver);
- }
- }
- _ => (),
- }
- }
- /// 玩家得分检测
- fn player_score_system(
- mut commands: Commands,
- mut game_data: ResMut<GameData>,
- static_assets: Res<StaticAssets>,
- audio_player: Res<Audio>,
- obstacle_query: Query<(Entity, &Transform), With<Obstacle>>,
- player_query: Query<(Entity, &Transform), With<Player>>,
- ) {
- let player_result = player_query.get_single();
- match player_result {
- Ok((_, player_tf)) => {
- let mut need_add_score = false;
- for (entity, obstacle_tf) in obstacle_query.iter() {
- // 鸟的 尾巴通过管道的右边缘
- if player_tf.translation.x - BIRD_IMG_SIZE.0 / 2.
- > obstacle_tf.translation.x + PIPE_IMG_SIZE.0 / 2.
- {
- // 通过的话,将需要得分记为 true 并销毁管道
- need_add_score = true;
- commands.entity(entity).despawn();
- }
- }
- // 判断是否需要增加得分
- if need_add_score {
- // 增加得分并播放声音
- game_data.add_score();
- audio_player.play(static_assets.point_audio.clone());
- game_data.call_obstacle_spawn();
- }
- }
- _ => (),
- }
- }
- /// 移动系统
- ///
- /// * 不考虑正负值,只做加法,需要具体的实体通过移动的方向自行考虑正负值
- fn movable_system(
- mut query: Query<(&mut Transform, &Velocity, &Movable), (With<Movable>, With<Velocity>)>,
- ) {
- for (mut transform, velocity, movable) in query.iter_mut() {
- let x = velocity.x * TIME_STEP;
- let y = velocity.y * TIME_STEP;
- transform.translation.x += x;
- transform.translation.y += y;
- // 判断是否需要旋转
- if movable.need_rotation {
- if velocity.y > 0. {
- transform.rotation = Quat::from_rotation_z(velocity.y / PLAYER_Y_MAX_UP_VELOCITY);
- } else {
- transform.rotation = Quat::from_rotation_z(velocity.y / PLAYER_Y_MAX_VELOCITY);
- };
- }
- }
- }
- /// 地面移动组件
- fn ground_move_system(mut query: Query<(&mut Transform, &mut Ground)>) {
- let result = query.get_single_mut();
- match result {
- Ok((mut transform, mut ground)) => {
- ground.0 += 1.;
- transform.translation.x = -ground.0;
- ground.0 = ground.0 % GROUND_ITEM_SIZE;
- }
- _ => (),
- }
- }
- /// 角色动画系统
- fn player_animation_system(
- time: Res<Time>,
- mut query: Query<(&mut PlayerAnimationTimer, &mut TextureAtlasSprite)>,
- ) {
- for (mut timer, mut texture_atlas_sprite) in query.iter_mut() {
- timer.0.tick(time.delta());
- if timer.0.just_finished() {
- let next_index = (texture_atlas_sprite.index + 1) % BIRD_ANIMATION_LEN;
- texture_atlas_sprite.index = next_index;
- }
- }
- }
- /// 分数更新系统
- fn score_display_update_system(
- game_data: Res<GameData>,
- mut query: Query<&mut Text, With<DisplayScore>>,
- ) {
- for mut text in &mut query {
- text.sections[1].value = game_data.get_score().to_string();
- }
- }
- fn system_startup(
- mut commands: Commands,
- asset_server: Res<AssetServer>,
- mut texture_atlases: ResMut<Assets<TextureAtlas>>,
- windows: Query<&Window>,
- ) {
- commands.spawn(Camera2dBundle::default());
- let game_data = GameData::new();
- commands.insert_resource(game_data);
- let window = windows.single();
- let (window_w, window_h) = (window.width(), window.height());
- let win_size = WinSize {
- width: window_w,
- height: window_h,
- };
- commands.insert_resource(win_size);
- let player_handle = asset_server.load(BIRD_IMG_PATH);
- // 将 player_handle 加载的图片,用 BIRD_IMG_SIZE 的大小,按照 1 列,3 行,切图。
- let texture_atlas =
- TextureAtlas::from_grid(player_handle, Vec2::from(BIRD_IMG_SIZE), 1, 3, None, None);
- let player = texture_atlases.add(texture_atlas);
- let background = asset_server.load(BACKGROUND_IMG_PATH);
- let pipe = asset_server.load(PIPE_IMG_PATH);
- let ground = asset_server.load(GROUND_IMG_PATH);
- let fly_audio = asset_server.load(FLAY_AUDIO_PATH);
- let die_audio = asset_server.load(DIE_AUDIO_PATH);
- let point_audio = asset_server.load(POINT_AUDIO_PATH);
- let hit_audio = asset_server.load(HIT_AUDIO_PATH);
- let kenney_future_font = asset_server.load(KENNEY_FUTURE_FONT_PATH);
- let static_assets = StaticAssets {
- player,
- background,
- pipe,
- ground,
- fly_audio,
- die_audio,
- point_audio,
- hit_audio,
- kenney_future_font,
- };
- commands.insert_resource(static_assets);
- let (background_w, background_h) = BACKGROUND_IMG_SIZE;
- let (ground_w, ground_h) = GROUND_IMG_SIZE;
- commands.spawn(SpriteBundle {
- texture: asset_server.load(BACKGROUND_IMG_PATH),
- sprite: Sprite {
- custom_size: Some(Vec2 {
- x: background_w * 2.,
- y: background_h,
- }),
- ..Default::default()
- },
- transform: Transform {
- translation: Vec3 {
- x: 0.,
- y: ground_h / 2.,
- z: 1.,
- },
- ..Default::default()
- },
- ..Default::default()
- });
- commands.spawn((
- SpriteBundle {
- texture: asset_server.load(GROUND_IMG_PATH),
- sprite: Sprite {
- custom_size: Some(Vec2 {
- x: ground_w * 2.,
- y: ground_h,
- }),
- ..Default::default()
- },
- transform: Transform {
- translation: Vec3 {
- x: 0.,
- y: window_h / 2. - background_h - ground_h / 2.,
- z: 4.,
- },
- ..Default::default()
- },
- ..Default::default()
- },
- Ground(GROUND_ITEM_SIZE),
- ));
- }
复制代码 obstacle.rs
- use rand::{thread_rng, Rng};
- use std::time::Duration;
- use crate::{
- components::{Movable, Obstacle, Velocity},
- constants::{
- BACKGROUND_IMG_SIZE, GAP_MAX, GAP_MIN, GROUND_IMG_SIZE, PIPE_IMG_SIZE,
- PLAYER_X_MAX_VELOCITY, SPAWN_OBSTACLE_TICK,
- },
- resource::{GameData, StaticAssets, WinSize},
- state::GameState,
- };
- use bevy::{
- prelude::{
- Commands, Entity, IntoSystemAppConfig, IntoSystemConfig, OnEnter, OnUpdate, Plugin, Query,
- Res, ResMut, Transform, Vec3, With,
- },
- sprite::{Sprite, SpriteBundle},
- time::common_conditions::on_timer,
- };
- /// 障碍物插件
- pub struct ObstaclePlugin;
- impl Plugin for ObstaclePlugin {
- fn build(&self, app: &mut bevy::prelude::App) {
- app.add_system(obstacle_init_system.in_schedule(OnEnter(GameState::InGame)))
- .add_system(
- spawn_obstacle_system
- .run_if(on_timer(Duration::from_secs_f32(0.2)))
- .in_set(OnUpdate(GameState::InGame)),
- );
- }
- }
- /// 障碍物初始化
- fn obstacle_init_system(
- mut commands: Commands,
- static_assets: Res<StaticAssets>,
- win_size: Res<WinSize>,
- game_data: Res<GameData>,
- query: Query<Entity, With<Obstacle>>,
- ) {
- let count = query.iter().count();
- if count >= 4 {
- return;
- }
- let mut rng = thread_rng();
- // 初始 x 坐标
- let x = win_size.width / 2. + PIPE_IMG_SIZE.0 / 2.;
- // 初始化 管道区域的中心点。因为要排除地面的高度
- let center_y = (win_size.height - BACKGROUND_IMG_SIZE.1) / 2.;
- // 定义合理范围
- let reasonable_y_max = win_size.height / 2. - 100.;
- let reasonable_y_min = -(win_size.height / 2. - 100. - GROUND_IMG_SIZE.1);
- let size = SPAWN_OBSTACLE_TICK * PLAYER_X_MAX_VELOCITY;
- for i in 0..2 {
- let x = x - PIPE_IMG_SIZE.0 - size * i as f32;
- // y轴 随机中心点
- // 随机可通过区域的中心点
- let point_y = rng.gen_range(reasonable_y_min..reasonable_y_max);
- let half_distance = (center_y - point_y).abs() / 2.;
- // 获取得分 , 并根据得分获取一个随机的可通过区域的大小
- let score = game_data.get_score();
- let max = GAP_MAX - score as f32 / 10.;
- // 不让 max 小于最小值
- // 这里也可以做些其他的判断。改变下别的数据。比如说 让管道的移动速度变快!
- let max = max.max(GAP_MIN);
- let min = GAP_MIN;
- let gap = rng.gen_range(min.min(max)..min.max(max));
- let rand_half_gap = gap * rng.gen_range(0.3..0.7);
- // 通过中心点,可通过区域,以及管道的高来计算 上下两个管道各自中心点的 y 坐标
- let half_pipe = PIPE_IMG_SIZE.1 / 2.;
- let pipe_upper = center_y + half_distance + (rand_half_gap + half_pipe);
- let pipe_down = center_y - half_distance - (gap - rand_half_gap + half_pipe);
- // 下方水管
- commands.spawn((
- SpriteBundle {
- texture: static_assets.pipe.clone(),
- transform: Transform {
- translation: Vec3 {
- x,
- y: pipe_down,
- z: 2.,
- },
- ..Default::default()
- },
- ..Default::default()
- },
- Velocity {
- x: -PLAYER_X_MAX_VELOCITY,
- y: 0.,
- },
- Movable {
- need_rotation: false,
- },
- Obstacle,
- ));
- // 上方水管
- commands.spawn((
- SpriteBundle {
- texture: static_assets.pipe.clone(),
- transform: Transform {
- translation: Vec3 {
- x,
- y: pipe_upper,
- z: 2.,
- },
- ..Default::default()
- },
- sprite: Sprite {
- flip_y: true,
- ..Default::default()
- },
- ..Default::default()
- },
- Velocity {
- x: -PLAYER_X_MAX_VELOCITY,
- y: 0.,
- },
- Movable {
- need_rotation: false,
- },
- Obstacle,
- ));
- }
- }
- fn spawn_obstacle_system(
- mut commands: Commands,
- mut game_data: ResMut<GameData>,
- static_assets: Res<StaticAssets>,
- win_size: Res<WinSize>,
- ) {
- if !game_data.need_spawn_obstacle() {
- return;
- }
- game_data.obstacle_call_back();
- let mut rng = thread_rng();
- // 初始 x 坐标
- let x = win_size.width / 2. + PIPE_IMG_SIZE.0 / 2.;
- // 初始化 管道区域的中心点。因为要排除地面的高度
- let center_y = (win_size.height - BACKGROUND_IMG_SIZE.1) / 2.;
- // y轴 随机中心点
- // 定义合理范围
- let reasonable_y_max = win_size.height / 2. - 100.;
- let reasonable_y_min = -(win_size.height / 2. - 100. - GROUND_IMG_SIZE.1);
- // 随机可通过区域的中心点
- let point_y = rng.gen_range(reasonable_y_min..reasonable_y_max);
- let half_distance = (center_y - point_y).abs() / 2.;
- // 获取得分 , 并根据得分获取一个随机的可通过区域的大小
- let score = game_data.get_score();
- let max = GAP_MAX - score as f32 / 10.;
- // 不让 max 小于最小值
- // 这里也可以做些其他的判断。改变下别的数据。比如说 让管道的移动速度变快!
- let max = max.max(GAP_MIN);
- let min = GAP_MIN;
- let gap = rng.gen_range(min.min(max)..min.max(max));
- let rand_half_gap = gap * rng.gen_range(0.3..0.7);
- // 通过中心点,可通过区域,以及管道的高来计算 上下两个管道各自中心点的 y 坐标
- let half_pipe = PIPE_IMG_SIZE.1 / 2.;
- let pipe_upper = center_y + half_distance + (rand_half_gap + half_pipe);
- let pipe_down = center_y - half_distance - (gap - rand_half_gap + half_pipe);
- // 下方水管
- commands.spawn((
- SpriteBundle {
- texture: static_assets.pipe.clone(),
- transform: Transform {
- translation: Vec3 {
- x,
- y: pipe_down,
- z: 2.,
- },
- ..Default::default()
- },
- ..Default::default()
- },
- Velocity {
- x: -PLAYER_X_MAX_VELOCITY,
- y: 0.,
- },
- Movable {
- need_rotation: false,
- },
- Obstacle,
- ));
- // 上方水管
- commands.spawn((
- SpriteBundle {
- texture: static_assets.pipe.clone(),
- transform: Transform {
- translation: Vec3 {
- x,
- y: pipe_upper,
- z: 2.,
- },
- ..Default::default()
- },
- sprite: Sprite {
- flip_y: true,
- ..Default::default()
- },
- ..Default::default()
- },
- Velocity {
- x: -PLAYER_X_MAX_VELOCITY,
- y: 0.,
- },
- Movable {
- need_rotation: false,
- },
- Obstacle,
- ));
- }
复制代码 player.rs
- use bevy::{
- prelude::{
- Audio, Commands, Input, IntoSystemAppConfig, IntoSystemConfigs, KeyCode, OnEnter, OnUpdate,
- Plugin, Query, Res, ResMut, Transform, Vec3, With,
- },
- sprite::{SpriteSheetBundle, TextureAtlasSprite},
- time::{Timer, TimerMode},
- };
- use crate::{
- components::{Movable, Player, PlayerAnimationTimer, Velocity},
- constants::{
- GRAVITY_VELOCITY, PLAYER_Y_MAX_UP_VELOCITY, PLAYER_Y_MAX_VELOCITY, PLAYER_Y_UP_PIXEL,
- TIME_STEP,
- },
- resource::{GameData, StaticAssets, WinSize},
- state::GameState,
- };
- pub struct PlayerPlugin;
- impl Plugin for PlayerPlugin {
- fn build(&self, app: &mut bevy::prelude::App) {
- app.add_systems(
- (input_key_system, bird_automatic_system).in_set(OnUpdate(GameState::InGame)),
- )
- .add_system(spawn_bird_system.in_schedule(OnEnter(GameState::InGame)));
- }
- }
- /// 产生玩家
- fn spawn_bird_system(
- mut commands: Commands,
- win_size: Res<WinSize>,
- static_assets: Res<StaticAssets>,
- mut game_data: ResMut<GameData>,
- ) {
- if !game_data.player_alive() {
- let bird = static_assets.player.clone();
- let (x, y) = (-win_size.width / 4. / 2., win_size.height / 2. / 3.);
- commands.spawn((
- SpriteSheetBundle {
- texture_atlas: bird,
- transform: Transform {
- translation: Vec3 { x, y, z: 2. },
- ..Default::default()
- },
- sprite: TextureAtlasSprite::new(0),
- ..Default::default()
- },
- Player,
- Velocity { x: 0., y: 0. },
- Movable {
- need_rotation: true,
- },
- PlayerAnimationTimer(Timer::from_seconds(0.2, TimerMode::Repeating)),
- ));
- game_data.alive();
- }
- }
- /// 游戏中键盘事件系统
- fn input_key_system(
- kb: Res<Input<KeyCode>>,
- static_assets: Res<StaticAssets>,
- audio_player: Res<Audio>,
- mut query: Query<(&mut Velocity, &mut Transform), With<Player>>,
- ) {
- if kb.just_released(KeyCode::Space) {
- let vt = query.get_single_mut();
- // 松开空格后,直接向上20像素,并且给一个向上的速度。
- match vt {
- Ok((mut velocity, mut transform)) => {
- transform.translation.y += PLAYER_Y_UP_PIXEL;
- velocity.y = PLAYER_Y_MAX_UP_VELOCITY;
- }
- _ => (),
- }
- audio_player.play(static_assets.fly_audio.clone());
- }
- }
- /// 小鸟重力系统
- fn bird_automatic_system(mut query: Query<&mut Velocity, (With<Player>, With<Movable>)>) {
- for mut velocity in query.iter_mut() {
- velocity.y = velocity.y - GRAVITY_VELOCITY * TIME_STEP;
- if velocity.y < -PLAYER_Y_MAX_VELOCITY {
- velocity.y = -PLAYER_Y_MAX_VELOCITY;
- }
- }
- }
复制代码 resource.rs
- use bevy::{
- prelude::{AudioSource, Handle, Image, Resource},
- sprite::TextureAtlas,
- text::Font,
- };
- /// 游戏数据资源
- #[derive(Resource)]
- pub struct GameData {
- score: u8,
- alive: bool,
- need_add_obstacle: bool,
- }
- impl GameData {
- pub fn new() -> Self {
- Self {
- score: 0,
- alive: false,
- need_add_obstacle: false,
- }
- }
- pub fn need_spawn_obstacle(&self) -> bool {
- self.need_add_obstacle
- }
- pub fn obstacle_call_back(&mut self) {
- self.need_add_obstacle = false;
- }
- pub fn call_obstacle_spawn(&mut self) {
- self.need_add_obstacle = true;
- }
- pub fn alive(&mut self) {
- self.alive = true;
- }
- pub fn death(&mut self) {
- self.alive = false;
- self.score = 0;
- }
- pub fn get_score(&self) -> u8 {
- self.score
- }
- pub fn add_score(&mut self) {
- self.score += 1;
- }
- pub fn player_alive(&self) -> bool {
- self.alive
- }
- }
- /// 窗口大小资源
- #[derive(Resource)]
- pub struct WinSize {
- pub width: f32,
- pub height: f32,
- }
- /// 静态资源
- #[derive(Resource)]
- pub struct StaticAssets {
- /* 图片 */
- /// 玩家动画
- pub player: Handle<TextureAtlas>,
- /// 管道图片
- pub pipe: Handle<Image>,
- /// 背景图片
- pub background: Handle<Image>,
- /// 地面图片
- pub ground: Handle<Image>,
- /* 声音 */
- /// 飞行声音
- pub fly_audio: Handle<AudioSource>,
- /// 死亡声音
- pub die_audio: Handle<AudioSource>,
- /// 得分声音
- pub point_audio: Handle<AudioSource>,
- /// 被撞击声音
- pub hit_audio: Handle<AudioSource>,
- /* 字体 */
- /// 游戏字体
- pub kenney_future_font: Handle<Font>,
- }
复制代码 state.rs
- use bevy::{
- prelude::{
- Color, Commands, Entity, Input, IntoSystemAppConfig, IntoSystemConfig, KeyCode, NextState,
- OnEnter, OnExit, OnUpdate, Plugin, Query, Res, ResMut, States, Transform, Vec3, With,
- },
- text::{Text, Text2dBundle, TextAlignment, TextSection, TextStyle},
- };
- use crate::{
- components::{DisplayGameOver, DisplayMenu, DisplayScore, Obstacle, Player},
- constants::GROUND_IMG_SIZE,
- resource::{GameData, StaticAssets, WinSize},
- };
- #[derive(Debug, Default, States, PartialEq, Eq, Clone, Hash)]
- pub enum GameState {
- #[default]
- Menu,
- InGame,
- Paused,
- GameOver,
- }
- pub struct StatesPlugin;
- impl Plugin for StatesPlugin {
- fn build(&self, app: &mut bevy::prelude::App) {
- app
- //菜单状态
- .add_system(menu_display_system.in_schedule(OnEnter(GameState::Menu)))
- .add_system(enter_game_system.in_set(OnUpdate(GameState::Menu)))
- .add_system(exit_menu.in_schedule(OnExit(GameState::Menu)))
- // 暂停状态
- .add_system(enter_paused_system.in_schedule(OnEnter(GameState::Paused)))
- .add_system(paused_input_system.in_set(OnUpdate(GameState::Paused)))
- .add_system(paused_exit_system.in_schedule(OnExit(GameState::Paused)))
- // 游戏中状态
- .add_system(in_game_display_system.in_schedule(OnEnter(GameState::InGame)))
- .add_system(in_game_input_system.in_set(OnUpdate(GameState::InGame)))
- .add_system(exit_game_system.in_schedule(OnExit(GameState::InGame)))
- // 游戏结束状态
- .add_system(game_over_enter_system.in_schedule(OnEnter(GameState::GameOver)))
- .add_system(in_game_over_system.in_set(OnUpdate(GameState::GameOver)))
- .add_system(game_over_exit_system.in_schedule(OnExit(GameState::GameOver)));
- }
- }
- //// 进入菜单页面
- fn menu_display_system(mut commands: Commands, static_assets: Res<StaticAssets>) {
- let font = static_assets.kenney_future_font.clone();
- let common_style = TextStyle {
- font: font.clone(),
- font_size: 32.,
- color: Color::BLUE,
- ..Default::default()
- };
- let special_style = TextStyle {
- font: font.clone(),
- font_size: 38.,
- color: Color::RED,
- ..Default::default()
- };
- let align = TextAlignment::Center;
- commands.spawn((
- Text2dBundle {
- text: Text::from_sections(vec![
- TextSection::new("PRESS \r\n".to_owned(), common_style.clone()),
- TextSection::new(" SPACE \r\n".to_owned(), special_style.clone()),
- TextSection::new("START GAME!\r\n".to_owned(), common_style.clone()),
- TextSection::new(" P \r\n".to_owned(), special_style.clone()),
- TextSection::new("PAUSED GAME!\r\n".to_owned(), common_style.clone()),
- ])
- .with_alignment(align),
- transform: Transform {
- translation: Vec3::new(0., 0., 4.),
- ..Default::default()
- },
- ..Default::default()
- },
- DisplayMenu,
- ));
- }
- //// 进入游戏显示系统
- fn in_game_display_system(
- mut commands: Commands,
- win_size: Res<WinSize>,
- static_assets: Res<StaticAssets>,
- ) {
- let font = static_assets.kenney_future_font.clone();
- let common_style = TextStyle {
- font: font.clone(),
- font_size: 32.,
- color: Color::BLUE,
- ..Default::default()
- };
- let special_style = TextStyle {
- font: font.clone(),
- font_size: 38.,
- color: Color::RED,
- ..Default::default()
- };
- let y = -(win_size.height / 2. - GROUND_IMG_SIZE.1 + special_style.font_size * 1.5);
- let align = TextAlignment::Center;
- commands.spawn((
- Text2dBundle {
- text: Text::from_sections(vec![
- TextSection::new("SCORE: ".to_owned(), common_style),
- TextSection::new("0".to_owned(), special_style),
- ])
- .with_alignment(align),
- transform: Transform {
- translation: Vec3::new(0., y, 6.),
- ..Default::default()
- },
- ..Default::default()
- },
- DisplayScore,
- ));
- }
- /// 进入游戏
- fn enter_game_system(kb: Res<Input<KeyCode>>, mut state: ResMut<NextState<GameState>>) {
- if kb.just_released(KeyCode::Space) {
- state.set(GameState::InGame)
- }
- }
- /// 退出游戏
- fn exit_game_system(
- mut commands: Commands,
- query: Query<Entity, (With<Text>, With<DisplayScore>)>,
- ) {
- for entity in query.iter() {
- commands.entity(entity).despawn();
- }
- }
- /// 退出菜单
- fn exit_menu(mut commands: Commands, query: Query<Entity, (With<Text>, With<DisplayMenu>)>) {
- for entity in query.iter() {
- commands.entity(entity).despawn();
- }
- }
- /// 进入暂停状态下运行的系统
- pub fn enter_paused_system(mut commands: Commands, static_assets: Res<StaticAssets>) {
- // 字体引入
- let font = static_assets.kenney_future_font.clone();
- let common_style = TextStyle {
- font: font.clone(),
- font_size: 32.,
- color: Color::BLUE,
- ..Default::default()
- };
- let special_style = TextStyle {
- font: font.clone(),
- font_size: 38.,
- color: Color::RED,
- ..Default::default()
- };
- let align = TextAlignment::Center;
- commands.spawn((
- Text2dBundle {
- text: Text::from_sections(vec![
- TextSection::new("PAUSED \r\n".to_owned(), common_style.clone()),
- TextSection::new(" R \r\n".to_owned(), special_style.clone()),
- TextSection::new("RETURN GAME!".to_owned(), common_style.clone()),
- ])
- .with_alignment(align),
- transform: Transform {
- translation: Vec3::new(0., 0., 4.),
- ..Default::default()
- },
- ..Default::default()
- },
- DisplayMenu,
- ));
- }
- /// 暂停状态状态下的键盘监听系统
- pub fn paused_input_system(kb: Res<Input<KeyCode>>, mut next_state: ResMut<NextState<GameState>>) {
- if kb.pressed(KeyCode::R) {
- next_state.set(GameState::InGame);
- }
- }
- /// 退出暂停状态时执行的系统
- pub fn paused_exit_system(
- mut commands: Commands,
- query: Query<Entity, (With<Text>, With<DisplayMenu>)>,
- ) {
- for entity in query.iter() {
- commands.entity(entity).despawn();
- }
- }
- /// 游戏中监听暂停
- pub fn in_game_input_system(kb: Res<Input<KeyCode>>, mut next_state: ResMut<NextState<GameState>>) {
- if kb.pressed(KeyCode::P) {
- next_state.set(GameState::Paused);
- }
- }
- /// 游戏结束状态下运行的系统
- pub fn game_over_enter_system(
- mut commands: Commands,
- game_data: Res<GameData>,
- static_assets: Res<StaticAssets>,
- ) {
- // 字体引入
- let font = static_assets.kenney_future_font.clone();
- let common_style = TextStyle {
- font: font.clone(),
- font_size: 32.,
- color: Color::BLUE,
- ..Default::default()
- };
- let special_style = TextStyle {
- font: font.clone(),
- font_size: 38.,
- color: Color::RED,
- ..Default::default()
- };
- let align = TextAlignment::Center;
- commands.spawn((
- Text2dBundle {
- text: Text::from_sections(vec![
- TextSection::new(
- "GAME OVER ! \r\n You got ".to_owned(),
- common_style.clone(),
- ),
- TextSection::new(game_data.get_score().to_string(), special_style.clone()),
- TextSection::new(" score. \r\n ".to_owned(), common_style.clone()),
- TextSection::new("SPACE ".to_owned(), special_style.clone()),
- TextSection::new("RESTART GAME! \r\n".to_owned(), common_style.clone()),
- TextSection::new("M ".to_owned(), special_style.clone()),
- TextSection::new("TO MENU".to_owned(), common_style.clone()),
- ])
- .with_alignment(align),
- transform: Transform {
- translation: Vec3::new(0., 80., 4.),
- ..Default::default()
- },
- ..Default::default()
- },
- DisplayGameOver,
- ));
- }
- /// 退出游戏状态时执行的系统
- pub fn game_over_exit_system(
- mut commands: Commands,
- query: Query<Entity, (With<Text>, With<DisplayGameOver>)>,
- obstacle_query: Query<Entity, With<Obstacle>>,
- player_query: Query<Entity, With<Player>>,
- ) {
- for entity in query.iter() {
- commands.entity(entity).despawn();
- }
- for entity in obstacle_query.iter() {
- commands.entity(entity).despawn();
- }
- for entity in player_query.iter() {
- commands.entity(entity).despawn();
- }
- }
- /// 退出游戏状态监听
- pub fn in_game_over_system(
- kb: Res<Input<KeyCode>>,
- mut game_data: ResMut<GameData>,
- mut next_state: ResMut<NextState<GameState>>,
- ) {
- game_data.death();
- if kb.pressed(KeyCode::M) {
- next_state.set(GameState::Menu);
- } else if kb.pressed(KeyCode::Space) {
- next_state.set(GameState::InGame);
- }
- }
复制代码 Cargo.toml
- [package]
- name = "flappy_bird_bevy"
- version = "0.1.0"
- edition = "2021"
- # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
- [dependencies]
- bevy = { version = "0.10.1" }
- rand = "0.8.5"
- [workspace]
- resolver = "2"
复制代码 about me
目前失业,在家学习 rust 。
我的 bilibili,我的 Github。
Rust官网
Rust 中文社区
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |