Rust + Bevy 實現的 Flappy Bird 游戲 簡介 一個使用 bevy 引擎復刻的 Flappy Bird 經典小游戲。 通過該項目我們可以學到:bevy 的自定義組件,自定義插件,自定義資源,sprite 的旋轉,sprite 的移動,sprite sheet 動畫的定義使用,狀態 ...
Rust + Bevy 實現的 Flappy Bird 游戲
簡介
一個使用 bevy 引擎復刻的 Flappy Bird 經典小游戲。
通過該項目我們可以學到:bevy 的自定義組件,自定義插件,自定義資源,sprite 的旋轉,sprite 的移動,sprite sheet 動畫的定義使用,狀態管理,等內容…
簡單介紹一下包含的內容:
- 游戲狀態管理 Menu、InGame、Paused、GameOver。
- 小鳥碰撞檢測。
- 地面移動。
- 小鳥飛翔動畫。
- 小鳥飛行方向變化。
- 小鳥重力系統。
- 障礙物隨機生成。
通過空格向上飛行。
按 P 暫停游戲,按 R 恢復游戲。
代碼結構
·
├── assets/
│ ├──audios/
│ ├──fonts/
│ └──images/
├── src/
│ ├── build.rs
│ ├── components.rs
│ ├── constants.rs
│ ├── main.rs
│ ├── obstacle.rs
│ ├── player.rs
│ ├── resource.rs
│ └── state.rs
├── Cargo.lock
└── Cargo.toml
- assets/audios 聲音資源文件。
- assets/fonts 字體資源文件。
- assets/images 圖片資源文件。
- build.rs 構建之前執行的腳本文件。
- components.rs 游戲組件定義。
- constants.rs 負責存儲游戲中用到的常量。
- main.rs 負責游戲的邏輯、插件交互、等內容。
- obstacle.rs 障礙物生成、初始化。
- player.rs 玩家角色插件,生成、移動、鍵盤處理的實現。
- resource.rs 游戲資源定義。
- state.rs 游戲狀態管理。
build.rs
use std::{
env, fs,
path::{Path, PathBuf},
};
const COPY_DIR: &'static str = "assets";
/// A helper function for recursively copying a directory.
fn copy_dir<P, Q>(from: P, to: Q)
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let to = to.as_ref().to_path_buf();
for path in fs::read_dir(from).unwrap() {
let path = path.unwrap().path();
let to = to.clone().join(path.file_name().unwrap());
if path.is_file() {
fs::copy(&path, to).unwrap();
} else if path.is_dir() {
if !to.exists() {
fs::create_dir(&to).unwrap();
}
copy_dir(&path, to);
} else { /* Skip other content */
}
}
}
fn main() {
// Request the output directory
let out = env::var("PROFILE").unwrap();
let out = PathBuf::from(format!("target/{}/{}", out, COPY_DIR));
// If it is already in the output directory, delete it and start over
if out.exists() {
fs::remove_dir_all(&out).unwrap();
}
// Create the out directory
fs::create_dir(&out).unwrap();
// Copy the directory
copy_dir(COPY_DIR, &out);
}
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 。
本文來自博客園,作者:賢雲曳賀,轉載請註明原文鏈接:https://www.cnblogs.com/SantiagoZhang/p/17349695.html