Bevy migration is basically completed!
编辑器 @宏楼 Koiro
现在编辑器可以进行方块编辑、选区移动、加载 & 保存存档的功能。
写 gizmos 事遇到一些 bevy Gizmos 的一些问题: https://github.com/bevyengine/bevy/issues/13027 https://github.com/bevyengine/bevy/issues/13028
补:同时编辑器也做了更好的存档系统
更好的方块注册系统和序列化系统 @宏楼 Koiro
方块注册系统
系统本身并不复杂,核心实现大概是这样,我们确保每个方块有一个唯一的 registry name,利用 registry name 和对应的 Block Builder 的行为生成方块:
lazy_static! {
pub static ref BLOCK_REGISTRY: Mutex<BlockRegistry> = Mutex::new(BlockRegistry::default());
}
pub trait BlockBuilder {
fn spawn(&self, world: &mut World, spawn_cmd: &SpawnBlockCmd) -> Result<()>;
}
#[derive(Default)]
pub struct BlockRegistry {
map: HashMap<String, Box<dyn BlockBuilder + Send>>,
}
impl BlockRegistry {
pub fn register_block<T: BlockBuilder + Send + 'static>(&mut self, reg_name: &str, builder: T) {
//...
}
pub fn get_block_builder(&self, reg_name: &str) -> Option<&Box<dyn BlockBuilder + Send>> {
//...
}
//...
}
因为生成方块时 world 所有权冲突问题,所以方块注册表并没有使用 Bevy 的 Resource 和 Non-Send Resource,而是使用 lazy_static.
序列化系统
序列化和反序列化行为用 DeserializeManager 和 SerializeManager 管理。分别在保存和加载时调用。序列化时,场景会被序列化成 toml 中的 Table. 根据需要保存在磁盘或内存中。
#[derive(Default, Resource, Clone)]
pub struct CatDeserializerManager {
pub map: HashMap<Stage, HashMap<String, SystemId<DeserializeContext, Result<()>>>>,
}
#[derive(Default, Resource, Clone)]
pub struct CatSerializerManager {
pub map: HashMap<String, SystemId<(), Result<Table>>>,
}
同时为加载做了阶段,每个阶段会间隔几帧执行。
#[derive(Default, Clone, PartialEq, Eq, Reflect, Hash)]
pub enum Stage {
Start,
Pre,
#[default]
Normal,
Post,
End,
}
Action 系统 @Rasp
@Koiro 补充:命令模式之所以叫 Action 是因为 Command 被 bevy 用了
为了让玩家的交互和地图上发生的事情可以被撤销,我们要先把命令模式做完。
用几个栈的组合可以实现动作和反动作(也就是撤销)操作的存储和执行,这个比较简单。比较有趣的问题是,这些栈里面要存什么。
由于我们在使用 Rust,我决定用 Trait 对象实现所有 Action 通用的功能。
pub trait Action: Send + Sync {
fn r#do(&self, world: &mut World);
fn undo(&self, world: &mut World);
fn name(&self) -> String;
}
可以注意到,两个核心方法的签名都是对自身的不可变借用。这实际上是一个设计失误,不可变借用自身导致在动作和反动作之间保存状态变得不可能。这会让类似“移除一个方块并在撤销时还原这个方块”这类的操作无法实现,因为我们无法保存那个“被移除的方块”。但是如果使用了可变借用,又会遇到一些跟 Bevy 相关的所有权问题。
一个临时解决方案是使用 Mutex:
pub trait ActionMut: Send + Sync {
fn r#do(&mut self, world: &mut World);
fn undo(&mut self, world: &mut World);
}
impl<AM: ActionMut> Action for (String, Mutex<AM>) {
fn r#do(&self, world: &mut World) {
let mut inner = self.1.lock().unwrap();
inner.r#do(world);
}
fn undo(&self, world: &mut World) {
let mut inner = self.1.lock().unwrap();
inner.undo(world);
}
fn name(&self) -> String {
format!("{}", self.0)
}
}
这样,我们就可以为需要可变借用自身的 Action 实现对应的 Trait 并且不会破坏先前实现的代码。
方块的合并与反合并 @Rasp
之前在 Unity 里面实现这个功能的时候因为没有认真想过怎么做撤销,用了一个非常草率的解决方案。
这个草率的解决方案基于一个假设:方块集合不能从中间断开。意思是,如果两个方块粘在一起,那么游戏中没有正常的操作能够把它们分开。相信你已经看出问题了,撤回操作作为合并操作的反操作,会破坏这个假设,导致旧有的解决方案没法正常使用。
新的解决方案基于一个基本原理:我们要保存方块、保存它跟谁粘在一起,更重要的是:保存这个方块所有的邻居信息。这样,通过对比当前邻居信息和之前邻居的差异,可以分辨出到底发生了什么:连接还是断开,又或者是增加了新的方块。
#[derive(Component, Reflect, Default)]
pub struct BlockUnionSet {
// core
neighbors: HashMap<Entity, [Option<Neighbor>; 4]>,
// polymer
polymer_id: HashMap<Entity, u64>,
polymers: HashMap<u64, Vec<Entity>>,
empty_polyer_id: Vec<u64>,
// edge
#[reflect(ignore)]
edges: HashMap<String, BTreeSet<Edge>>,
}
比较邻居信息并且更新方块集合的代码看起来像这样:
for (neighbor_in_set, neighbor_on_map) in neighbors {
match (neighbor_in_set, neighbor_on_map) {
(None, None) => { /* nop */ }
(None, Some(on_map)) => {
// in this case, we have to check the new neighbor is the same or not
if block.reg_name == blocks[&on_map].reg_name {
// if they are, we merge
self.merge_polymer(entity, on_map);
}
}
(Some(in_set), None) => {
// in this case, we have to check our old neighbor
if in_set.0 {
// if our old neighbor and entity are the same, it means we lost a neighbor
// we will request a deep refresh for this entity and it's connected blocks
deep_refresh_list.push(entity);
}
}
(Some(in_set), Some(on_map)) => {
// in this case, we have to consider our old neighbor is the same with us or not
if in_set.0 {
// if they were and it's not now
if blocks[&on_map].reg_name != block.reg_name {
// we lost a neighbor
deep_refresh_list.push(entity);
deep_refresh_list.push(on_map);
}
} else {
// if they were not but they are now
if blocks[&on_map].reg_name == block.reg_name {
// we have a new neighbor
deep_refresh_list.push(entity);
deep_refresh_list.push(on_map);
}
}
}
}
}
更令人欣喜的是,存储了邻居相关的信息之后,后面方块集合边缘的寻找和判断也变得简单了。
玩家移动 @Rasp
在方块合并和反合并工作的基础上,我们可以把玩家移动的部分做完。
由于 Tilemap 部分完成了很多工作,留给我的部分不多。
首先我们要为单个方块编写移动相关的 Action,实现过程也很简单。在有了这个功能之后,我们通过先判断玩家的移动方向是否有障碍,如果有就判断障碍物是否可以移动。最后把所有需要移动的方块对应的移动 Action 都推进栈中等待执行即可。
边界改变交互 @宏楼 Koiro
边界改变的逻辑由 Rasp 完成了,我仅需要做边界改变的交互与视觉设计。我们使用了虚拟光标。目前的效果是临时的,借此机会熟悉了些 bevy 中使用 shader.
Leave a comment
Log in with itch.io to leave a comment.