游戏开发日志 (其π):Rust、Bevy & ECS

发表于: 7/22/2022

Rust

Bevy

ECS

本来嘛,想着重构要趁早,现在也阶段性完成一部分内容,试着把项目用 Rust 重写,刚好最近了解了 Bevy 这个 Rust 写的使用 ECS 范式的游戏引擎,一举多得岂不美哉?

初见

看看这目录结构,好不整齐 directory

数据使用组件完全打散,在系统中通过查询来获取组件或实体,可以说是彻底解决了我不知道要怎么组织代码才算好的问题,功能也都完全隔离开,可以说是没有耦合的机会了。

最先实现的则是地图网格生成,感觉上既不算是过于复杂,又足够可以踩坑了。

试探

于是果不其然,当前版本的 Bevy ^0.7.0^ 并没有物理系统,于是递归查找物理引擎的插件。 倒还真有 Rapier 这个Rust写的物理引擎,他们给 Bevy 写了一个物理引擎插件提供了物理系统。 但我们的主角却并非其人,而也正是这个插件,使我的心态发生了一点变化。

Heron,基于 Rapier 提供了一套API,看上去也挺对味1,使我选择了它。

由于没有诸如OverlapBox之类的助手函数,我只好按块在地图上生成{碰撞检测体^Sensor},在下一帧中查询物体是否发生重叠。

for i in 0..grid.width {
    for j in 0..grid.height {
        world
            .spawn()
            .insert_bundle::<TransformBundle>(
                Transform::from_translation(
                    grid.get_world_coordinate((i, j).into()).extend(0.0),
                )
                .with_scale(MapGrid::vec_size().extend(0.0))
                .into(),
            )
            .insert(RigidBody::Sensor)
            .insert(CollisionShape::Cuboid {
                half_extends: MapGrid::vec_size().extend(0.0),
                border_radius: None,
            })
            .insert(Collisions::default())
            .insert(Tile {
                grid: (i, j).into(),
            });
    }
}
world.insert_resource(Tiled);

:::info 这里展示了创建实体的方法,在最后生成完砖块后在世界里添加Tiled资源,以在下一次调用时作为判断依据。 :::

查询Tile (存储着砖块的索引),和Collision(存储着碰撞的信息),生成网格数据, 把Entity存起来后面清理用。 这样网格数据作为游戏内有唯一性的数据就作为Resource存储好了,随后切换{状态^State}表示游戏可以开始了2

let mut map_grid = MapGrid {
    width: map.size.x as i32,
    height: map.size.y as i32,
    status: vec![MapStatus::Empty; (map.size.x * map.size.y) as usize],
};
// map is tiled, check tiles
let mut query = world.query::<(Entity, &Tile, &Collisions)>();
let mut tile_entities = Vec::new();
{
    let mut tiles = query.iter(&world);
    for (e, Tile { grid: pos }, collision) in tiles.by_ref() {
        map_grid[[pos.x as usize, pos.y as usize]] = if collision.is_empty() {
            MapStatus::Empty
        } else {
            MapStatus::Obstacle
        };
        tile_entities.push(e);
    }
}

for t in tile_entities {
    world.despawn(t);
}
world.remove_resource::<Tiled>();

world.insert_resource(map_grid);
world
    .get_resource_mut::<State<GameState>>()
    .unwrap()
    .set(GameState::Game)
    .unwrap();

:::info 中间突兀的大括号方法块是为了让其中的world不可变引用早日如土为安,不要妨碍后面的可变引用调用。 这里展示了查询组件和实体,进行操作的方法 :::

递进

这里可以透露一下地图文件的设计,使用json存储,地图上的对象设计成内置、外置素材,方便进一步自定义。

这里值得一提的是Rust可以将 Result<T, E> 的迭代器(不做处理的最终结果将是Vec<Result<T, E>>)收集为Result<Vec<T>, E>,这样极大方便了对数组映射时的错误处理,真是百闻不如一见,令人耳目一新。 这样我就可以放心的返回一个Err,而它最终也会冒泡到闭包外部成为返回值。

// pub fn load_map(path: &str) -> Result<Map, Box<dyn Error>>
let objects = map["objects"]
    .entries()
    .into_iter()
    .map(|obj| match obj.1["type"].as_str() {
        Some(map_object_meta::SpawnPoint::TYPENAME) => {
            let position: Vec2 = (
                obj.1["position"][0].as_f32().ok_or("map parsing err")?,
                obj.1["position"][1].as_f32().ok_or("map parsing err")?,
            )
                .into();
            let player = obj.1["player"].as_i32().ok_or("map parsing err")?;

            Ok(MapObject::SpawnPoint(map_object_meta::SpawnPoint::new(
                position, player,
            )))
        }
        Some(map_object_meta::Struct::TYPENAME) => {
            let position: Vec2 = (
                obj.1["position"][0].as_f32().ok_or("map parsing err")?,
                obj.1["position"][1].as_f32().ok_or("map parsing err")?,
            )
                .into();
            let player = obj.1["player"].as_i32().ok_or("map parsing err")?;
            let group = obj.1["group"].as_i32().ok_or("map parsing err")?;
            let source = obj.1["source"].as_str().ok_or("map parsing err")?;

            Ok(MapObject::Struct(map_object_meta::Struct::new(
                position,
                player,
                group,
                source.to_string(),
            )))
        }
        _ => Err("unknown map object type"),
    }) // Iter<Result<MapObject, &str>>
    .collect::<Result<Vec<_>, _>>()?;

到这为止都还显得十分宁静祥和。

𝆑𝆑𝆑 终章

那么很自然的,我要生成地图了,我要生成地图边界了,我发现没有这样一个碰撞体。

Heron声明提供底层Rapier的功能,我找到这样的碰撞体了,我找到所在的模块了,我发现这个struct没有#[derive(Component)]

ℱ𝒾𝓃.


不过此处折腾也不能说是一无所获,本文也没有任何抱怨的意思,Rust、Bevy可以说未来可期了。不过一无所有的我还是要以完成项目为优先事项,暂别。

Footnotes

  1. 相比 Rapier,使用内置的Vector类型,API也更加简单

  2. 其实不应该写在这里,而应该单独写一个系统用于检查资源是否准备完成切换状态,不过现在姑且还算简单就先这么写了。