Rust 實現的飛機游戲 簡介 一個使用 bevy 引擎製作的飛機游戲。 原視頻教程地址,github 地址。 因為 bevy 已經升級到 0.10.1 了,所以重新做一遍。順帶手出個教程。 下麵是做的部分變動: 將激光以及玩家的移動模塊進行了拆分。 新增了背景圖片。 新增了游戲狀態管理 Welco ...
Rust 實現的飛機游戲
簡介
一個使用 bevy 引擎製作的飛機游戲。
因為 bevy 已經升級到 0.10.1 了,所以重新做一遍。順帶手出個教程。
下麵是做的部分變動:
- 將激光以及玩家的移動模塊進行了拆分。
- 新增了背景圖片。
- 新增了游戲狀態管理 Welcome/InGame/Paused。
- 新增了聲音播放模塊。
- 新增了游戲記分板。
通過左右方向鍵進行控制,使用空格發射激光。
按 P 暫停游戲,按 S 恢復游戲。
更新後的GitHub地址
代碼結構
·
├── assets/
│ ├──audios/
│ ├──fonts/
│ └──images/
├── src/
│ ├──enemy/
│ │ ├── formation.rs
│ │ └── mod.rs
│ ├── components.rs
│ ├── constants.rs
│ ├── main.rs
│ ├── player.rs
│ ├── resource.rs
│ └── state.rs
├── Cargo.lock
└── Cargo.toml
- assets/audios 聲音資源文件。
- assets/fonts 字體資源文件。
- assets/images 圖片資源文件。
- enemy/formation.rs 敵人陣型系統的實現。
- enemy/mod.rs 敵人插件,生成、移動、攻擊的實現。
- components.rs 負責游戲的邏輯、控制、等內容。
- constants.rs 負責存儲游戲中用到的常量。
- main.rs 負責游戲的邏輯、控制、等內容。
- player.rs 玩家角色插件,生成、移動、攻擊、鍵盤處理的實現。
- resource.rs 游戲資源定義。
- state.rs 游戲組件定義。
兩點間的距離公式 \(|AB|=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}\)
enemy/formation.rs
use bevy::prelude::{Component, Resource};
use rand::{thread_rng, Rng};
use crate::{WinSize, BASE_SPEED, FORMATION_MEMBER_MAX};
/// 敵人陣型
#[derive(Component, Clone)]
pub struct Formation {
/// 啟始位置
pub start: (f32, f32),
/// 半徑
pub radius: (f32, f32),
/// 原點
pub pivot: (f32, f32),
/// 速度
pub speed: f32,
/// 角度
pub angle: f32,
}
/// 陣型資源
#[derive(Resource, Default)]
pub struct FormationMaker {
/// 當前陣型
current_template: Option<Formation>,
/// 當前數量
current_members: u32,
}
impl FormationMaker {
pub fn make(&mut self, win_size: &WinSize) -> Formation {
match (
&self.current_template,
self.current_members >= FORMATION_MEMBER_MAX,
) {
// 當前陣型還有空位 直接加入
(Some(template), false) => {
self.current_members += 1;
template.clone()
}
// 當前陣型沒有空位,或還沒有陣型,需要創建新的陣型
_ => {
let mut rng = thread_rng();
// 生成 起點坐標
let w_spawn = win_size.w / 2. + 100.;
let h_spawn = win_size.h / 2. + 100.;
let x = if rng.gen_bool(0.5) { w_spawn } else { -w_spawn };
let y = rng.gen_range(-h_spawn..h_spawn);
let start = (x, y);
// 生成原點坐標
let w_spawn = win_size.w / 4.;
let h_spawn = win_size.h / 3. + 50.;
let pivot = (
rng.gen_range(-w_spawn..w_spawn),
rng.gen_range(0. ..h_spawn),
);
// 生成半徑
let radius = (rng.gen_range(80. ..150.), 100.);
// 計算初始角度
let angle = (y - pivot.1).atan2(x - pivot.0);
// 速度
let speed = BASE_SPEED;
let formation = Formation {
start,
pivot,
radius,
angle,
speed,
};
self.current_template = Some(formation.clone());
self.current_members = 1;
formation
}
}
}
}
enemy/mod.rs
use std::{f32::consts::PI, time::Duration};
use crate::{
components::{Enemy, FromEnemy, Laser, Movable, SpriteSize, Velocity},
resource::GameState,
GameTextures, MaxEnemy, WinSize, ENEMY_LASER_SIZE, ENEMY_SIZE, MAX_ENEMY, SPRITE_SCALE,
TIME_STEP,
};
use bevy::{prelude::*, time::common_conditions::on_timer};
use rand::{thread_rng, Rng};
use self::formation::{Formation, FormationMaker};
mod formation;
#[derive(Component)]
pub struct EnemyPlugin;
impl Plugin for EnemyPlugin {
fn build(&self, app: &mut App) {
// 間隔執行
app.insert_resource(FormationMaker::default())
.add_system(
enemy_spawn_system
.run_if(on_timer(Duration::from_secs_f32(0.5)))
.in_set(OnUpdate(GameState::InGame)),
)
.add_system(
enemy_fire_system
.run_if(enemy_fire_criteria)
.in_set(OnUpdate(GameState::InGame)),
)
.add_system(enemy_movement_system.in_set(OnUpdate(GameState::InGame)));
}
}
/// 敵人生成系統
fn enemy_spawn_system(
mut commands: Commands,
mut max_enemy: ResMut<MaxEnemy>,
mut formation_maker: ResMut<FormationMaker>,
game_textures: Res<GameTextures>,
win_size: Res<WinSize>,
) {
// 如果當前的敵人數量大於等於最大敵人數量,則不再產生新的敵人
if max_enemy.0 >= MAX_ENEMY {
return;
}
// 隨機生成
// let mut rng = thread_rng();
// let w_span = win_size.w / 2. - 100.;
// let h_span = win_size.h / 2. - 100.;
// let x = rng.gen_range(-w_span..w_span);
// let y = rng.gen_range(-h_span..h_span);
// 使用 陣型
let formation = formation_maker.make(&win_size);
let (x, y) = formation.start;
commands
.spawn(SpriteBundle {
texture: game_textures.enemy.clone(),
transform: Transform {
// 坐標
translation: Vec3::new(x, y, 10.),
// 縮放
scale: Vec3::new(SPRITE_SCALE, SPRITE_SCALE, 1.),
// 旋轉
rotation: Quat::IDENTITY,
},
..Default::default()
})
.insert(Enemy)
.insert(formation)
.insert(SpriteSize::from(ENEMY_SIZE));
max_enemy.0 += 1;
}
/// 敵人射擊系統
fn enemy_fire_system(
mut commands: Commands,
game_textures: Res<GameTextures>,
query: Query<&Transform, With<Enemy>>,
) {
for &enemy_tf in query.iter() {
let (x, y) = (enemy_tf.translation.x, enemy_tf.translation.y);
commands
.spawn(SpriteBundle {
texture: game_textures.enemy_laser.clone(),
transform: Transform {
translation: Vec3::new(x, y, 1.),
scale: Vec3::new(SPRITE_SCALE, SPRITE_SCALE, 1.),
rotation: Quat::from_rotation_x(PI),
},
..Default::default()
})
.insert(Laser)
.insert(SpriteSize::from(ENEMY_LASER_SIZE))
.insert(FromEnemy)
.insert(Movable { auto_despawn: true })
.insert(Velocity::new(0., -1.));
}
}
/// 是否發射攻擊
fn enemy_fire_criteria() -> bool {
if thread_rng().gen_bool(1. / 60.) {
true
} else {
false
}
}
/// 敵人移動系統
///
/// 兩點間的距離公式 $|AB|=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}$
fn enemy_movement_system(mut query: Query<(&mut Transform, &mut Formation), With<Enemy>>) {
// 當前時間
// let now = time.elapsed_seconds();
for (mut transform, mut formation) in query.iter_mut() {
// 當前坐標
let (x_org, y_org) = (transform.translation.x, transform.translation.y);
// let (x_org, y_org) = formation.start;
// 單位時間內最大移動距離
// let max_distance = BASE_SPEED * TIME_STEP;
let max_distance = formation.speed * TIME_STEP;
// 方向 1 順時針 -1 逆時針
// let dir = -1.;
let dir = if formation.start.0 < 0. { 1. } else { -1. };
// 中心點
// let (x_pivot, y_pivot) = (0., 0.);
let (x_pivot, y_pivot) = formation.pivot;
// 半徑
// let (x_radius, y_radius) = (200., 130.);
let (x_radius, y_radius) = formation.radius;
// 基於當前時間計算的角度
// let angel = dir * BASE_SPEED * TIME_STEP * now % 360. / PI;
let angel = formation.angle
+ dir * formation.speed * TIME_STEP / (x_radius.min(y_radius) * PI / 2.);
// 計算目標點位
let x_dst = x_radius * angel.cos() + x_pivot;
let y_dst = y_radius * angel.sin() + y_pivot;
// 計算距離
// 兩點間的距離公式 根號下 a.x - b.x
let dx = x_org - x_dst;
let dy = y_org - y_dst;
let distance = (dx * dx + dy * dy).sqrt();
let distance_radio = if distance != 0. {
max_distance / distance
} else {
0.
};
// 計算 x y 的最終坐標
let x = x_org - dx * distance_radio;
let x = if dx > 0. { x.max(x_dst) } else { x.min(x_dst) };
let y = y_org - dy * distance_radio;
let y = if dy > 0. { y.max(y_dst) } else { y.min(y_dst) };
// 圖片資源在橢圓上 或接近橢圓時開始加入旋轉
if distance < max_distance * formation.speed / 20. {
formation.angle = angel;
}
let translation = &mut transform.translation;
(translation.x, translation.y) = (x, y);
}
}
components.rs
use bevy::{
prelude::{Component, Vec2, Vec3},
time::{Timer, TimerMode},
};
// 通用控制組件
#[derive(Component)]
pub struct Velocity {
pub x: f32,
pub y: f32,
}
impl Velocity {
pub fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
}
/// 移動能力組件
#[derive(Component)]
pub struct Movable {
/// 自動銷毀
pub auto_despawn: bool,
}
/// 玩家組件
#[derive(Component)]
pub struct Player;
/// 玩家信息組件
#[derive(Component)]
pub struct FromPlayer;
/// 敵人組件
#[derive(Component)]
pub struct Enemy;
/// 敵人信息組件
#[derive(Component)]
pub struct FromEnemy;
/// 激光組件
#[derive(Component)]
pub struct Laser;
/// 圖片大小組件
#[derive(Component)]
pub struct SpriteSize(pub Vec2);
/// 實現 (f32,f32) 轉 SpritSize
impl From<(f32, f32)> for SpriteSize {
fn from(value: (f32, f32)) -> Self {
Self(Vec2::new(value.0, value.1))
}
}
/// 爆炸組件
#[derive(Component)]
pub struct Explosion;
/// 產生爆炸組件
#[derive(Component)]
pub struct ExplosionToSpawn(pub Vec3);
/// 爆炸事件組件
#[derive(Component)]
pub struct ExplosionTimer(pub Timer);
impl Default for ExplosionTimer {
fn default() -> Self {
Self(Timer::from_seconds(0.05, TimerMode::Once))
}
}
/// 分數顯示組件
#[derive(Component)]
pub struct DisplayScore;
/// 歡迎組件
#[derive(Component)]
pub struct WelcomeText;
/// 暫停組件
#[derive(Component)]
pub struct PausedText;
constants.rs
/// 游戲背景圖片路徑
pub const BACKGROUND_SPRITE: &str = "images/planet05.png";
/// 玩家圖片路徑
pub const PLAYER_SPRITE: &str = "images/player_a_01.png";
/// 玩家大小
pub const PLAYER_SIZE: (f32, f32) = (144., 75.);
/// 玩家攻擊圖片路徑
pub const PLAYER_LASER_SPRITE: &str = "images/laser_a_01.png";
/// 玩家攻擊圖片大小
pub const PLAYER_LASER_SIZE: (f32, f32) = (9., 54.);
/// 敵人圖片路徑
pub const ENEMY_SPRITE: &str = "images/enemy_a_01.png";
/// 敵人大小
pub const ENEMY_SIZE: (f32, f32) = (144., 75.);
/// 敵人攻擊圖片路徑
pub const ENEMY_LASER_SPRITE: &str = "images/laser_b_01.png";
/// 敵人攻擊圖片大小
pub const ENEMY_LASER_SIZE: (f32, f32) = (17., 55.);
/// 爆炸圖片路徑
pub const EXPLOSION_SHEET: &str = "images/explosion_a_sheet.png";
/// 爆炸圖片大小
pub const EXPLOSION_SIZE: (f32, f32) = (64., 64.);
/// 爆炸畫面幀數
pub const EXPLOSION_ANIMATION_LEN: usize = 16;
/// 圖片縮放比例
pub const SPRITE_SCALE: f32 = 0.5;
/// 步長 (幀數)
pub const TIME_STEP: f32 = 1. / 60.;
/// 基礎速度
pub const BASE_SPEED: f32 = 500.;
/// 敵人最大數量
pub const MAX_ENEMY: u32 = 2;
/// 玩家自動重生時間
pub const PLAYER_RESPAWN_DELAY: f64 = 2.;
/// 陣型內敵人最大數量
pub const FORMATION_MEMBER_MAX: u32 = 2;
/// 敵人被摧毀聲音
pub const ENEMY_EXPLOSION_AUDIO: &str = "audios/enemy_explosion.ogg";
/// 玩家被摧毀的聲音
pub const PLAYER_EXPLOSION_AUDIO: &str = "audios/player_explosion.ogg";
/// 玩家發射激光的聲音
pub const PLAYER_LASER_AUDIO: &str = "audios/player_laser.ogg";
/// 字體路徑
pub const KENNEY_BLOCK_FONT: &str = "fonts/kenney_blocks.ttf";
main.rs
use bevy::{math::Vec3Swizzles, prelude::*, sprite::collide_aabb::collide, utils::HashSet};
use components::*;
use constants::*;
use enemy::EnemyPlugin;
use player::PlayerPlugin;
use resource::{GameAudio, GameData, GameState, GameTextures, MaxEnemy, PlayerState, WinSize};
use state::StatePlugin;
mod components;
mod constants;
mod enemy;
mod player;
mod resource;
mod state;
fn main() {
// add_startup_system 啟動生命周期時只運行一次 ,
// add_system 每幀都會被調用方法
App::new()
.add_state::<GameState>()
.insert_resource(ClearColor(Color::rgb(0.04, 0.04, 0.04)))
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Invaders".to_owned(),
resolution: (598., 676.).into(),
position: WindowPosition::At(IVec2::new(2282, 0)),
..Window::default()
}),
..WindowPlugin::default()
}))
.add_plugin(PlayerPlugin)
.add_plugin(EnemyPlugin)
.add_plugin(StatePlugin)
.add_startup_system(setup_system)
// InGame 狀態下執行的函數
.add_systems(
(
laser_movable_system,
player_laser_hit_enemy_system,
explosion_to_spawn_system,
explosion_animation_system,
enemy_laser_hit_player_system,
score_display_update_system,
)
.in_set(OnUpdate(GameState::InGame)),
)
// 啟動 esc 鍵退出程式
.add_system(bevy::window::close_on_esc)
.run();
}
/// 資源載入
fn setup_system(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut texture_atlases: ResMut<Assets<TextureAtlas>>,
mut windows: Query<&mut Window>,
) {
// 創建2d鏡頭
commands.spawn(Camera2dBundle::default());
// 獲取當前視窗
let window = windows.single_mut();
let win_w = window.width();
let win_h = window.height();
// 添加 WinSize 資源
let win_size = WinSize { w: win_w, h: win_h };
commands.insert_resource(win_size);
// 創建爆炸動畫
let texture_handle = asset_server.load(EXPLOSION_SHEET);
let texture_atlas =
TextureAtlas::from_grid(texture_handle, Vec2::from(EXPLOSION_SIZE), 4, 4, None, None);
let explosion = texture_atlases.add(texture_atlas);
// 添加 GameTextures
let game_texture = GameTextures {
background: asset_server.load(BACKGROUND_SPRITE),
player: asset_server.load(PLAYER_SPRITE),
player_laser: asset_server.load(PLAYER_LASER_SPRITE),
enemy: asset_server.load(ENEMY_SPRITE),
enemy_laser: asset_server.load(ENEMY_LASER_SPRITE),
font: asset_server.load(KENNEY_BLOCK_FONT),
explosion,
};
// 聲音資源引入
let game_audio = GameAudio {
player_laser: asset_server.load(PLAYER_LASER_AUDIO),
player_explosion: asset_server.load(PLAYER_EXPLOSION_AUDIO),
enemy_explosion: asset_server.load(ENEMY_EXPLOSION_AUDIO),
};
// 背景圖片
commands.spawn(SpriteBundle {
texture: game_texture.background.clone(),
sprite: Sprite {
custom_size: Some(Vec2 { x: win_w, y: win_h }),
..Default::default()
},
transform: Transform::from_scale(Vec3::new(1.5, 1.5, 0.0)),
..Default::default()
});
// 字體引入
let font = game_texture.font.clone();
let text_style = TextStyle {
font: font.clone(),
font_size: 32.,
color: Color::ANTIQUE_WHITE,
};
let text_alignment = TextAlignment::Center;
// 分數展示控制項
commands.spawn((
Text2dBundle {
text: Text::from_section("SCORE:0", text_style).with_alignment(text_alignment),
transform: Transform {
translation: Vec3 {
x: 0.,
y: win_h / 2. - 20.,
z: 11.,
},
..Default::default()
},
..Default::default()
},
DisplayScore,
));
let game_data = GameData::new();
commands.insert_resource(game_data);
commands.insert_resource(game_audio);
commands.insert_resource(game_texture);
commands.insert_resource(MaxEnemy(0));
}
/// 激光移動系統
fn laser_movable_system(
mut commands: Commands,
win_size: Res<WinSize>,
mut query: Query<(Entity, &Velocity, &mut Transform, &Movable), With<Laser>>,
) {
for (entity, velocity, mut transform, movable) in query.iter_mut() {
// 移動位置
let translation = &mut transform.translation;
translation.x += velocity.x * BASE_SPEED * TIME_STEP;
translation.y += velocity.y * BASE_SPEED * TIME_STEP;
// 自動銷毀
if movable.auto_despawn {
const MARGIN: f32 = 200.;
if translation.y > win_size.h / 2. + MARGIN
|| translation.y < -win_size.h / 2. - MARGIN
|| translation.x > win_size.w / 2. + MARGIN
|| translation.x < -win_size.w / 2. - MARGIN
{
commands.entity(entity).despawn();
}
}
}
}
/// 敵人激光攻擊玩家判定系統
fn enemy_laser_hit_player_system(
mut commands: Commands,
mut player_state: ResMut<PlayerState>,
time: Res<Time>,
audio_source: Res<GameAudio>,
audio: Res<Audio>,
mut game_data: ResMut<GameData>,
mut next_state: ResMut<NextState<GameState>>,
laser_query: Query<(Entity, &Transform, &SpriteSize), (With<Laser>, With<FromEnemy>)>,
player_query: Query<(Entity, &Transform, &SpriteSize), With<Player>>,
) {
if let Ok((player_entity, player_tf, player_size)) = player_query.get_single() {
let player_scale = Vec2::from(player_tf.scale.xy());
for (laser, laser_tf, laser_size) in laser_query.into_iter() {
let laser_scale = Vec2::from(laser_tf.scale.xy());
let collision = collide(
player_tf.translation,
player_size.0 * player_scale,
laser_tf.translation,
laser_size.0 * laser_scale,
);
if let Some(_) = collision {
// 播放音樂
audio.play(audio_source.player_explosion.clone());
// 重置分數
game_data.reset_score();
next_state.set(GameState::Welcome);
// 銷毀角色
commands.entity(player_entity).despawn();
// 記錄被命中的時刻
player_state.shot(time.elapsed_seconds_f64());
// 銷毀激光
commands.entity(laser).despawn();
// 產生爆炸動畫
commands.spawn(ExplosionToSpawn(player_tf.translation.clone()));
break;
}
}
}
}
/// 玩家攻擊敵人判定系統
fn player_laser_hit_enemy_system(
mut commands: Commands,
audio_source: Res<GameAudio>,
audio: Res<Audio>,
mut max_enemy: ResMut<MaxEnemy>,
mut game_data: ResMut<GameData>,
laser_query: Query<(Entity, &Transform, &SpriteSize), (With<Laser>, With<FromPlayer>)>,
enemy_query: Query<(Entity, &Transform, &SpriteSize), With<Enemy>>,
) {
// 重覆刪除檢測
let mut despawn_entities: HashSet<Entity> = HashSet::new();
// 玩家激光
for (laser_entity, laser_tf, laser_size) in laser_query.iter() {
if despawn_entities.contains(&laser_entity) {
continue;
}
// 玩家激光的坐標
let laser_scale = Vec2::from(laser_tf.scale.xy());
// 敵人
for (enemy_entity, enemy_tf, enemy_size) in enemy_query.iter() {
if despawn_entities.contains(&enemy_entity) || despawn_entities.contains(&laser_entity)
{
continue;
}
// 敵人坐標
let enemy_scale = Vec2::from(enemy_tf.scale.xy());
// collide 定義兩個元素的碰撞,a 點坐標,a 的大小,b 點坐標,b 的大小,如果未發生碰撞返回 None
let collision = collide(
laser_tf.translation,
laser_size.0 * laser_scale,
enemy_tf.translation,
enemy_size.0 * enemy_scale,
);
// 碰撞檢測
if let Some(_) = collision {
// 敵人數量 -1
if max_enemy.0 != 0 {
max_enemy.0 -= 1;
}
game_data.add_score();
audio.play(audio_source.enemy_explosion.clone());
// 銷毀敵人
commands.entity(enemy_entity).despawn();
despawn_entities.insert(enemy_entity);
// 銷毀激光
commands.entity(laser_entity).despawn();
despawn_entities.insert(laser_entity);
// 播放爆炸動畫
commands.spawn(ExplosionToSpawn(enemy_tf.translation.clone()));
}
}
}
}
/// 爆炸畫面生成系統
fn explosion_to_spawn_system(
mut commands: Commands,
game_textures: Res<GameTextures>,
query: Query<(Entity, &ExplosionToSpawn)>,
) {
for (explosion_spawn_entity, explosion_to_spawn) in query.iter() {
commands
.spawn(SpriteSheetBundle {
texture_atlas: game_textures.explosion.clone(),
transform: Transform {
translation: explosion_to_spawn.0,
..Default::default()
},
..Default::default()
})
.insert(Explosion)
.insert(ExplosionTimer::default());
commands.entity(explosion_spawn_entity).despawn();
}
}
/// 爆炸動畫系統
fn explosion_animation_system(
mut commands: Commands,
time: Res<Time>,
mut query: Query<(Entity, &mut ExplosionTimer, &mut TextureAtlasSprite), With<Explosion>>,
) {
for (entity, mut timer, mut texture_atlas_sprite) in query.iter_mut() {
timer.0.tick(time.delta());
if timer.0.finished() {
texture_atlas_sprite.index += 1;
if texture_atlas_sprite.index >= EXPLOSION_ANIMATION_LEN {
commands.entity(entity).despawn();
}
}
}
}
/// 分數更新系統
fn score_display_update_system(
game_data: Res<GameData>,
mut query: Query<&mut Text, With<DisplayScore>>,
) {
for mut text in &mut query {
let new_str: String = format!("SCORE:{}", game_data.get_score());
text.sections[0].value = new_str;
}
}
player.rs
use bevy::{prelude::*, time::common_conditions::on_timer};
use std::time::Duration;
use crate::{
components::{FromPlayer, Laser, Movable, Player, SpriteSize, Velocity},
resource::GameAudio,
resource::PlayerState,
resource::WinSize,
resource::{GameState, GameTextures},
BASE_SPEED, PLAYER_LASER_SIZE, PLAYER_RESPAWN_DELAY, PLAYER_SIZE, SPRITE_SCALE, TIME_STEP,
};
pub struct PlayerPlugin;
impl Plugin for PlayerPlugin {
fn build(&self, app: &mut App) {
// add_startup_system 應用程式生命周期開始時運行一次
// StartupSet::PostStartup 在 StartupSet::Startup 後運行一次
// add_startup_system(player_spawn_system.in_base_set(StartupSet::PostStartup))
// add_system 每幀都運行 , 可以在函數後通過 run_if 傳入 bool 類型的條件進行限制
app.insert_resource(PlayerState::default())
.add_system(
player_spawn_system
.run_if(on_timer(Duration::from_secs_f32(0.5)))
.in_set(OnUpdate(GameState::InGame)),
)
.add_systems(
(
player_keyboard_event_system,
player_movable_system,
player_fire_system,
)
.in_set(OnUpdate(GameState::InGame)),
);
}
}
/// 玩家角色生成系統
fn player_spawn_system(
mut commands: Commands,
mut player_state: ResMut<PlayerState>,
time: Res<Time>,
game_textures: Res<GameTextures>,
win_size: Res<WinSize>,
) {
let now = time.elapsed_seconds_f64();
let last_shot = player_state.last_shot;
if !player_state.on && (player_state.last_shot == -1. || now - PLAYER_RESPAWN_DELAY > last_shot)
{
let bottom = -win_size.h / 2.;
// 創建組件實體,並返回對應的 EntityCommand
commands
.spawn(SpriteBundle {
texture: game_textures.player.clone(),
transform: Transform {
translation: Vec3::new(
0.,
bottom + PLAYER_SIZE.1 / 2. * SPRITE_SCALE + 5.0,
10.,
),
scale: Vec3::new(SPRITE_SCALE, SPRITE_SCALE, 1.0),
..default()
},
..SpriteBundle::default()
})
.insert(Velocity::new(0., 0.))
.insert(Movable {
auto_despawn: false,
})
.insert(SpriteSize::from(PLAYER_SIZE))
.insert(Player);
player_state.spawned();
}
}
/// 玩家攻擊系統
fn player_fire_system(
mut commands: Commands,
audio_source: Res<GameAudio>,
audio: Res<Audio>,
kb: Res<Input<KeyCode>>,
game_textures: Res<GameTextures>,
query: Query<&Transform, With<Player>>,
) {
if let Ok(player_tf) = query.get_single() {
// just_released 鬆開按鍵
if kb.just_released(KeyCode::Space) {
audio.play(audio_source.player_laser.clone());
let (x, y) = (player_tf.translation.x, player_tf.translation.y);
let x_offset = PLAYER_SIZE.0 / 2. * SPRITE_SCALE - 5.;
// 激光生成閉包 因為這裡使用了 commands 生成新的包 所以這裡的閉包需要定義為 mut 類型
let mut spawn_laser = |x_offset: f32| {
commands
.spawn(SpriteBundle {
texture: game_textures.player_laser.clone(),
transform: Transform {
translation: Vec3::new(x + x_offset, y + 15., 1.),
scale: Vec3::new(SPRITE_SCALE, SPRITE_SCALE, 0.),
..Default::default()
},
..Default::default()
})
.insert(Laser)
.insert(FromPlayer)
.insert(SpriteSize::from(PLAYER_LASER_SIZE))
.insert(Movable { auto_despawn: true })
.insert(Velocity::new(0., 1.));
};
spawn_laser(x_offset);
spawn_laser(-x_offset);
}
}
}
/// 鍵盤事件系統
fn player_keyboard_event_system(
kb: Res<Input<KeyCode>>,
mut next_state: ResMut<NextState<GameState>>,
mut query: Query<&mut Velocity, With<Player>>,
) {
if let Ok(mut velocity) = query.get_single_mut() {
// pressed 按下按鍵
if kb.pressed(KeyCode::Left) {
velocity.x = -1.
} else if kb.pressed(KeyCode::Right) {
velocity.x = 1.
} else if kb.just_pressed(KeyCode::P) {
next_state.set(GameState::Paused);
} else {
velocity.x = 0.
}
};
}
/// 玩家移動系統
fn player_movable_system(
win_size: Res<WinSize>,
mut query: Query<(&Velocity, &mut Transform), With<Player>>,
) {
let max_w = win_size.w / 2.;
for (velocity, mut transform) in query.iter_mut() {
let distance = velocity.x * BASE_SPEED * TIME_STEP;
let new_x = transform.translation.x + distance;
if -max_w <= new_x && new_x <= max_w {
// 移動位置
transform.translation.x += distance;
}
}
}
resource.rs
use bevy::{
prelude::{AudioSource, Handle, Image, Resource, States},
sprite::TextureAtlas,
text::Font,
};
/// 游戲視窗大小資源
#[derive(Resource)]
pub struct WinSize {
pub w: f32,
pub h: f32,
}
/// 游戲圖像資源
#[derive(Resource)]
pub struct GameTextures {
pub background: Handle<Image>,
pub player: Handle<Image>,
pub player_laser: Handle<Image>,
pub enemy: Handle<Image>,
pub enemy_laser: Handle<Image>,
pub explosion: Handle<TextureAtlas>,
pub font: Handle<Font>,
}
/// 敵人最大數量
#[derive(Resource)]
pub struct MaxEnemy(pub u32);
/// 玩家狀態
#[derive(Resource)]
pub struct PlayerState {
pub on: bool,
pub last_shot: f64,
}
impl Default for PlayerState {
fn default() -> Self {
Self {
on: false,
last_shot: -1.,
}
}
}
impl PlayerState {
/// 被命中
pub fn shot(&mut self, time: f64) {
self.on = false;
self.last_shot = time;
}
/// 重生
pub fn spawned(&mut self) {
self.on = true;
self.last_shot = -1.;
}
}
#[derive(Resource)]
pub struct GameAudio {
pub enemy_explosion: Handle<AudioSource>,
pub player_explosion: Handle<AudioSource>,
pub player_laser: Handle<AudioSource>,
}
/// 游戲狀態
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
pub enum GameState {
/// 歡迎
#[default]
Welcome,
/// 游戲中
InGame,
/// 暫停
Paused,
}
/// 游戲數據
#[derive(Resource)]
pub struct GameData {
score: u32,
}
impl GameData {
pub fn new() -> Self {
Self { score: 0 }
}
/// 獲取當前得分
pub fn get_score(&self) -> u32 {
self.score
}
/// 增加得分
pub fn add_score(&mut self) {
self.score += 1;
}
/// 增加得分
pub fn reset_score(&mut self) {
self.score = 0;
}
}
state.rs
use bevy::{
prelude::{
Color, Commands, Entity, Input, IntoSystemAppConfig, IntoSystemConfig, IntoSystemConfigs,
KeyCode, NextState, OnEnter, OnExit, OnUpdate, Plugin, Query, Res, ResMut, Transform, Vec3,
With,
},
text::{Text, Text2dBundle, TextAlignment, TextSection, TextStyle},
time::Time,
};
use crate::{
components::{PausedText, WelcomeText},
resource::{GameState, GameTextures},
};
pub struct StatePlugin;
impl Plugin for StatePlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app
// 在 CoreSet::StateTransitions 期間,當 AppState::Menu 時,該函數執行,
//當退出該狀態進入下一個狀態時,會先執行當前狀態的退出函數,再執行下個狀態的函數
// OnEnter 進入時執行、OnUpdate 期間內每幀執行、OnExit 退出時執行
.add_system(welcome_system.in_schedule(OnEnter(GameState::Welcome)))
// CoreSet::Update 期間 主函數中的 on_update 將會檢查 State 資源的值,並判斷是否應該運行
.add_systems(
(welcome_input_system, welcome_text_scale_system)
.in_set(OnUpdate(GameState::Welcome)),
)
.add_system(welcome_exit_system.in_schedule(OnExit(GameState::Welcome)))
// Paused 狀態下執行的函數
.add_system(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)));
}
}
/// 歡迎狀態下運行的系統
pub fn welcome_system(mut commands: Commands, game_textures: Res<GameTextures>) {
// 字體引入
let font = game_textures.font.clone();
let text_style = TextStyle {
font: font.clone(),
font_size: 46.,
color: Color::BLUE,
};
let text_alignment = TextAlignment::Center;
let text = Text {
sections: vec![
TextSection::new("PRESS ", text_style.clone()),
TextSection::new(
" ENTER ",
TextStyle {
color: Color::RED,
..text_style.clone()
},
),
TextSection::new("START GAME !\r\n", text_style.clone()),
TextSection::new("PRESS ", text_style.clone()),
TextSection::new(
" P ",
TextStyle {
color: Color::RED,
..text_style.clone()
},
),
TextSection::new("TO PAUSED GAME !", text_style.clone()),
],
..Default::default()
}
.with_alignment(text_alignment);
commands.spawn((
Text2dBundle {
text,
transform: Transform {
translation: Vec3 {
x: 0.,
y: -20.,
z: 11.,
},
..Default::default()
},
..Default::default()
},
WelcomeText,
));
}
/// 歡迎狀態狀態下的鍵盤監聽系統
pub fn welcome_input_system(kb: Res<Input<KeyCode>>, mut next_state: ResMut<NextState<GameState>>) {
if kb.just_pressed(KeyCode::Return) {
next_state.set(GameState::InGame);
}
}
/// 歡迎狀態字體變化系統
pub fn welcome_text_scale_system(
time: Res<Time>,
mut query: Query<&mut Transform, (With<Text>, With<WelcomeText>)>,
) {
for mut transform in &mut query {
transform.scale = Vec3::splat(time.elapsed_seconds().sin() / 4. + 0.9);
}
}
/// 退出歡迎狀態時執行的系統
pub fn welcome_exit_system(
mut commands: Commands,
query: Query<Entity, (With<Text>, With<WelcomeText>)>,
) {
for entity in query.iter() {
commands.entity(entity).despawn();
}
}
/// 暫停狀態下運行的系統
pub fn paused_system(mut commands: Commands, game_textures: Res<GameTextures>) {
// 字體引入
let font = game_textures.font.clone();
let text_style = TextStyle {
font: font.clone(),
font_size: 46.,
color: Color::BLUE,
};
let text_alignment = TextAlignment::Center;
let text = Text {
sections: vec![
TextSection::new("GAME PAUSED!\r\nPRESSED", text_style.clone()),
TextSection::new(
" R ",
TextStyle {
color: Color::RED,
..text_style.clone()
},
),
TextSection::new("RETURN GAME!", text_style.clone()),
],
..Default::default()
}
.with_alignment(text_alignment);
commands.spawn((
Text2dBundle {
text,
transform: Transform {
translation: Vec3 {
x: 0.,
y: -20.,
z: 11.,
},
..Default::default()
},
..Default::default()
},
PausedText,
));
}
/// 暫停狀態狀態下的鍵盤監聽系統
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<PausedText>)>,
) {
for entity in query.iter() {
commands.entity(entity).despawn();
}
}
about me
目前失業,在家學習 rust 。
本文來自博客園,作者:賢雲曳賀,轉載請註明原文鏈接:https://www.cnblogs.com/SantiagoZhang/p/17334165.html