深度剖析Minecraft #1 游戏流程


  • TIS成员

    1. 游戏流程

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

    来吧顺着代码DFS

    让我们从Minecraft服务端最底层的代码 —— MinecraftServer.tick() 开始。这个 tick 函数包含了两个与游戏阶段相关的函数:

    // 主运算部分
    this.updateTimeLightAndEntities(hasTimeLeft) 
    // 900gt一次的自动保存
    this.saveAllWorlds(true);
    

    随后我们进入到 updateTimeLightAndEntities() 中,在这里的精简代码(删去无关代码以及过长参数)如下

    // MinecraftServer.tick()
    // 成就命令相关
    this.getFunctionManager().tick(); 
    // 循环每一个世界:
    for (WorldServer worldserver : this.getWorlds())
    {
        // 同步玩家的客户端时间
        this.playerList.sendPacketToAllPlayersInDimension(new SPacketTimeUpdate); 
        // 运算游戏内容
        worldserver.tick(hasTimeLeft); 
        // 运算实体
        worldserver.tickEntities(); 
        // 传输至客户端的实体信息相关
        worldserver.getEntityTracker().tick(); 
    }
    // 网络玩家信息运算
    this.getNetworkSystem().tick(); 
    

    让我们进入 worldserver.tick(hasTimeLeft),终于开始要有具体的游戏阶段了。精简代码如下

    // worldserver.tick(hasTimeLeft)
    // 极限模式下难度锁困难判断
    if (this.getWorldInfo().isHardcore() && this.getDifficulty() != EnumDifficulty.HARD)
    {
        this.getWorldInfo().setDifficulty(EnumDifficulty.HARD);
    }
    // 群系生成相关
    this.chunkProvider.getChunkGenerator().getBiomeProvider().tick();
    if (this.areAllPlayersAsleep())
    {
        if (this.getGameRules().getBoolean("doDaylightCycle"))
        {
            long i = this.worldInfo.getDayTime() + 24000L;
            this.worldInfo.setDayTime(i - i % 24000L);
        }
        this.wakeAllPlayers();
    }
    // 刷怪
    this.entitySpawner.findChunksForSpawning();
    this.getChunkProvider().spawnMobs();
    // 区块卸载
    this.chunkProvider.tick(hasTimeLeft);
    // 天空光衰减计算
    int j = this.calculateSkylightSubtracted(1.0F);
    if (j != this.getSkylightSubtracted())
    {
        this.setSkylightSubtracted(j);
    }
    // 设置GameTime与Daytime
    this.worldInfo.setGameTime(this.worldInfo.getGameTime() + 1L);
    if (this.getGameRules().getBoolean("doDaylightCycle"))
    {
        this.worldInfo.setDayTime(this.worldInfo.getDayTime() 
    }
    // 计划刻 Tile Tick (Next Tick Entry)
    this.tickPending();
    // 各类方块特性的运算,将进入继续分析
    this.tickBlocks();
    // 玩家加载的区块列表更新,并发送更新客户端世界信息的包
    this.playerChunkMap.tick();
    // 村庄运算
    this.villageCollection.tick();
    // 僵尸围城
    this.villageSiege.tick();
    // 地狱门缓存清空
    this.worldTeleporter.tick(this.getGameTime());
    // 方块事件 Block Event
    this.sendQueuedBlockEvents();
    

    好的,我们再来看看 this.tickBlocks(); 这个各类方块相关的事件运算。由于有部分事件已经涉及到了底层实现而没有用函数封装,对于这部分事件我用前缀"·"表示

    // this.tickBlocks()
    // 随机检查并更新玩家附近的亮度
    this.playerCheckLight();
    // 循环每个玩家周围的区块进行:
    for (Iterator<Chunk> iterator = this.playerChunkMap.getChunkIterator(); iterator.hasNext(); this.profiler.endSection())
    {
        Chunk chunk = iterator.next();
        // 客户端的亮度检查
        chunk.enqueueRelightChecks();
        // 天空光的计算与将新增的 Tile Entity 储存至区块内
        chunk.tick(false);
        // 生成雷电,以及骷髅马陷阱
        ·thunder
        // 下雪与结冰
        ·iceandsnow
        // 随机刻
        ·randomTick
    }
    

    终于跟踪分析完worldserver.tick(hasTimeLeft);这个运算游戏内容,然后就是worldserver.tickEntities();

    // worldserver.tickEntities()
    public void tickEntities()
    {
        // 维度卸载判定相关
        if (this.playerEntities.isEmpty())
        {
            if (this.updateEntityTick++ >= 300)
            {
                return;
            }
        }
        else
        {
            this.resetUpdateEntityTick();
        }
        // 维度相关的运算,目前仅有末地的龙战相关逻辑
        this.dimension.tick();
        // 调用父类World的tickEntities()
        super.tickEntities();
    }
    
    // World.tickEntities()
    // 天气相关实体运算
    ·weatherEffects
    // 玩家实体运算
    this.tickPlayers();
    // 普通实体运算
    ·loadedEntities
    // 方块实体运算
    ·TileEntities
    

    现在让我们来按顺序总结一下所有的游戏阶段运行顺序:

    1. 成就命令相关 ^codeOrder1
    2. 同步玩家的客户端时间 ^codeOrder2
    3. 极限模式下难度锁困难判断 ^codeOrder3
    4. 群系生成相关 ^codeOrder4
    5. 玩家睡觉逻辑 ^codeOrder5
    6. 生物、怪物刷新 ^codeOrder6
    7. 区块卸载 ^codeOrder7
    8. 天空光衰减计算 ^codeOrder8
    9. 设置 GameTime 与 Daytime ^codeOrder9
    10. 计划刻 Tile Tick (Next Tick Entry) ^codeOrder10
    11. 随机检查并更新玩家附近的亮度 ^codeOrder11
    12. 天空光的计算与将新增的 Tile ^codeOrder12 Entity 储存至区块内;雷电;下雪与结冰;随机刻
    13. 玩家加载的区块列表更新,并发送客户端方块更新数据包 ^codeOrder13
    14. 村庄运算 ^codeOrder14
    15. 僵尸围城 ^codeOrder15
    16. 地狱门缓存清空 ^codeOrder16
    17. 方块事件 Block Event ^codeOrder17
    18. 维度卸载判定相关 ^codeOrder18
    19. 维度相关的运算,目前仅有末地的龙战相关逻辑 ^codeOrder19
    20. 天气相关实体运算 ^codeOrder20
    21. 玩家实体运算 ^codeOrder21
    22. 普通实体运算 ^codeOrder22
    23. 方块实体运算 ^codeOrder23
    24. 发送客户端实体更新数据包 ^codeOrder24
    25. 网络玩家信息运算 ^codeOrder25
    26. 自动保存 ^codeOrder26

    1.2 GameTick

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

    作为一个离散的时间量,在游戏的运算过程中一定存在某个时刻,GameTick 这个时间量发生改变,这就是 GameTick 的分界线。在 1.1 节,我们从代码执行顺序的角度列出了游戏的运算顺序。在这个长达 24 条的列表里,我将 GameTick 的分界线的划分在:8. 设置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. 设置 GameTime 与 Daytime
    2. 计划刻 Tile Tick (Next Tick Entry)
    3. 随机检查并更新玩家附近的亮度
    4. 天空光的计算与将新增的 Tile Entity 储存至区块内;雷电;下雪与结冰;随机刻
    5. 玩家加载的区块列表更新,并发送客户端方块更新数据包
    6. 村庄运算
    7. 僵尸围城
    8. 地狱门缓存清空
    9. 方块事件 Block Event
    10. 维度卸载判定相关
    11. 维度相关的运算,目前仅有末地的龙战相关逻辑
    12. 天气相关实体运算
    13. 玩家实体运算
    14. 普通实体运算
    15. 方块实体运算
    16. 发送客户端实体更新数据包
    17. 网络玩家信息运算
    18. 自动保存
    19. 成就命令相关
    20. 同步玩家的客户端时间
    21. 极限模式下难度锁困难判断
    22. 群系生成相关
    23. 刷怪
    24. 玩家睡觉逻辑
    25. 区块卸载
    26. 天空光衰减计算

    对于与修改服务端世界相关的操作所在的阶段,此表可化简为以下常用阶段顺序表

    序号 阶段 名称 缩写
    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 的刷怪空档期,不是一个完美的伪和平方案



  • 虽然看不懂,但还是支持一下(流下了不会java的泪水)


 

友情链接

魔茶国际
Powered by TIS