Minecraft服务器源码跟踪(2).运行服务器
运行服务器
GameTick概念
众所周知,在mc中存在时间,一切事件会随着时间流逝相继发生。在现实世界,时间往往是一个连续的概念,也就是任何一个瞬间都可以成为一个时间点。而在计算机中,我们很难描述一个绝对连续的时间去按序执行大量的事件。mc的解决办法是,将时间像手表的秒针一样分片,每过一段时间就会走1刻(1tick)。而每一tick就是游戏的最小时间单位,每1tick都会去做重复的工作去处理游戏的变化,这就是通常讲的game tick,也简称为gt。
由于在当前讨论环境下,不会存在其他时间单位,所以后文中的gt简称为tick或t
此处引出gt的概念是因为在mc中,普遍认为存在游戏刻(gametick)以及红石刻(redstone tick)这两种不同的时间单位,而gt则是更加贴近游戏底层的单位
runServer
入口: net.minecraft.server.MinecraftServer#runServer
此方法为运行服务器的主逻辑,控制整个服务器运行的生命周期。整体来看有如下几部分
最重要的方法就是this.tickServer()
,玩家能感知到的所有游戏内的逻辑都在此方法中。我们后续的分析基本上都是在分析该方法。
在这之前,还有一些点可以注意一下
过载判断
这是服务器判断是否过载以及相关的处理代码。接下来逐行分析
long l;
此处的l即为服务器设置的每tick的ms数。第一个if条件其实默认是不会触发的,因为正常运行的情况下,都是1tick保证50ms。除非使用/tick
指令调整速度。(才发现1.20.3开始tick指令从carpet合并到了原版,还以为我看错了)在else逻辑中,存在如下几个常量:
OVERLOADED_THRESHOLE_NANOS
: 1sOVERLOADED_WARNING_INTERVAL_NANOS
:10sNANOSECONDS_PER_MILLISECOND
:1ms
代码逻辑并不复杂,简单点可以归纳为如下几点:
- l 为预设的每tick的时间
- m为当前的时间 - nextTickTime。注意,此处nextTick还没更新,所以此处的nextTickTime其实是这一次循环对应tick的时间。换而言之,m就是循环中的这1tick延迟了多少时间
- 如果延迟超出了1s + 20tick = 2s,并且本tick具体上次过载告警的tick时间已经超过了 10s + 100tick = 15s,那就需要处理过载。
- 计算延迟了多少tick
- 在控制台中打warning日志
(这行日志如果你是服主相信你经常会看到) - 更新nextTickTime为实际时间,重设告警时间(这是为了让控制台不要产生告警风暴)
至此,nextTickTime已经被修正,服务器将进行实际的tick操作了。
tickServer
入口:net.minecraft.server.MinecraftServer#tickServer
参数
bl
:其实就是一个表达式的值,表示游戏是否在加速。正常状态为false
haveTime
: 这是MinecraftServer的方法,直接看源码:
返回当前是否有任务在执行,或者当前时间是否还没有到达下一tick或者任务延迟的最大时间
逻辑
- tick count + 1
- tickManager执行冻结相关的判断
- 执行整个tick中的各种流程(tick children)
- 设置player的一些状态,在Server status
- 每6000tick(30s) 触发一次自动保存
- 统计平均的tps
MinecraftServer.tickChildren
- 执行commandFuncions
- 遍历server中的各个维度,每过1s将维度的时间同步给玩家
- 对于每个维度,执行对应维度的tick
- 对当前服务器的连接执行tick
- 对玩家列表执行tick
- 其余流程
比较重要的是各个维度的tick,接下来详细讲
ServerLevel.tick
世界(以下称level)的tick执行流程较为复杂,涉及点也很多,还是从概览到细节逐个分析。
- 设置level的handlingTick为true PS.这个属性除了标识状态以外,还会影响活塞行为
- 如果服务器处于运行中,就更新世界边界以及天气状态。(如果服务器被
/tick freeze
了就跳过) - 判断是否跳过夜晚,检查配置以及玩家是否充足睡眠,如果跳过夜晚,清除下雨和打雷的天气
- 更新时间相关事件
- level的游戏时间 + 1
- level中的定时事件执行tick,传入新的游戏时间
- 如果游戏没有关掉
daylight
的游戏规则,游戏中的白天时间 + 1(影响日夜循环)PS.终于看到实际游戏里能用上的东西了
- 如果没有冻结,依次执行 blockTicks.tick(方块刻), fluidTicks.tick(流体刻),raids.tick(袭击刻),chunkSource.tick(区块刻),runBlockEvents(方块事件)
在这些阶段结束后,设置handlingTick为false,表示不再处理tick。 - 判断世界中是否存在玩家或者强加载区块。如果存在,那世界的emptyTime设为0。
- 在末地维度,上述条件满足,或者自不满足开始300tick内会执行末影龙战斗tick
- 对entityTickList中的实体进行检查,如果有需要清除的实体将会将其清除,第一阶段清除和gamerule或者配置文件不符的实体
- 对entityTickList中的实体检查,对在tick运算距离内的实体判断骑乘状态,如果被骑乘的实体被移除或未处于被骑乘状态,那么当前实体应当停止骑乘状态
- 执行tickNonPassenger,对于处于非骑乘状态的实体进行运算
- 执行方块实体运算
this.tickBlockEntites()