发表于: 7/22/2022
本来嘛,想着重构要趁早,现在也阶段性完成一部分内容,试着把项目用 Rust 重写,刚好最近了解了 Bevy 这个 Rust 写的使用 ECS 范式的游戏引擎,一举多得岂不美哉?
初见
看看这目录结构,好不整齐
数据使用组件完全打散,在系统中通过查询来获取组件或实体,可以说是彻底解决了我不知道要怎么组织代码才算好的问题,功能也都完全隔离开,可以说是没有耦合的机会了。
最先实现的则是地图网格生成,感觉上既不算是过于复杂,又足够可以踩坑了。
试探
于是果不其然,当前版本的 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可以说未来可期了。不过一无所有的我还是要以完成项目为优先事项,暂别。