Rust 实现的飞机游戏
简介
一个使用 bevy 引擎制作的飞机游戏。
原视频教程地址,github 地址。
因为 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|=√(x1−x2)2+(y1−y2)2
| 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 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 |
| } |
| } |
| |
| |
| |
| |
| fn enemy_movement_system(mut query: Query<(&mut Transform, &mut Formation), With<Enemy>>) { |
| |
| |
| for (mut transform, mut formation) in query.iter_mut() { |
| |
| let (x_org, y_org) = (transform.translation.x, transform.translation.y); |
| |
| |
| |
| |
| let max_distance = formation.speed * TIME_STEP; |
| |
| |
| |
| let dir = if formation.start.0 < 0. { 1. } else { -1. }; |
| |
| |
| let (x_pivot, y_pivot) = formation.pivot; |
| |
| |
| let (x_radius, y_radius) = formation.radius; |
| |
| |
| |
| 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; |
| |
| |
| |
| 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. |
| }; |
| |
| |
| 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); |
| |
| |
| 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() { |
| |
| |
| 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) |
| |
| .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)), |
| ) |
| |
| .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>, |
| ) { |
| |
| commands.spawn(Camera2dBundle::default()); |
| |
| |
| let window = windows.single_mut(); |
| let win_w = window.width(); |
| let win_h = window.height(); |
| |
| |
| 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); |
| |
| |
| 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()); |
| |
| |
| let collision = collide( |
| laser_tf.translation, |
| laser_size.0 * laser_scale, |
| enemy_tf.translation, |
| enemy_size.0 * enemy_scale, |
| ); |
| |
| |
| if let Some(_) = collision { |
| |
| 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) { |
| |
| |
| |
| |
| 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.; |
| |
| |
| 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() { |
| |
| 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.; |
| |
| |
| 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() { |
| |
| 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 |
| |
| |
| |
| .add_system(welcome_system.in_schedule(OnEnter(GameState::Welcome))) |
| |
| .add_systems( |
| (welcome_input_system, welcome_text_scale_system) |
| .in_set(OnUpdate(GameState::Welcome)), |
| ) |
| .add_system(welcome_exit_system.in_schedule(OnExit(GameState::Welcome))) |
| |
| .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 。
我的 bilibili,我的 GitHub。
Rust官网
Rust 中文社区
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!