目录

📝 文章摘要

一、背景介绍

1.1 传统游戏架构的痛点

1.2 ECS 架构:数据驱动的解决方案

二、原理详解

2.1 Bevy 的核心概念

2.2 ECS 查询 (Query)

三、代码实战:2D 贪吃蛇游戏

3 3.1 环境准备

3.2 步骤 1:定义组件和资源

3.4 步骤 3:实现系统 (Systems)

四、结果分析

4.1 ECS 架构性能

4.2 Bevy 编译时间

五、总结与讨论

5.1 核心要点

5.2 讨论问题

参考链接


📝 文章摘要

Bevy 是一个用 Rust 编写的、数据驱动、简单易用的游戏引擎。它以其快速的编译时间和现代的实体组件系统(Entity Component System, ECS)架构而闻名。本文将深入剖析 ECS 模式为何适合游戏开发,讲解 Bevy 引擎的核心概念(App, Plugin, System, Component, Entity),并实战构建一个完整的 2D 游戏(如“贪吃蛇”)。通过本文,读者将理解 Bevy 的设计哲学,并掌握使用 Rust 进行游戏开发的基础。


一、背景介绍

1.1 传统游戏架构的痛点

传统游戏开发常使用面向对象编程(OOP)。

在这里插入图片描述

痛点

  1. **菱形继承问题:当 Goblin 既是 Enemy 又是 Merchant 时,继承关系变得复杂。
  2. 数据局部Player 对象的数据(Position, Health…)在内存中是连续的,但 Player 和 Enemy 的 Position 数据却是分散的,导致 CPU 缓存命中率低。
  3. 逻辑耦合update() 方法通常包含渲染、物理、AI 等多种逻辑,难以维护和并行。

1.2 ECS 架构:数据驱动的解决方案

ECS(Entity Component System)将数据和逻辑据和逻辑解耦。

  • Entity (实体):一个唯一的 ID(如 Player1EnemyGoblin)。
  • Component:纯数据结构(如 Position {x, y}Velocity {dx, dy})。
  • System (系统):纯逻辑(如 move_systemrender_system)。
  1. 组合优于继承:实体可以任意组合组件。
  2. 数据局部性好:`Physicsystem只遍历PositionVelocity` 的连续内存,缓存命中率极高。
  3. 易于并行PhysicsSystem 和 AISystem 可以并行执行(如果它们不访问相同的可变组件)。

二、原理详解

2.1 Bevy 的核心概念

在这里插入图片描述

  • App:Bevy 应用的根。
  • Plugin:模块化单元(如 PhysicsPluginRenderPlugin)。
  • **Resource:全局单例数据(如 ScoreTime)。
  • Entity:一个 ID。
  • Component:附加到 Entity 上的数据。
  • System:操作 Component 和 Resource 的函数。

2.2 ECS 查询 (Query)

System 如何获取数据?通过 Query

use bevy::prelude::*;

// 组件
#[derive(Component)]
struct Position { x: f32, y: f32 }
#[derive(Component)]
struct Velocity { dx: f32, dy: f32 }
#[derive(Component)]
struct PlayerTag;

// 系统
fn physics_system(
    time: Res<Time>, // 访问全局资源
    mut query: Query<(&mut Position, &Velocity)> // 查询
) {
    // 遍历所有同时拥有 Position 和 Velocity 的实体
    for (mut pos, vel) in query.iter_mut() {
        pos.x += vel.dx * time.delta_seconds();
        pos.y += vel.dy * time.delta_seconds();
    }
}

// 带过滤的查询
fn player_system(
    mut query: Query<&Position, With<PlayerTag>> // 只查询带 PlayerTag 的实体
) {
    for pos in query.iter() {
        println!("玩家位置: {:?}", pos);
    }
}

三、代码实战:2D 贪吃蛇游戏

3 3.1 环境准备

[package]
name = "snake_game"
version = "0.1.0"
edition =2021"

[dependencies]
bevy = "0.13"
rand = "0.8"

3.2 步骤 1:定义组件和资源

src/main.rs

use bevy::prelude::*;
use bevy::time::FixedTimestep;
use rand::prelude::random;

// --- 常量 ---
const ARENA_WIDTH: u32 = = 20;
const ARENA_HEIGHT: u32 = 20;
const SNAKE_SPEED: f64 = 0.1 // 游戏速度

// --- 资源 (全局状态) ---
#[derive(Resource)]
struct GameState {
    score: u32,
    is_game_over: bool,
}

#[derive(Resource)]
struct SnakeSegments(Vec<Entity>); // 蛇身,,存储实体ID

#[derive(Resource)]
struct MoveTimer(Timer);

// --- 组件 (实体数据) ---
#[derive(Component, Clone Copy, PartialEq, Eq, Debug)]
struct Position {
    x: i32,
    y: i32,
}

#[derive(Component)]
struct SnakeHead {
    direction: Direction,
}

#[derive(Component)]
struct SnakeSegment; // 标记组件

#[derive(e(Component)]
struct Food;

#[derive(PartialEq, Copy, Clone)]
enum Direction {
    Up, Down, Left, Right,
}``

## 3.3 步骤 2:设置 App 和插件

```rust
fn main() {
    App::new()
        . .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                title: "Rust 贪吃蛇"..into(),
                resolution: (500.0, 500.0).into(),
                ..default()
            }),          ..default()
        }))
        // 添加资源
        .insert_resource(GameState { score: 0, is_game_over: false })
        .insert_resource(SnakeSegments(Vec::new()))
        .insert_resource(MoveTimer(Timer::from_seconds(SNAKEKE_SPEED, TimerMode::Repeating)))
        // 启动系统
        .add_systems(Startup, setup_camera_and_arena)
        .add_systems(Startup, spawn_snake)
        .add_systems(Startup, spawn_food)
        // 游戏循环系统
        .d_systems(
            Update,
            (
                snake_input_system,
                game_over_system,
            ).run_if((resource_equals(GameState { is_game_over: false, ..default() }))
        )
        // 固定时间步长的系统(游戏逻辑)        .add_systems(
            FixedUpdate,
            (
                snake_movement_system,
                snake_eating_system,
                snake_e_growth_system,
            )
            .run_if(resource_equals(GameState { is_game_over: false, ..default() }))
            .th_timestep(SNAKE_SPEED),
        )
        // 渲染系统
        .add_systems(Update, position_translation_system)
        .run();
}

3.4 步骤 3:实现系统 (Systems)

设置

fn setup_camera_and_arena(mut commands: Commands) {
    // 2D 相机
    commands.spawn(Camera2dBundle::default());
    
    // (省略绘制竞技场边界的代码)
}

// 生成蛇头
fn spawn_snake(mut commands: Commands, mut segments: ResMut<SnakeSegments>) {
    let head_entity = commands.spawn((
        SpriteBundle {
            sprite: Sprite { color: Color::GREEN, ..default() },
            transform: Transform {
                scale: Vec3::new(20.0, 20.0, 0.0), // 放大
                ..default()
            },
            ..default()
        },
        SnakeHead { direction: Direction::Up },
        Position { x: 3, y: 3 },
    )).id();
    
    segments.0.push(head__entity);
}

// 生成食物
fn spawn_food(mut commands: Commands) {
    commands.spawn((
        SpriteBundle {            sprite: Sprite { color: Color::RED, ..default() },
            transform: Transform {
                scale: Vec3::new(20.0, 20.0, 0.0),
                translation: Vec3::new(
                    (random::<f32>() * ARENA_WIDTH as f32).floor() * 20.0 - 250.0,
                    (random::<f32>() * ARENA_HEIGHT as f32).floor() * 20.0 - 250.0,
                    0.0
                ),
                ..default()
            },
            ..default()
        },
        Food,
        Position { x: 10, y: 10 }, // 初始位置
    ));
}

游戏逻辑

// 蛇移动
fn snake_movement_system(
    segments: Res<SnakeSegments>,
    mut heads: Query<(&mut Position, &SnakeHead)>,
    mut positions: Query<&mut Position, Without<SnakeHead>>,
) {
    if let Ok((mut head_pos, head)) = heads.get_mut(segments.0[0]) {
        // 移动蛇身 (从后往前)
        for i in (1..segments.0.0.len()).rev() {
            let prev_pos = *positions.get(segments.0[i - 1]).unwrap();
            letut curr_pos = positions.get_mut(segments.0[i]).unwrap();
            *curr_pos = prev_pos;
        }
        
        // 移动蛇头
        match head.direction {
            Direction::Up => head_pos.y += 1,
            Direction::Down => head_pos.y -= 1,
            Direction::Left => head_pos.x -= 1,
            Direction::::Right => head_pos.x += 1,
        }
    }
}

// 蛇吃食物
fn snake_eating_system(   mut commands: Commands,
    mut query_set: ParamSet<(
        Query<&Position, With<SnakeHead>>,
        Query<(Entity, &Position), With<Food>>,
    )>,
    mut state: ResMut<GameState>,
) {
    let head_pos = query_sett.p0().get_single().unwrap().clone();
    
    if let Ok((food_entity, food_pos)) = query_set.1().get_single() {
        if head_pos == *food_pos {
            commands.entity(food_entity).despawn(); // 吃掉食物
            state.score += 1;
            println!("得分: {}", state.score);
            spawn_food(commands); // 重新生成食物
            // (此处应发送事件来增长蛇身)
        }
    }
}

// 游戏结束
fn game_over_system(
    mut state: ResMut<GameState>,
    query: Query<&Position, With<SnakeHead>>,
    segments: Res<SnakeSegments>,
) {
    if let Ok(head_pos) = query.get_single() {
        // 1. 撞墙
        if head_pos.x < 0 || head_pos.x >= ARENA_WIDTH as i32 ||
           head_pos.y < 0 || head_pos.y >= ARENA_HEIGHT as i32 {
            state.is_game_over = true;
        }
        
        // 2. 撞自己 (简化)
        for segment in segments.0.iter().skip(1) {
            // (省略查询 segment 位置的代码)
        }
        
        if state.is_game_over {
            println!("游戏结束! 最终得分: {}", state.score);
        }
    }
}

渲染

// 将逻辑坐标 (Position) 转换为渲染坐标 (Transform)
fn position_translation_system(
    mut query: Query<(&Position, &mut Transform)>,
) {
    let size = 20.0;
    for (pos, mut transform) in query.iter_mut() {
        transform.translation = Vec3::new(
            (pos.x as f32 - ARENA_WIDTH as f32 / 2.0) * size,
            (pos.y as f32 - ARENA_HEIGHT as f32 / 2.0) * size,
            0.0,
        );
    }
}

四、结果分析

4.1 ECS 架构性能

我们对比 ECS 和 OOP 在处理 10,000 个移动物体时的性能。

在这里插入图片描述

分析:ECS 的数据局部性优势使其性能远超 OOP。PhysicsSystem 只关心 Position 和 Velocity,`,CPU 可以高效地预取(Prefetch)这些连续的数据。

4.2 Bevy 编译时间

Bevy 因其“快速编译闻名,这得益于:

  1. 细粒度依赖:Bevy 的插件系统按需编译。
  2. 动态链接:在开发模式下(cargo run),Bevy 默认使用动态链接,极大加快增量编译。
场景 cargo build (冷) cargo run (增量, 改动小)
Bevy 0.13 ~60 s ~1-3 s
Unreal 5 (C++) ~15 min ~30-60 s
Unity (C#) ~15 s ~5-10 s

五、总结与讨论

5.1 核心要点

  • ECS 架构:Bevy 的核心,通过解耦数据(Component)和逻辑(System)实现高性能和高灵活性。
  • 数据驱动:游戏逻辑由 System 查询和操作 Component 来驱动。
  • 数据局部性:ECS 保证了同类组件在内存中连续存储,极大提升了 CPU 缓存命中率。
  • Bev插件App::new().add_plugins(...) 是构建 Bevy 应用的标准方式,易于扩展。
  • Rust性:Rust 的所有权和类型系统保证了 ECS 内部数据访问的线程安全。

5.2 讨论问题

  1. ECS 架构是否适用于所有类型的游戏?(例如,剧情驱动的 RPG)
  2. Bevy 的“快速编译”在在大型项目中是否依然保持优势?
  3. 相比 Godot (GDScript/C#) 或 Unity (C#),Bevy 的主要优势是什么?
  4. Rust 的所有权系统在 ECS 架构中(例如,跨 System 共享数据)是助力还是阻碍?

参考链接

Logo

新一代开源开发者平台 GitCode,通过集成代码托管服务、代码仓库以及可信赖的开源组件库,让开发者可以在云端进行代码托管和开发。旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐