一个简单的 rust 项目 飞机大战
Posted 偷学rust的欧老叭
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一个简单的 rust 项目 飞机大战相关的知识,希望对你有一定的参考价值。
rust 小游戏
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 。
本文来自博客园,作者:贤云曳贺,转载请注明原文链接:https://www.cnblogs.com/SantiagoZhang/p/17334165.html
C/C++游戏项目教程:看完只会说“编译飞机大战太简单了吧”
《飞机大战》这是一款经典飞行射击类游戏,为玩家呈现一场不一样射击体验。简单的触屏操作,触屏按住随意一个地方,左右移动,便可自动攻击敌人,上下移动亦可躲避强敌。玩家在游戏中要做的就是驾驶着最新战机,向敌人发起冲击。
今天我就用C++带大家一步步去完成风靡全球的小程序飞机大战小游戏~
PS:要安装easyx图形库哦 #include<easyx.h>
开发工具为VS2013
在此之前呢,和大家说明一下,因为这是一个比较大的项目了,所以展示所有代码会有些困难,所以我裁剪了主要的大部分代码,主要目的是让大家明白实现这个项目的逻辑思路,希望大家可以理解,完整代码/编译器/图形库在文章最下方获取哦
正片开始:
第一步: 打开vs2013,创建一个项目,并将准备好的素材资源放到同级目录下(素材可以在文章最下方领取),如图:
第二步:写出基本框架,看自己背景图片的像素,创建一个相同像素的图形窗口,并把背景图片加上:
第三步:设置音乐,及玩家、敌机、子弹,开启双缓冲绘图:
#include<stdio.h>
#include<graphics.h>//包含图形库头文件 easyx
#include<mmsystem.h>
#pragma comment(lib,"winmm.lib")//加载多媒体设备库
#define WIN_WIDTH 591
#define WIN_HEIGHT 864
#define PLAYER_BULLET 15 //玩家子弹数量
#define ENEMYPLANE_NUM 10
//定义变量 int a=666;
//定义一个图片
IMAGE bk;
IMAGE img_player[4];
IMAGE img_bullet[2];
IMAGE img_enemy[2];
struct Plane
int x;
int y;
bool flag;//..是否死亡
int width;
int height;
player, pbull[PLAYER_BULLET], enemy[ENEMYPLANE_NUM];
//时间的变化量
DWORD t1, t2;
//分模块处理,专门的事情,准们的函数做
void GameInit()
//1,创建一个图形窗口
initgraph(WIN_WIDTH, WIN_HEIGHT);
//设置随机数种子
srand(GetTickCount());
//2,贴背景图片
//2.1加载图片到bk里面
loadimage(&bk, "./images/background.jpg");
loadimage(&img_player[0], "./images/planeNormal_1.jpg");
loadimage(&img_player[1], "./images/planeNormal_2.jpg");
loadimage(&img_player[2], "./images/planeBoom_1.jpg");
loadimage(&img_player[3], "./images/planeBoom_2.jpg");
loadimage(&img_bullet[0], "./images/bullet1.jpg");
loadimage(&img_bullet[1], "./images/bullet2.jpg");
loadimage(&img_enemy[0], "./images/enemy_1.jpg");
loadimage(&img_enemy[1], "./images/enemy_2.jpg");
//3,播放音乐 首先打开音乐,然后播放音乐
mciSendString("open ./images/game_music.mp3 alias BGM", 0, 0, 0);
mciSendString("play BGM repeat", 0, 0, 0);
//初始化玩家数据
player.height = 120;
player.width = 117;
player.x = WIN_WIDTH / 2 - player.width / 2;
player.y = WIN_HEIGHT - player.height;
player.flag = true;
//初始化子弹
for (int i = 0; i < PLAYER_BULLET; i++)
pbull[i].flag = false;
pbull[i].height = 24;
pbull[i].width = 19;
//初始化时间变量
t1 = t2 = GetTickCount();
//初始化敌机
for (int i = 0; i < ENEMYPLANE_NUM; i++)
enemy[i].flag = false;
enemy[i].width = 52;
enemy[i].height = 39;
//游戏绘制函数
void GameDarw()
//双缓冲绘图,防止画面闪烁
BeginBatchDraw();
//2.2输出背景图片
putimage(0, 0, &bk);
//透明贴图
if (player.flag)
putimage(player.x, player.y, &img_player[0], NOTSRCERASE);
putimage(player.x, player.y, &img_player[1], SRCINVERT);
//绘制玩家子弹
for (int i = 0; i < PLAYER_BULLET; i++)
if (pbull[i].flag)
putimage(pbull[i].x, pbull[i].y, &img_bullet[0], NOTSRCERASE);
putimage(pbull[i].x, pbull[i].y, &img_bullet[1], SRCINVERT);
//绘制敌机
for (int i = 0; i < ENEMYPLANE_NUM; i++)
if (enemy[i].flag)
putimage(enemy[i].x, enemy[i].y, &img_enemy[0], NOTSRCERASE);
putimage(enemy[i].x, enemy[i].y, &img_enemy[1], SRCINVERT);
EndBatchDraw();
//产生玩家子弹
void CreatBullet()
for (int i = 0; i < PLAYER_BULLET; i++)
if (!pbull[i].flag)
pbull[i].x = player.x+53;
pbull[i].y = player.y;
pbull[i].flag = true;
break;
//生成敌机
void CreateEnemy()
for (int i = 0; i < ENEMYPLANE_NUM; i++)
if (!enemy[i].flag)
enemy[i].x = rand() % (WIN_WIDTH - enemy[i].width);
enemy[i].y =0;
enemy[i].flag = true;
break;
void EnemyMove(int speed)
for (int i = 0; i < ENEMYPLANE_NUM; i++)
if (enemy[i].flag)
enemy[i].y += speed;
if (enemy[i].y>WIN_HEIGHT)
enemy[i].flag = false;
//操控飞机,开..
void KeyControl(int speed)
//按键处理,用getch()
//向上移动 边界处理
if(GetAsyncKeyState(VK_UP) && player.y>=0)
player.y -= speed;
//向下移动
if (GetAsyncKeyState(VK_DOWN) && player.y+player.height<=WIN_HEIGHT)
player.y += speed;
//向左移动
if (GetAsyncKeyState(VK_LEFT) && player.x+player.width/2>=0)
player.x -= speed;
//向右移动
if (GetAsyncKeyState(VK_RIGHT) && player.x+player.width/2<=WIN_WIDTH)
player.x += speed;
//发射子弹
if (GetAsyncKeyState(VK_SPACE) && t2-t1>200)
mciSendString("close gun", 0, 0, 0);
mciSendString("open ./images/f_gun.mp3 alias gun", 0, 0, 0);
mciSendString("play gun", 0, 0, 0);
//产生一个子弹
CreatBullet();
t1 = t2;
t2 = GetTickCount();
//生成敌机
void CreateEnemy()
for (int i = 0; i < ENEMYPLANE_NUM; i++)
if (!enemy[i].flag)
enemy[i].x = rand() % (WIN_WIDTH - enemy[i].width);
enemy[i].y =0;
enemy[i].flag = true;
break;
void EnemyMove(int speed)
for (int i = 0; i < ENEMYPLANE_NUM; i++)
if (enemy[i].flag)
enemy[i].y += speed;
if (enemy[i].y>WIN_HEIGHT)
enemy[i].flag = false;
第四步:设置子弹的移动,鼠标操控飞机:
//玩家子弹的移动
void BulletMove(int speed)
for (int i = 0; i < PLAYER_BULLET; i++)
if (pbull[i].flag)
pbull[i].y -= speed ;
if (pbull[i].y < 0)
pbull[i].flag = false;
//操控飞机,开..
void KeyControl(int speed)
//按键处理,用getch()
//向上移动 边界处理
if(GetAsyncKeyState(VK_UP) && player.y>=0)
player.y -= speed;
//向下移动
if (GetAsyncKeyState(VK_DOWN) && player.y+player.height<=WIN_HEIGHT)
player.y += speed;
//向左移动
if (GetAsyncKeyState(VK_LEFT) && player.x+player.width/2>=0)
player.x -= speed;
//向右移动
if (GetAsyncKeyState(VK_RIGHT) && player.x+player.width/2<=WIN_WIDTH)
player.x += speed;
//发射子弹
if (GetAsyncKeyState(VK_SPACE) && t2-t1>200)
mciSendString("close gun", 0, 0, 0);
mciSendString("open ./images/f_gun.mp3 alias gun", 0, 0, 0);
mciSendString("play gun", 0, 0, 0);
//产生一个子弹
CreatBullet();
t1 = t2;
t2 = GetTickCount();
第五步:设置游戏开始,判断子弹是否击中:
//开始...
void PlayPlane()
//每个敌军和每个子弹都要比较
for (int i = 0; i < ENEMYPLANE_NUM; i++)
if (!enemy[i].flag)
continue;
//遍历子弹数组
for (int k = 0; k < PLAYER_BULLET; k++)
if (!pbull[k].flag)
continue;
//判断子弹是否击中敌机
if (pbull[k].x>enemy[i].x && pbull[k].x<enemy[i].x+enemy[i].width &&
pbull[k].y>enemy[i].y && pbull[k].y<enemy[i].y+enemy[i].height)
//击中了,敌机要消失,子弹也要消失
enemy[i].flag = false;
pbull[i].flag = false;
//分数+=10;
int main()
GameInit();
while (1)
GameDarw();
KeyControl(1);
BulletMove(1);
if (rand() % 30 == 1)
CreateEnemy();
PlayPlane();
EnemyMove(1);
getchar();
return 0;
好啦,重要的基础代码就这些~需要完整源码对照的同学可以点击文章末链接群领取,飞机大战/雷霆战机教程就到此结束啦,后续我会发布更多的项目源码以及学习资料,希望大家可以持续关注,想要C/C++学习资料以及完整源码素材图形库开发工具等的可以加群【639681529】了解,或者有问题也可以进群提问,希望大家可以在这里得到自己想要的知识,也希望如果对你有所帮助的话可以多多关注点赞评论,有建议也可以在评论区提出,谢谢大家的支持!
点击下方链接进群:
源码素材编译器图形库还有各种学习资料都在这里等你哦~https://jq.qq.com/?_wv=1027&k=ne3yBn5V
以上是关于一个简单的 rust 项目 飞机大战的主要内容,如果未能解决你的问题,请参考以下文章