深度剖析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 内各阶段发生的顺序了:
对于与修改服务端世界相关的操作所在的阶段,即树状图中使用黄色标出的阶段,可归纳得到下常用阶段顺序表:
序号 | 阶段 | 名称 | 缩写 |
---|---|---|---|
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利用活塞计划方块事件来确保自动保存后能加载回自身区块。
备注:此活塞加载器并非完美设计,但足以应用于本实例分析
这个方案是可以 100% 阻止生物刷新的,也就是不存在 1gt 的刷怪间隔。微观时序分析很简单。先列一下相关的阶段:
序号 | 阶段 | 名称 | 缩写 |
---|---|---|---|
5 | 方块事件 | Block Event | BE |
9 | 刷怪 | Spawning | S |
10 | 区块卸载 | Chunk Unload | CU |
可看到,在自动保存等引发的区块卸载之后,下一次进行刷怪前,游戏执行了方块事件相关的运算,并在此处加载回了存怪的区块,让怪物容量超过上限,阻止下一次进行刷怪时的生物刷新。因此,这是一个完美的伪和平
如果出于某些原因,活塞区块加载器与存怪装置不在同一个区块,需要使用漏斗加载存怪区块,如下图所示。这样的话这种伪和平装置是否还是完美的?
相关的阶段:
序号 | 阶段 | 名称 | 缩写 |
---|---|---|---|
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% 时刻不刷怪,是个完美的伪和平
假如有个小天才嫌一个漏斗太少,非得多串几个漏斗才接到存怪装置区块,那会怎么样?
区块卸载在 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 的刷怪空档期,不是一个完美的伪和平方案