Bevy - 写个三角形打方块(space-shooter)

创建项目

写个射击小游戏用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,
            ));
        }
    }
}

还有设置一下计时器的时间,问题是?为什么要在这个地方设置间隔,很匪is好吧,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; // 子弹已经消失,跳出内层循环
            }
        }
    }
}

大功告成!恭喜你,写了个垃圾游戏(((((((((((((