创建项目
写个射击小游戏用rust的bevy,资源纯靠网上白嫖
[package]
name = "space-shooter"
version = "0.1.0"
edition = "2024"
[dependencies]
bevy = "0.17.3"先抄个main方法(这个库下载了两年半)
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.run();
}现在启动bevy,就是一个灰色的啥也没有的窗口,现在这个灰色窗口就像一个空舞台,我们要往里面放东西!
为什么是空的?
因为你只创建了窗口,但没有:
1. 相机(就像现实中没有摄像机,啥也拍不到)
2. 任何物体(舞台上没演员)
创建一个白色方块
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.run();
}
fn setup(mut commands: Commands) {
// 生成 2D 相机
commands.spawn(Camera2d);
// 生成一个白色方块
commands.spawn((
Sprite {
color: Color::WHITE,
custom_size: Some(Vec2::new(100.0, 100.0)),
..default()
},
Transform::default(),
));
把相机删掉会怎么样?为什么需要相机?
想象一下:
游戏世界 = 真实舞台(有演员、道具)
相机 = 摄像机(拍摄画面)
窗口 = 电视屏幕(显示拍到的内容)
没有相机,舞台上的东西确实存在,但没人拍摄,所以屏幕上啥也看不到。
第一个玩家
#[derive(Component)]
struct Player;定义一个实体Player,标记一下这个Transform是一个Player
fn setup(mut commands: Commands) {
// 生成 2D 相机
commands.spawn(Camera2d);
// 生成一个白色方块
commands.spawn((
Sprite {
color: Color::srgb(1.0, 1.0, 1.0),
custom_size: Some(Vec2::new(100.0, 100.0)),
..default()
},
Transform::default(),
Player,
));
}实现玩家移动
实现一个系统move_palyer, 在main里面加上add_systems(Update, move_player)
fn move_player(
keyboard: Res<ButtonInput<KeyCode>>, // 读取键盘
mut query: Query<&mut Transform, With<Player>>, // 查询有Player标记的实体的Transform
) {
for mut transform in query.iter_mut() {
let speed = 5.0; // 移动速度
if keyboard.pressed(KeyCode::ArrowLeft) {
transform.translation.x -= speed; // 往左移
}
if keyboard.pressed(KeyCode::ArrowRight) {
transform.translation.x += speed; // 往右移
}
if keyboard.pressed(KeyCode::ArrowUp) {
transform.translation.y += speed; // 往上移
}
if keyboard.pressed(KeyCode::ArrowDown) {
transform.translation.y -= speed; // 往下移
}
}
}Query 是什么?
Query<&mut Transform, With<Player>>
意思是:"找到所有带 Player 标记的实体,给我它们的 Transform 组件"
每帧执行
- Update 系统每秒执行 60 次(60 FPS)
- 所以 move_player 每秒被调用 60 次
- 每次按键盘,方块移动 5 像素
- 60 次 × 5 像素 = 每秒移动 300 像素
为什么用 for 循环?
虽然现在只有 1 个玩家,但 Query 可能返回多个结果,所以用循环遍历。
---
编译完成后,用方向键试试!
飞船应该是三角形的!
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>, // 用来创建形状
mut materials: ResMut<Assets<ColorMaterial>>, // 用来上色
) {
// 生成 2D 相机
commands.spawn(Camera2d);
let triangle_mesh = Mesh::from(Triangle2d::new(
Vec2::new(0.0, 25.0), // 顶点(上)
Vec2::new(-20.0, -25.0), // 左下
Vec2::new(20.0, -25.0), // 右下
));
commands.spawn((
Mesh2d(meshes.add(triangle_mesh)),
MeshMaterial2d(materials.add(Color::srgb(0.3, 0.5, 1.0))),
// 蓝色
Transform::default(),
Player,
));
}接下来你的飞窜就是一个蓝色的三角形了,虽然丑的一比...
来颗子弹吧!
#[derive(Component)]
struct Bullet;// 发射子弹系统
fn shoot_bullet(
keyboard: Res<ButtonInput<KeyCode>>,
player_query: Query<&Transform, With<Player>>,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
if keyboard.just_pressed(KeyCode::Space) { // 空格射击
// 用 for 循环获取玩家位置
for player_transform in player_query.iter() {
let circle_mesh = Mesh::from(Circle::new(5.0));
commands.spawn((
Mesh2d(meshes.add(circle_mesh)),
MeshMaterial2d(materials.add(Color::srgb(1.0, 1.0, 0.0))),
Transform::from_xyz(
player_transform.translation.x,
player_transform.translation.y + 30.0,
0.0,
),
Bullet,
));
}
}
}当然你的子弹要会移动!
// 移动子弹系统
fn move_bullets(mut query: Query<&mut Transform, With<Bullet>>) {
for mut transform in query.iter_mut() {
transform.translation.y += 8.0; // 子弹速度:每帧往上 8像素
}
}自动射击?实现!
什么?你想要自动射击?这个要加个定时器资源,不过为什么定时器是资源?匪夷所思
#[derive(Resource)]
struct ShootTimer(Timer);
// 发射子弹(定时)
fn shoot_bullet(
time: Res<Time>, // 获取时间
mut timer: ResMut<ShootTimer>, // 获取计时器
player_query: Query<&Transform, With<Player>>,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
// 更新计时器
timer.0.tick(time.delta());
// 计时器到时间就发射
if timer.0.just_finished() {
for player_transform in player_query.iter() {
let circle_mesh = Mesh::from(Circle::new(5.0));
commands.spawn((
Mesh2d(meshes.add(circle_mesh)),
MeshMaterial2d(materials.add(Color::srgb(1.0, 1.0,
0.0))),
Transform::from_xyz(
player_transform.translation.x,
player_transform.translation.y + 30.0,
0.0,
),
Bullet,
));
}
}
}还有设置一下计时器的时间,问题是?为什么要在这个地方设置间隔,很匪i所s好吧,rust写游戏太奇怪了,只能说是很适合个人开发者...
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(ShootTimer(Timer::from_seconds(0.2, TimerMode::Repeating))) // 每 0.2 秒发射一次
.add_systems(Startup, setup) // 启动时执行 setup 函数
.add_systems(Update, (move_player, shoot_bullet, move_bullets))
.run();
}添加敌人
添加敌人标记组件
#[derive(Component)]
struct Enemy; // 新增:敌人标记添加敌人生成计时器
#[derive(Resource)]
struct EnemySpawnTimer(Timer);
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(ShootTimer(Timer::from_seconds(0.2, TimerMode::Repeating))) // 每 0.2 秒发射一次
.insert_resource(EnemySpawnTimer(Timer::from_seconds(
1.5,
TimerMode::Repeating,
)))
.add_systems(Startup, setup) // 启动时执行 setup 函数
.add_systems(
Update,
(
move_player,
shoot_bullet,
move_bullets,
spawn_enemies,
move_enemies,
),
)
.run();
}在最后面加这两个函数:
// 生成敌人系统
fn spawn_enemies(
time: Res<Time>,
mut timer: ResMut<EnemySpawnTimer>,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
timer.0.tick(time.delta());
if timer.0.just_finished() {
// 随机 X 坐标(-300 到 300)
use rand::Rng;
let mut rng = rand::rng();
let x = rng.random_range(-300.0..300.0);
// 创建方块敌人
let square_mezh = Mesh::from(Rectangle::new(40.0, 40.0));
commands.spawn((
Mesh2d(meshes.add(square_mesh)),
MeshMaterial2d(materials.add(Color::srgb(1.0, 0.2,0.2))), // 红色
Transform::from_xyz(x, 400.0, 0.0), // 从顶部生成
Enemy,
));
}
} // 移动敌人系统
fn move_enemies(mut query: Query<&mut Transform, With<Enemy>>) {
for mut transform in query.iter_mut() {
transform.translation.y -= 3.0; // 往下移动
}
}碰撞检测呢?不会是每帧都去遍历子弹和敌人,然后判断是否碰到吧?
hhhh,你的直觉不错!不过对于这种小规模的游戏,暴力遍历其实完全够用
为什么简单遍历没问题?假设:
屏幕上有 10 个敌人
有 20 颗子弹
每帧检查:10 × 20 = 200 次比较
每秒 60 帧 = 12,000 次计算
对现代 CPU 来说,这点计算微不足道(纳秒级别)。
更高级的做法(当实体很多时)当你有成百上千个实体时,才需要:
1. 空间分割 - 四叉树/网格划分(只检查附近的物体)
2. 物理引擎 - 用 Bevy 插件如 bevy_rapier 或 bevy_xpbd
3. 宽相/窄相检测 - 先粗略筛选,再精确检测
我们先用简单方法,对于现在的游戏规模,简单遍历是最佳实践:
代码清晰易懂
性能足够好
不需要引入复杂的物理库
等游戏做大了再优化不迟!这叫 "premature optimization is the root of all evil"(过早优化是万恶之源)
碰撞检测
// 碰撞检测系统
fn check_collisions(
mut commands: Commands,
bullet_query: Query<(Entity, &Transform), With<Bullet>>, // 获取所有子弹的实体ID和位置
enemy_query: Query<(Entity, &Transform), With<Enemy>>, // 获取所有敌人的实体ID和位置
) {
// 遍历所有子弹
for (bullet_entity, bullet_transform) in bullet_query.iter() {
// 遍历所有敌人
for (enemy_entity, enemy_transform) in enemy_query.iter() {
// 计算两者之间的距离
let distance = bullet_transform
.translation
.distance(enemy_transform.translation);
// 如果距离小于 30(子弹半径5 + 敌人半边长20 +一点容差)
if distance < 30.0 {
// 删除子弹
commands.entity(bullet_entity).despawn();
// 删除敌人
commands.entity(enemy_entity).despawn();
break; // 子弹已经消失,跳出内层循环
}
}
}
}大功告成!恭喜你,写了个垃圾游戏(((((((((((((