Rust 实现的飞机游戏
简介
一个使用 bevy 引擎制作的飞机游戏。
因为 bevy 已经升级到 0.10.1 了,所以重新做一遍。顺带手出个教程。
下面是做的部分变动:
- 将激光以及玩家的移动模块进行了拆分。
- 新增了背景图片。
- 新增了游戏状态管理 Welcome/InGame/Paused。
- 新增了声音播放模块。
- 新增了游戏记分板。
通过左右方向键进行控制,使用空格发射激光。
按 P 暂停游戏,按 S 恢复游戏。
更新后的GitHub地址
代码结构
·
├── assets/
│ ├──audios/
│ ├──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/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 。
标签:mut,飞机,enemy,system,pub,大战,player,let,rust From: https://www.cnblogs.com/SantiagoZhang/p/17334165.html