SYS.READ_STREAM | UTF-8
PATH: MicroTiming/05-方块事件.md
WORDS:1,713
EST_TIME:6 MIN

##05 方块事件

BEHeroImg

本篇讨论的是方块事件(Block Event, BE)的实现原理。

基础部分

  • 方块事件的概念与存在意义
  • 方块事件的信息结构与相关元件
  • 方块事件延迟(BED)的通俗认识

进阶部分

  • 方块事件队列与广度优先搜索机制

在Minecraft游戏交互中,为了减少服务器与客户端之间的网络同步压力,游戏会避免把所有复杂方块的巨量数据都在每一刻同步给客户端。

相反,服务端会选择告诉客户端:“这里发生了一个特定的事件”,然后让客户端的方块自己把服务端发生的事情在本地模拟一遍。

方块事件(Block Event) 就是为了这种高效的“通知与模拟”而生的。

当一个方块事件被创建时,它包含且仅包含以下四个核心信息:

  • 位置pos:事件在哪里发生
  • 方块种类block:是哪一种方块在执行这个事件
  • 类型type:是哪种具体的事件动作(对活塞而言,代表伸出或收回)
  • 数据data:执行该事件时附带的必要参数(对活塞而言,代表推动的朝向)

一部分红石元件会产生方块事件,用途各不相同,包括但不限于以下方块:

元件种类
事件类型
附带数据
活塞(伸出0,收回1,瞬推收回2活塞朝向(0~5)
音符盒00
1敲击方向(0~5)

...


当多个方块在同一时间被激活并产生方块事件时,它们会同时执行吗?

Minecraft的世界观告诉我们:游戏中不存在绝对的“同时”。即使是在同一游戏刻内,事件的执行也必须有一个先后顺序。这种由执行顺序的时间差造成的现象,就称为方块事件延迟(Block Event Delay, 简称BED)

下面展示一下方块事件延迟是怎么在游戏中表现的:

方块事件延迟的观测

开关拉杆,我们发现,当改变红石块的位置的时候,旁边活塞推出的数量发生了变化,当红石块靠近压线方块的时候,数量减少了,而当红石块远离压线方块的时候,数量增多了。

你可以把方块事件的执行想象成人们正在排队买票:先来的先执行,后来的后执行

当一个方块事件执行时,它可能会触发后续的新事件。这些新事件会被追加到队列末尾。为了追踪这些事件是由“哪一代”产生的,我们引入深度概念:初始事件深度为 0,它直接产生的事件深度为 1,以此类推,用以刻画事件间的衍生因果链

下面是一个树场0t下吸底座的例子,用于展示方块事件深度是如何在实际设计中应用的:

星河牌下吸

0gt AT:拉杆被拉下

1gt BE 深度0:粘性活塞开始收回

1gt BE 深度1:粘性活塞将灰化土拉回,转向粉转向

1gt BE 深度2:下吸活塞被更新,自检,发现自己被激活,伸出

1gt BE 深度3:充能方块被移除,更新下吸活塞,自检,发现自己应该收回,添加收回的方块事件,发现自己正在伸出,0t发生。

星河牌下吸_gt4

3gt TE:全部方块均到位。

4gt BE 深度0:上下两个普通活塞伸出,泥土的粘性活塞添加方块事件

4gt BE 深度1:普通活塞伸出,更新粘性活塞,回推粘性活塞伸出,底座激活活塞自检。红石块被移除。

4gt BE 深度2:上下两个普通活塞收回,发出更新,更新泥土的粘性活塞,粘性活塞自检,发现自己不应该推出,添加收回的方块事件,下方粘性活塞同理

4gt BE 深度3:红石块粘性活塞和灰化土粘性活塞的0t发生,灰化土瞬间到位。红石块到位,控制下吸活塞充能方块的粘性活塞伸出。

6gt TE:红石粉被转向,充能方块到位,底座完成重置。


在源码层面,方块事件的“排队”本质上是一个广度优先搜索的队列机制。

在游戏的主循环中,方块事件被存放于一个先进先出的队列中。当某一源点(例如被拉杆激活的中心活塞)以NC更新的顺序向周围发出更新是,周围相应的活塞会依次将自己的方块事件压入队列末尾。

执行时,游戏会从队列头部取出一个事件执行:

  1. 取出深度为0的第一个节点执行
  2. 若该更新引发了新的更新,产生里新的方块事件(深度1)会被按更新顺序压入当前队列的最末端。
  3. 游戏继续执行队列中下一个深度为0的节点,直到深度0的事件全部出队,才会开始执行深度1的事件。

这一操作完全等价于广度优先搜索。

这里给出一个例子

BED_BFS

其中长边从+x延伸至-x方向

当拉下拉杆时,中心活塞被激活,并以-x、+x、+z的顺序更新周围三个活塞,依次将它们压入队列。游戏随后按顺序执行-x方向和+x方向的活塞,然后更新+x方向的下一个活塞。此时会添加方块事件并将其压入队列,位于+z方向活塞之后。接着执行+z方向活塞,最后执行+x方向的最后一个活塞。

我们可以将每个活塞抽象为一个节点,把深度为0的活塞视作根节点。需要注意的是,在实际情况中,这个图可能存在多个源点。我们对这个抽象出的"活塞图"进行多源BFS模拟,其中子节点加入队列的顺序就是NC的更新顺序,这个顺序由子节点相对于父节点的方向决定。已被访问过的子节点将不会被重复访问。