惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

腾讯CDC
IT之家
IT之家
有赞技术团队
有赞技术团队
WordPress大学
WordPress大学
Apple Machine Learning Research
Apple Machine Learning Research
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
人人都是产品经理
人人都是产品经理
The Cloudflare Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 【当耐特】
V
V2EX
Last Week in AI
Last Week in AI
H
Help Net Security
The GitHub Blog
The GitHub Blog
S
SegmentFault 最新的问题
F
Fortinet All Blogs
I
InfoQ
宝玉的分享
宝玉的分享
A
About on SuperTechFans
MongoDB | Blog
MongoDB | Blog
Microsoft Azure Blog
Microsoft Azure Blog
Blog — PlanetScale
Blog — PlanetScale
B
Blog

机核

游戏性能旗舰最强之选,一加 Ace 6 至尊版国补到手价2999元起 6元钱自己更换电动车刹车线 《生化危机9:安魂曲》编剧Haris Orkin专访 摸金游戏?音乐游戏!暗区新赛季这把能弹的琴有何来历? 好评国产武侠SRPG《息风谷战略》免费DLC现已推出 | 机核 GCORES 碎片 《生化危机:安魂曲》将于5月实装首个免费更新内容 | 机核 GCORES 新锐东方游戏,谱写世界新章! 沉浸式恋爱视觉小说游戏《心象演算》免费试玩版现已正式上线 | 机核 GCORES 互动影游《代号三国:龙起》上线!穿越三国与曹操并肩、与佳人同行、与权谋博弈! 《老头收集梦想生活》,游戏酒桌会6,录音笔VOL.689 | 机核 GCORES 破界·共生——《白日梦:无限世界》五大核心游戏特点解析 价格已到史低,锐龙5 9600X/锐龙7 9700X正适合抄底 时间循环之旅即刻启程!《归环》一周目测试今日开启 热门在线韩游变魂游,洛奇衍生作能否打破“花瓶”魔咒? LG UltraGear evo 全新高端显示器系列:当“5K”遇见“AI”,不止强大,更懂热爱 反套路三国互动影游《代号三国:龙起》今日上线! 愿望单登记人数突破10万!备受瞩目的“女儿养成游戏” 《梦幻魔法公主》今日于Steam平台上线!限时八折优惠中 《黑神话:悟空》全球音乐会2026巡演将于4月29日12时开票 | 机核 GCORES “Snowguelike”生存肉鸽挖矿新作《蛙穿雪境》公布发售日期,5月7日正式上线 | 机核 GCORES 重塑移动办公、AI创作新境!全新华硕灵耀Air系列、ProArt 骁龙版震撼首发,创芯未来 均分88:《Saros》媒体评分汇总 | 机核 GCORES 上海烛龙公布合作遗迹探险游戏《吉时已到》首支预告片 | 机核 GCORES 首个独立游戏《萝薇日记》已上线Steam! 烛龙新IP《吉时已到》首曝,打造国内首款中式合作遗迹探险游戏 喜加一:《暗黑破坏神Ⅳ》国服现已开启限时免费领取本体活动 | 机核 GCORES 新版《生化危机》电影定于9月18日上映,官方网站现已上线 | 机核 GCORES 《冲就完事模拟器2》“星球大战”联动DLC正式宣布 | 机核 GCORES SteamController将于5月4日发售,售价99美元 | 机核 GCORES 事已至此,内存这么用也算省钱了,“2+1”非对称双通道应用实测
编辑器开发、Bevy 移植基本完成 etc. #002
宏楼 · 2024-06-03 · via 机核

因为本篇涉及较大量代码,又因为机核文章没有代码块,因此阅读体验不佳。 若你有阅读代码的兴趣,可以到 itch.io 阅读本日志:点击跳转

编辑器 @宏楼 Koiro

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

1 / 2

更好的方块注册系统和序列化系统 @宏楼 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

边界改变的逻辑由 Asepirin 完成了,我仅需要做边界改变的交互与视觉设计。我们使用了虚拟光标。目前的效果是临时的,借此机会熟悉了些 bevy 中使用 shader.