跳到主要内容

5-1 刷怪塔杀手

实体(以及它带来的问题)

在生存服务器上,生电(为高效自动化获取某些资源而建设的机械)往往在大幅增加生存模式的发展速度时,也消耗着大量的服务器资源。有些是缘于大量的红石更新,有些是高昂的水流计算,有些则是实体数目过多。实体的运算通常消耗大量的资源,过多的实体会严重影响服务器的运行流畅性。

如果你还不了解实体是什么,可以查看 Minecraft Wiki 上的相关内容。

在继续之前,让我们先来看看实体为什么会造成服务器的卡顿。在理想的情况下,Minecraft 一秒运行 20 次更新,读取各种事件并相应更新世界数据。要在一秒内完成 20 次更新(术语称作),就需要确保平均每刻的耗时不能超过 50 毫秒,否则世界的运行速度就会开始降低。

50 毫秒对于一个可能有数百人的服务器来说是非常紧张的资源,因为大多数 Minecraft 服务器都只在一个 CPU 核心上进行游戏运算,而在这段时间内它必须计算世界中所有怪物、物品、村民、动物等各种实体的移动,还有红石、活塞更新之类的工作。由于实体在运动时要进行物理模拟运算,消耗的资源就尤其多。这种资源消耗在生电中尤其明显,因为生电所在的地方每秒都有大量的实体被创建和销毁。

本章我们就来复刻一个曾在 BiliCraft 中使用的插件 —— 区块过热,它能以牺牲刷怪塔效率为代价,解决实体数目过多的问题。尽管由于过于破坏生存模式下的游戏体验,这个插件现今已不再在 BiliCraft 的生电服务器上使用,也不是传统意义上的优化插件,但仍然是用于学习和练习的好例子。

设计思路

区块过热(Chunk Heat)

如果一个区块在短时间内刷新大量指定的生物,那么就暂缓或阻止这些生物继续生成。

插件的功能应当可以随时启停,而不需要重载或重启服务器。

管理员可以自定义哪些类型的生物将被统计,以及开始限制生物生成的数目。

当有生物生成时,区块的热度会增加,当热度超出一个预期的数值时,我们就阻止该区块创建更多生物,并且等待一段时间让区块冷却,随后把热度清零,重新开始计数。这个插件正是因此而得名。

这项技术的主要用途是高效且可靠地检测刷怪塔一类生电的存在,并且限制高效率刷怪塔的产量。就游戏体验而言这不是个太好的策略,但在服务器由于算力极度有限必须禁用生电机器时,这项技术是一个可选的方案。

为了检测在一定时间内实体生成的数目,我们需要监听相应的事件,对应的事件是 EntitySpawnEvent(嗯,我是猜出来的),同时还要统计这个事件发生的频率,以及最重要的一点 —— 判断这个实体是不是生物,以及在不在管理员所配置的范围内。

这些运算都是以区块为单位的,如果你还不知道什么是区块,可以在 Minecraft Wiki 上找到相应内容。在插件设计中,许多与世界(方块、实体等)交互的任务都是按区块运行的。

向区块附加数据

那么,如何记录某个区块当前已经创建的实体数呢?你或许会想,要是能修改区块类(Chunk 及其实现)的代码就好了,但这是做不到的。你或许又会想,那能不能使用扩展函数来添加一个计数器呢?但这也是做不到的,因为扩展函数本质上是利用已有的方法和属性组建新的方法,但 Chunk 接口并没有提供任何可以直接修改的属性。

幸运的是,Bukkit 已经为我们提供了这个问题的解决方法,这就是持久性数据容器(Persistent Data Container,PDC),Bukkit 为世界数据中常用的对象都设置了对应的 PDC,PDC 基本上就是一张表,我们可以在里面存储一些插件所用到的数据。

PDC 由 Bukkit(和 Paper)负责管理,所以要在 PDC 中登记任何数据,就必须先想想该给这个数据用什么名字。这里用到的是一种被称为命名空间 ID(Namespaced Key) 的格式,形如 minecraft:grass_block,也就是 所属组织:键名 这样的格式。一般而言,所属组织部分会使用插件的名字,而键名则可以由插件自选。由于命名空间 ID 只能包含小写字符,因此我们的插件需要使用 chunk_heat 这样的命名空间。

如果想了解更多关于命名空间 ID 的内容,可以查看 Minecraft Wiki 上的相关页面。

在这个插件里,我们将使用 PDC 来存储当前区块的热度,要存储的数据很简单,就是一个代表当前热度的整数。

使用 PDC 的代码大致如下(你不必了解具体的函数签名):

chunk.persistentDataContainer.set(
    NamespacedKey.fromString("chunk_heat:heat")!!,  // 键名
    PersistentDataType.INTEGER,                     // 类型
    someValue                                       // 内容
)

你可以大致从代码中看到,我们指定了 chunk_heat:heat 作为键名,INTEGER(整数)作为数据类型,然后就可以将指定的数值设置到 PDC 中。我们稍后可以通过相同的键取回这一数据。


以上就是这个插件设计的大致思路,事不宜迟,接下来我们就来编写相关的代码,介绍 PDC 的用法,以及说明如何检测实体的类型。