Bevy migration is basically completed!


编辑器 @宏楼 Koiro

现在编辑器可以进行方块编辑、选区移动、加载 & 保存存档的功能。

Save Edit

写 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.

Run

Get Cat with blocks whose edges can be manipulated

Leave a comment

Log in with itch.io to leave a comment.