在ECS中,我们可以想象Component就是数据库表中的项,而Entity,则是一行记录。
Component使我们能够将数据附加到Entity中。
实际上,一个单独的Entity并没有什么用,它只是代表了一行记录的Id,也就是游戏世界中一些创建的东西的Id。
那么,哪些是Component呢?
- 游戏窗口,也就是
Window,就是一个Component。 - 相机
Camera也是一个Component。
而如果我们需要修改一个Entity的值,我们就需要通过System来查询到这条记录,然后修改。
定义组件
通过派生宏的方式,来定义组件,这里我们定义几个组件:
//枚举可以是Component
#[derive(Component)]
enum Ship {
Boat,
Clipper,
}
//元组可以是Component
#[derive(Component)]
struct Health(f32);
//结构体可以是Component
#[derive(Component)]
struct Position {
x: f32,
y: f32,
}
//一个什么数据都没有的结构体,也可以是Component
#[derive(Component)]
struct Player;
#[derive(Component)]
struct Enemy;
关于最后两个Component,Player和Enemy,是Bevy中一种典型的用法,即标记。他会标记我们添加的Entity方便后续我们查询到该Entity,并访问相关的Component。
使用Component
我们先看看,如果在Bevy中,使用这些Component:
//定义一个生成这些Component的System
fn setup_entities(mut commands: Commands) {
commands.spawn((Health(100.0), Player, Position { x: 100f32, y: 200f32 }));
commands.spawn((Health(72.0), Enemy, Position { x: 20f32, y: 60f32 }));
}
//一定不要忘了添加到App
.add_systems(Startup, setup_entities)
在创建这些Component时,有两个点需要注意:
- 每个Entity生成的组件,类型不能重复(
commands.spawn((Health(100.0), Health(40.0)) //不可以这样)。 - 如果想在后续再出入一条同类型,那么之前类型的数据就会覆盖。
行与列
现在,我们用行与列的概念想象一下上述代码:
| Entity Id | Health | Player | Position | Enemy |
|---|---|---|---|---|
| 1 | 100.0 | √ | (100, 200) | |
| 2 | 72 | (20,60) | √ |
在Bevy中,Component被存储为许多单独的连续数组。
每个数组都有一种类型的组件。Entity实际上是每个不同数组中其所有组件的索引。
例如,我们有一个Health和Position组件的数组:
Health: [Health(100.0), Health(72.0)]
Position: [Position { x: 100f32, y: 200f32 }, Position { x: 20f32, y: 60f32 }]
在内存中,它会存储成:
Entity 0 → [HealthArray[0], PositionArray[0]]
Entity 1 → [HealthArray[1], PositionArray[1]]
如果Entity的索引是0,那么我们就能很快找到数据:Health(100.0),Position { x: 100f32, y: 200f32 }。
这就是为什么我们只能拥有每种类型的一个组件的原因。我们只能在插槽中找到实体索引相匹配的一个组件。
这样做就是为了提高性能——尽量从CPU的缓存中获取数据。
必须组件
我们稍微对Player做一下修改:
#[derive(Component)]
#[require(Health)] //声明了一个必须组件
struct Player;
当我们创建一个组件的时候,如果我们需要有另外一个或者多个组件必须一起创建,我们可以使用#[require(xxx,xxx)]来声明必须组件。
如果在创建的时候已经有了该必须组件,那么Bevy会自动使用这个组件,如果没有,则会自动创建一个必须组件。所以,每一个必须组件必须实现Default trait。
#[derive(Component)]
struct Health(f32);
impl Default for Health {
fn default() -> Self {
Self(100.0)
}
}
当我们添加到Bevy中时:
commands.spawn((Player, Position { x: 100f32, y: 200f32 }));
Bevy会自动创建一个Health(100.0)的组件,生成一条记录。
总结
组件就像游戏世界的乐高积木,每个零件都让实体更鲜活!试着用不同组件组合出飞船、英雄或魔法道具,Bevy的灵活架构会让创意轻松落地~
欢迎在评论区留言。