Deep Dark ECS

发表于: 11/29/2023

ECS

游戏开发

有了上一篇的铺垫,在这一篇里我们将聚焦于复杂一些的逻辑。

本文将基于 Bevy 演示代码。

首先来思考一个“简单”的问题:实体的创建。

fn spawn_xxx(mut commands: Command){}
    commands.spawn((
        XXXComponent,
        // ...
    ));
}

如上是一个简单创建实体的过程,spawn 方法可以接受一个组件组成的元组,其中的组件将被附加到创建的实体上。 很简单地,我们可以把所有用到的组件一股脑写上去。比如:

(Sprite, Name, Life, Movable, Player, Character, Transform {x: 0.0, y: 0.0, z: 0.0})

:::info 该示例仅用作展示一些可能的组件的类型,实际初始化的结构体包含更多的字段。 :::

但是这种简单的做法并不十分美好,这个spawn_xxx的系统仅能生成一种实体,而且无从定制一些初始化参数:生成位置、不同阵营实体的区分、特定的强化等等。而且在省略的 Sprite 中,很显然我们还需要加载{Asset^资产},来让角色有面目以示人。 我们需要更多参数!

世界出现了参差,这意味这我们一定把一些隐藏的东西给忽略了。

容易发现,我们的需求实际是两部分:创建实体、实体的初始化。这在平常的范式中通常是实例化一个预制体,于是我们得到了这个实体的引用,在此之上进行我们需要的修改,就是初始化过程了。但在这里我们做不到这一点:spawn_xxx 的运行独立于其他系统,如果我们不想在不同的逻辑里插入一大堆重复的 spawn((...)) 的话。

那么你一定想问了:你不是说系统可以作用到特定的实体的组件上吗,我们设计一个 “Initialize” 组件,只需要创建一个包含必要初始化信息的实体,让这个 spawn_xxx 成为 initialize_xxx 补充缺少的组件不就好了?

Bravo,这就是我们要做的:

fn spawner(mut commands: Command){
    commands.spawn((Initialize {
        position: ...
    }, Team(1), ...))
}
fn initializer( // Entity 本质为一个自增id,Initialize是组件,查询时获取其引用,Character则是(组件)筛选条件,并不读取其数据
    things_to_initialize: Query<(Entity, &Initialize), With<Character>>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
    mut commands: Command
) {
    things_to_initialize.for_each(|(entity, initialize_infomation)|{
        commands.entity(entity).insert((
            MaterialMesh2dBundle {
                mesh: meshes.add(shape::Circle::new(32.).into()).into(),
                material: materials.add(ColorMaterial::from(Color::ALICE_BLUE)),
                transform: Transform::from_translation(initialize_infomation.position),
                ..default()
            },
            ...
        )).remove::<Initialize>()
    })
}

通过查询组件(Initialize, Character)的组合,我们得到了需要初始化的实体,以一套通用的逻辑通过 insert 补上我们需要的组件,然后删除 Initialize 就完成了初始化,实体不再符合查询条件。

而当我们想要以一个通用的逻辑来生成一类不同的实体时,这样做更是必须的:

比如 “武器” 可以装载不同 “弹药” ,发射时就需要生成不同的子弹。 我们就需要一个 Bullet 组件来承载一些公共参数:速度、散布、伤害等等,每种弹药一个组件作为区分:霰弹(弹头数量)、冲锋枪弹、狙击步枪弹等等。

当武器开火时,发送一个事件到 spawner,事件包含弹药等必要信息。

fn spawner(mut event: EventReader<BulletSpawnEvent>, ...) {
    // ... 读取事件
        commands.spawn((event.bullet, Initialize, event.bullet_type));
}

fn shotgun_initializer(shotgun_bullets: Query<(Entity, &Bullet, &BulletType, &Initialize)>, ...) { ... }
// ... 其他子弹初始化

:::info 由于 Bevy 目前 ^0.12.0 尚未有足够方便好用的预制体一类的功能,这或许也是目前最“舒服”的重复创建过程了。 :::