2.2 端
2.2.1 端的介绍
当编写我的世界Mod时,需要理解一个非常重要的概念,我的世界中有两个端:客户端和服务器。关于端的理解有很多的误解和错误,这很可能会导致代码运行错误,甚至会导致我的世界运行崩溃的可能。
2.2.2 端的种类
四个端的定义:
- 物理客户端:每次从启动器启动Minecraft,物理客户端用作整个程序的运行。在游戏的图形化、可交互的生命周期中运行的所有线程、进程和服务都是物理客户端的一部分。
- 物理服务器:通常被称为专用服务器,这个物理服务器是用于运行任意类型的minecraft_server.jar。该程序不会显示可播放的GUI。
- 逻辑服务器:逻辑服务器是运行游戏逻辑机制(村民繁殖、天气变化、更新商品、生命状况、村民AI和所有的其他游戏机制)。逻辑服务器存在于物理服务器中,但是它可以和逻辑客户端一起存在物理客户端中运行,作为一个单机世界。逻辑服务器始终在名为"服务器线程"的线程中运行。
- 逻辑客户端:逻辑客户端接受玩家的输入并将其转发到逻辑服务器。并且它还从逻辑服务器接受信息,并以图形的方式提供给玩家。逻辑客户端在"渲染线程"中运行,但是通常会派生出其他几个线程来处理音频和批量处理方块渲染等事务。
在MinecraftForge代码库中,物理端由一个名为Dist的枚举表示,而逻辑端则由一个称为LogicalSide的枚举代表。
2.2.3 执行端的特定操作
2.2.3.1 Level#isClientSide
这种布尔型的检查是比较常用检查端的方法。在Level对象上查询此字段将建立该级别所属的逻辑段。也就是说,如果此字段为true,则该级别当前正在逻辑客户端上运行。如果该字段为false,则表示当前正在逻辑服务器上运行。因此,物理服务器在该字段中总是fasle,但是我们不能假设false就是物理服务器,因为对于物理客户端的逻辑服务器也有可能是false的(指的是单人世界)。
若需要在运行游戏逻辑和其他机制时需要确定在那端,请使用此检查。例如,如果你想在玩家每次点击你的方块时伤害他们,或者让你的机器将泥土转换成钻石,你只要在确定#isClientSide为false后才能这样做。在最好的情况下,将游戏逻辑应用于逻辑客户端可能会导致同步失败等问题,在最坏的情况下会导致游戏崩溃。
这个检查应用作默认设置。除了DisExecutor,你很少需要其他方法来确定端和调整方法。
2.2.3.2 DistExecutor
考虑到客户端和服务器Mods使用单个"共同"jar,并且将物理端分离为两个jar,我们想到一个重要的问题:我们如何是有只存在于一个物理端的代码?net.minecraft.client中的所有代码仅存在于物料客户端上。如果你编写的任何类以任何方式引用这些名称,那么当在不存在这些名称的环境中加载相应的类时,它们将导致游戏崩溃。初学者一个非常常见的错误时调用Mincraft.getInstance()()在block或Block实体类中,一旦加载该类,会使得物理服务器崩溃。
如何解决这类问题,FML有DisExecutor,它提供了各种方法在不同的物理端运行不同的方法,或者只在一端运行单个方法。
笔记 FML检查是基于物理端的。一个单机世界(逻辑服务器+物理客户端中的逻辑客户端)将始终使用Dist.client。
DistExecutor的工作原理是接收所提供的执行方法的供应者,通过利用invokedynamicJVM指令有效的防止类加载。执行的方法应该是在静态的并且在不同的类中。此外,如果静态方法没有参数,则应使用方法参考,而不是由供应者执行方法。
DistExecutor中由有两个主要的方法:#runWhenOn和#callWhenOn。这些方法采用执行方法应该运行的物理端和提供的执行方法,分别运行或返回结果。
这两种方法被进一步细分味#safe和#unsafe变体。安全和不安全的变体是用词不当的。主要区别在于,在开发环境中,#safe方法将验证提供的执行方法是否是lambda,该lambda返回对另一个类的方法引用,否则将抛出错误。在生产环境中,#safe和#unsafe在功能上是相同的。
//单独一端运行: 例子
public static void unsafeRunMethodExample(Object param1, Object param2) {
// ...
}
public static Object safeCallMethodExample() {
// ...
}
// In some common class
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> ExampleClass.unsafeRunMethodExample(var1, var2));
DistExecutor.safeCallWhenOn(Dist.CLIENT, () -> ExampleClass::safeCallMethodExample);
由于invokedynamic在Java
9+的工作方式发生了变化,DistExecutor方法的所有#safe变体都会在开发环境中抛出封装在BootstrapMethodError中的原始异常#unsafe变体或对FML
Environment#idst的检查
2.2.3.3 Thread Groups(线程组)
如果Therad.currentThread().getThreadGroup()==SideThreadGroups.SERVER味True,当前线程在逻辑服务器上。否则它在逻辑客户端上。当你没有使用Level对象检查isClientSide时,这对于检查逻辑端非常有用。它通过查看当前运行的线程组来猜测你处于那个逻辑端。因为这是一种猜测,所以只有当其他选项都用完时,才应该使用这种方法。在其他任何情况下,首选的都应该时检查Level#isClientSide。
2.2.3.4 FMLEnvironment#dist and @OnlyIn
FMLEnvironmnet#dist保存在代码运行的物理端。由于它是在启动时确定的,所以他不依赖于猜测来返回结果。然而,则会方面的使用案例数量是比较少的.
使用#Only(Dist)注解 对方法或者字端,段进行注解向加载程序表明,应当将相应的成员从定义中完成剥离,而不是在指定的物理端。通常,这些只有爱浏览反编译的Minecraft代码时才能看到,这表明Mojang模糊处理程序删除了一些方法。没有原因直接使用此注释。请使用DistExecutor或FMLEnivironment#dist检查。
2.2.4 常见错误
2.2.4.1 跨逻辑端错误
每当你想将信息从一个逻辑端发送到另一个逻辑端时,必须始终使用网络数据包。在单人场景中,将数据从逻辑服务器直接传输到逻辑客户端是非常诱人的操作。
实际上。这通常是用过静态场无意中完成的。由于在单个播放器场景中,逻辑客户端和逻辑服务器共同享有相同的JVM,所以静态字段写入和从静态字段读取的线程都会导致各种竞争条件及线程相关的问题。
通过从逻辑服务器上运行或可以运行的公共代码访问仅物理客户端的类(如Minecraft),也可能会明确的犯下这个错误。对于在物理客户你中调试的初学者来说,这个错误容易被忽略。代码被执行到的时候,会立即造成物理服务器的崩溃。
2.2.4.3 编写单端Mod错误
在最近的版本中,Minecraft Forge从mods.toml中删除了一个“sideedness”属性。这意味着无论你的mod是加载在物理客户端还是在物理服务器上,它都可以运行。因此,对于单端Mod。你通常会在DistExecuto#safeRunWhenOne或DistExecuto#unsafeRunWhen中注册实际处理程序,而不是直接调用mod构造函数中的相关注册方法。基本上,如果你的mod加载在错误的一端,他应该什么都不做,什么都不监听等等。单端的Mod本质上不应该注册块、物品…因为他们不需要在另一端可用。
此外,如果你的mod是单端的,它通常不会禁止用户加入缺乏该Mod的服务器。因此,你应该注册一个IExtensionPoint$DisplayTest扩展点,以确保Forge认为服务器上不需要你的mod,这会导致服务器显示为不兼容。为此,将类似的内容放入你的主Mod类构造函数中;
//确保另一个网络端没有Mod不会导致客户端将服务器显示为不兼容
ModLoadingContext.get().registerExtensionPoint(IExtensionPoint.DisplayTest.class, () -> new IExtensionPoint.DisplayTest(() -> NetworkConstants.IGNORESERVERONLY, (a, b) -> true));
这告诉客户端它应该忽略不存在的服务器版本,并且服务器不应该告诉客户端应该存在这个mod。因此,这个片段适用于仅客户端和服务器的mod。