深度剖析Minecraft #1 游戏流程

1 游戏流程

1.1 代码层面上的 GameTime 内游戏运算顺序

下图这张树状剖析图,代表了代码层面上,Minecraft 每执行一次 tick() 时的游戏执行顺序:

游戏流程剖析

如读者感兴趣,这里是1.15.2版本的流程分析图

1.2 GameTick

GameTick(gt),也就是游戏刻,或者说游戏里的时间量,是用来衡量电路延迟、生物生存周期等的重要指标。要想明确 GameTick 是什么,就先得给出 GameTick 的定义

作为一个离散的时间量,在游戏的运算过程中一定存在某个时刻,GameTick 这个时间量发生改变,这就是 GameTick 的分界线。在 1.1 节,我们从代码执行顺序的角度列出了游戏的运算顺序。在这个长达 24 条的列表里,我将 GameTick 的分界线的划分在:GameTime 与 DayTime 加一,并给出GameTick的定义:

GameTick 为 x 的定义是:所有执行 World.worldInfo.getGameTime() 得到的返回值为 x 的时刻的集合

于是,我们可以得到事件P发生于GameTick x的定义:

一个事件P发生于 GameTick x 的定义为: 发生事件P时若执行World.worldInfo.getGameTime(),得到的返回值为 x

这样做定义 GameTick 的好处有:

  • 与 TileTick 元件的执行时间相对应。在 GameTick N 触发的 x gt 延迟 TileTick 元件会在 GameTick N + x 执行动作
  • 可以直观地在代码中调用 World.worldInfo.getGameTime() 来确定当前的 GameTick

定义完 GameTick 并确定好分界线后,我们就可以重新排列 1.1 节的树状图,并获得一个 GameTick 内各阶段发生的顺序了:

游戏流程剖析,从 WTU 开始

对于与修改服务端世界相关的操作所在的阶段,即树状图中使用黄色标出的阶段,可归纳得到下常用阶段顺序表

序号 阶段 名称 缩写
1 设置世界时间 World Time Update WTU
2 计划刻 Tile Tick (Next Tick Entry) TT (NTE)
3 随机刻与气候 RandomTick&Climate RTC
4 村庄运算 Village V
5 方块事件 Block Event BE
6 实体 Entity Update EU
7 方块实体 Tile Entity TE
8 玩家操作 Network Update NU
9 刷怪 Spawning S
10 区块卸载 Chunk Unload CU

这个常用阶段顺序表是之后分析最为常用的列表,划重点记笔记!(其实只要记住缩写即可,因为下文会大量使用缩写)

对于之后对精确到一个游戏刻内阶段的分析,我称之为:微观时序分析

1.3 游戏事件执行时刻

这一章节的目的是概述各大部分游戏事件运作的时刻,其中性质的详细描述见后文

在 1.2 里,存在以下几个游戏阶段为抽象的阶段,并未明确声明在其中会发生什么事件。它们是:

  • 计划刻 TT
  • 方块事件 BE
  • 方块实体 TE
  • 玩家操作 NU

下面列一下大部分与之相关的游戏事件

  • 中继器、比较器、红石火把、侦测器的激活与熄灭:TT
  • 按钮、压力板、红石灯、绊线、绊线钩的激活:瞬时;熄灭:TT
  • 拉杆、红石线、铁轨、各类活版栅栏木铁门、漏斗、音符盒、投掷器发射器的激活与熄灭:瞬时
  • 投掷器发射器的工作:TT
  • 命令方块的运作:TT
  • 树叶、流体、脚手架的更新:TT
  • 重力方块判定并创建重力方块实体:TT
  • 活塞推拉的开始:BE
  • 移动中方块的运算:TE
  • 移动中方块的到位:BE(粘性活塞受短脉冲);TE(粘性活塞受长信号)
  • 玩家移动、放置破坏方块、与方块交互:NU

注:瞬时指的是可属于任意阶段,触发即运算,且触发与运算之间无法插入其他操作

实例 自加载型区块加载器伪和平

对于基于在卸载后能加载回自身的区块加载器的伪和平,在重加载时是否存在 1gt 的刷怪空档期是至关重要的,因为这直接与伪和平是否可用 100% 阻止生物刷新相关。完美的伪和平装置是不存在可刷怪空档期的

让我们分析一下基于活塞区块加载器的伪和平:

活塞区块加载器,利用了方块事件可以加载区块的原理,通过在每个gt利用活塞计划方块事件来确保自动保存后能加载回自身区块。

活塞加载器伪和平1 备注:此活塞加载器并非完美设计,但足以应用于本实例分析

这个方案是可以 100% 阻止生物刷新的,也就是不存在 1gt 的刷怪间隔。微观时序分析很简单。先列一下相关的阶段:

序号 阶段 名称 缩写
5 方块事件 Block Event BE
9 刷怪 Spawning S
10 区块卸载 Chunk Unload CU

可看到,在自动保存等引发的区块卸载之后,下一次进行刷怪前,游戏执行了方块事件相关的运算,并在此处加载回了存怪的区块,让怪物容量超过上限,阻止下一次进行刷怪时的生物刷新。因此,这是一个完美的伪和平


如果出于某些原因,活塞区块加载器与存怪装置不在同一个区块,需要使用漏斗加载存怪区块,如下图所示。这样的话这种伪和平装置是否还是完美的?

活塞加载器伪和平2

相关的阶段:

序号 阶段 名称 缩写
5 方块事件 Block Event BE
7 方块实体 Tile Entity TE
9 刷怪 Spawning S
10 区块卸载 Chunk Unload CU

让我们看一下这个设计的区块被卸载时的微观时序

GameTick 阶段 事件
N S 伪和平开启,不刷怪
N CU 伪和平装置的区块被卸载
N + 1 BE 活塞加载器区块加载,漏斗A被加载并立刻被添加至世界参与运算的TE列表
N + 1 TE 漏斗加载存怪装置区块
N + 1 S 刷怪阶段内怪物容量被占满,伪和平开启,不刷怪

因此,这个伪和平设计也能保证 100% 时刻不刷怪,是个完美的伪和平


假如有个小天才嫌一个漏斗太少,非得多串几个漏斗才接到存怪装置区块,那会怎么样?

活塞加载器伪和平3

区块卸载在 GameTick N,活塞加载器自加载在 GameTick N+1 的 BE,三个漏斗依次加载区块使存怪区域在 GameTick N+1 的 TE 被加载?并不是这样的

TE 阶段有个性质:在 TE 阶段内新增的 TE 实体,并不会立即参与运算,而是会先加入一个临时的列表 addedTileEntityList,等到该 TE 阶段运算结束后再统一添加新 TE 实体至参与运算的 TE 列表 loadedTileEntityList 中,也就是说在 GameTick N 新增的 TE 实体要等到 GameTick N+1 的 TE 阶段才能进行运算

因此,这个小天才活塞区块加载器伪和平的微观时序是这样的:

GameTick 阶段 事件
N S 伪和平开启,不刷怪
N CU 伪和平装置的区块被卸载
N + 1 BE 活塞加载器区块加载,漏斗 A 被加载并立刻被添加至世界参与运算的TE列表
N + 1 TE 漏斗 A 加载漏斗 B 所在的区块。漏斗 B 被加载但在 TE 阶段结束时才被添加进参与运算的 TE 列表
N + 1 S 伪和平失效,刷怪
N + 2 TE 漏斗 B 加载漏斗 C 所在的区块。漏斗 C 被加载但在 TE 阶段结束时才被添加进参与运算的 TE 列表
N + 2 S 伪和平失效,刷怪
N + 3 TE 漏斗 C 加载存怪装置区块
N + 3 S 伪和平开启,不刷怪

因此这个伪和平方案在每次被卸载时,足足有 2gt 的刷怪空档期,不是一个完美的伪和平方案