本篇讨论的是方块事件 (Block Event, BE) 的实现原理。
基础部分
进阶部分
在 Minecraft 游戏交互中,为了减少服务器与客户端之间的网络同步压力,游戏会避免把所有复杂方块的巨量数据都在每一刻同步给客户端。
相反,服务端会选择告诉客户端:“这里发生了一个特定的事件”,然后让客户端的方块自己把服务端发生的事情在本地模拟一遍。
方块事件 (Block Event) 就是为了这种高效的 “通知与模拟” 而生的。
当一个方块事件被创建时,它包含且仅包含以下四个核心信息:
pos:事件在哪里发生block:是哪一种方块在执行这个事件type:是哪种具体的事件动作(对活塞而言,代表伸出或收回)data:执行该事件时附带的必要参数(对活塞而言,代表推动的朝向)一部分红石元件会产生方块事件,用途各不相同,包括但不限于以下方块:
...
当多个方块在同一时间被激活并产生方块事件时,它们会同时执行吗?
Minecraft 的世界观告诉我们:游戏中不存在绝对的 “同时”。即使是在同一游戏刻内,事件的执行也必须有一个先后顺序。这种由执行顺序的时间差造成的现象,就称为方块事件延迟 (Block Event Delay, 简称 BED)
下面展示一下方块事件延迟是怎么在游戏中表现的:
开关拉杆,我们发现,当改变红石块的位置的时候,旁边活塞推出的数量发生了变化,当红石块靠近压线方块的时候,数量减少了,而当红石块远离压线方块的时候,数量增多了。
你可以把方块事件的执行想象成人们正在排队买票:先来的先执行,后来的后执行。
当一个方块事件执行时,它可能会触发后续的新事件。这些新事件会被追加到队列末尾。为了追踪这些事件是由 “哪一代” 产生的,我们引入深度概念:初始事件深度为 0,它直接产生的事件深度为 1,以此类推,用以刻画事件间的衍生因果链
下面是一个树场 0t 下吸底座的例子,用于展示方块事件深度是如何在实际设计中应用的:
0gt AT:拉杆被拉下
1gt BE 深度 0:粘性活塞开始收回
1gt BE 深度 1:粘性活塞将灰化土拉回,转向粉转向
1gt BE 深度 2:下吸活塞被更新,自检,发现自己被激活,伸出
1gt BE 深度 3:充能方块被移除,更新下吸活塞,自检,发现自己应该收回,添加收回的方块事件,发现自己正在伸出,0t 发生。
3gt TE:全部方块均到位。
4gt BE 深度 0:上下两个普通活塞伸出,泥土的粘性活塞添加方块事件
4gt BE 深度 1:普通活塞伸出,更新粘性活塞,回推粘性活塞伸出,底座激活活塞自检。红石块被移除。
4gt BE 深度 2:上下两个普通活塞收回,发出更新,更新泥土的粘性活塞,粘性活塞自检,发现自己不应该推出,添加收回的方块事件,下方粘性活塞同理
4gt BE 深度 3:红石块粘性活塞和灰化土粘性活塞的 0t 发生,灰化土瞬间到位。红石块到位,控制下吸活塞充能方块的粘性活塞伸出。
6gt TE:红石粉被转向,充能方块到位,底座完成重置。
在源码层面,方块事件的 “排队” 本质上是一个广度优先搜索的队列机制。
在游戏的主循环中,方块事件被存放于一个先进先出的队列中。当某一源点(例如被拉杆激活的中心活塞)以 NC 更新的顺序向周围发出更新是,周围相应的活塞会依次将自己的方块事件压入队列末尾。
执行时,游戏会从队列头部取出一个事件执行:
0的第一个节点执行1)会被按更新顺序压入当前队列的最末端。0的节点,直到深度0的事件全部出队,才会开始执行深度1的事件。这一操作完全等价于广度优先搜索。
这里给出一个例子
其中长边从 + x 延伸至 - x 方向
当拉下拉杆时,中心活塞被激活,并以 - x、+x、+z 的顺序更新周围三个活塞,依次将它们压入队列。游戏随后按顺序执行 - x 方向和 + x 方向的活塞,然后更新 + x 方向的下一个活塞。此时会添加方块事件并将其压入队列,位于 + z 方向活塞之后。接着执行 + z 方向活塞,最后执行 + x 方向的最后一个活塞。
我们可以将每个活塞抽象为一个节点,把深度为 0 的活塞视作根节点。需要注意的是,在实际情况中,这个图可能存在多个源点。我们对这个抽象出的 "活塞图" 进行多源 BFS 模拟,其中子节点加入队列的顺序就是 NC 的更新顺序,这个顺序由子节点相对于父节点的方向决定。已被访问过的子节点将不会被重复访问。