像许多其他程序一样,Minecraft 遵循客户端-服务器概念,其中客户端负责显示数据,而服务器负责更新数据。当我们使用这些术语时,我们对它们的含义有相当直观的理解……对吧?
事实证明,并非如此。许多混淆源于 Minecraft 根据上下文有两种不同的“侧”的概念:物理侧和逻辑侧。
物理侧与逻辑侧
物理侧
当你打开 Minecraft 启动器,选择一个版本安装并按下“开始游戏”时,你会启动一个物理客户端。“物理”一词在这里的含义是“这是一个客户端程序”。这尤其意味着客户端功能,例如所有渲染内容,都可以在这里使用,并可以根据需要使用。相比之下,物理服务器,也称为专用服务器,是当你启动 Minecraft 服务器 JAR 时打开的。虽然 Minecraft 服务器带有一个基本的 GUI,但它缺少所有仅客户端的功能。最值得注意的是,这意味着服务器 JAR 中缺少各种客户端类。在物理服务器上调用这些类会导致类缺失错误,即崩溃,因此我们需要对此进行防范。
逻辑侧
逻辑侧主要关注 Minecraft 的内部程序结构。逻辑服务器是游戏逻辑运行的地方。诸如时间和天气变化、实体刻度、实体生成等都在服务器上运行。各种数据,例如物品栏内容,也是服务器的职责。另一方面,逻辑客户端负责显示所有需要显示的内容。Minecraft 将所有客户端代码都保存在一个独立的 net.minecraft.client
包中,并在一个名为渲染线程的单独线程中运行它,而其他所有内容都被视为通用(即客户端和服务器)代码。
有什么区别吗?
物理侧和逻辑侧之间的区别最好通过两种情况来举例说明:
玩家加入一个多人世界。这相当简单:玩家的物理(和逻辑)客户端连接到其他地方的物理(和逻辑)服务器——玩家不关心服务器在哪里;只要他们可以连接,这就是客户端所知道的全部,也是客户端需要知道的全部。
玩家加入一个单人世界。这就是事情变得有趣的地方。玩家的物理客户端启动一个逻辑服务器,然后,现在以逻辑客户端的角色,连接到同一台机器上的该逻辑服务器。如果你熟悉网络,你可以将其视为连接到
localhost
(仅在概念上;不涉及实际的套接字或类似的东西)。
这两种情况也显示了主要问题:如果一个逻辑服务器可以使用你的代码,那么这并不能保证物理服务器也能使用。这就是为什么你应该始终使用专用服务器进行测试,以检查意外行为。由于不正确的客户端和服务器分离而导致的 NoClassDefFoundError
和 ClassNotFoundException
是模组中最常见的错误之一。另一个常见的错误是使用静态字段并从两个逻辑侧访问它们;这尤其棘手,因为通常没有任何迹象表明存在问题。
✅如果你需要将数据从一侧传输到另一侧,则必须发送一个数据包。
在 NeoForge 代码库中,物理侧由一个名为 Dist
的枚举表示,而逻辑侧由一个名为 LogicalSide
的枚举表示。
ℹ️从历史上看,服务器 JAR 拥有客户端没有的类。但在现代版本中,情况并非如此;你可以认为物理服务器是物理客户端的一个子集。
执行特定于侧的操作
Level#isClientSide()
这个布尔值检查将是你最常用的检查侧的方法。在 Level
对象上查询此字段可以确定该 Level
所属的逻辑侧:如果此字段为 true
,则该 Level
在逻辑客户端上运行。如果该字段为 false
,则该 Level
在逻辑服务器上运行。由此可见,物理服务器在此字段中将始终包含 false
,但我们不能假设 false
意味着物理服务器,因为对于物理客户端内的逻辑服务器(即单人游戏世界),此字段也可能为 false
。
每当你需要确定是否应运行游戏逻辑和其他机制时,请使用此检查。例如,如果你想让玩家每次点击你的方块都受到伤害,或者让你的机器将泥土加工成钻石,你应该仅在确保 #isClientSide
为 false
后才执行此操作。在逻辑客户端上应用游戏逻辑在最好的情况下会导致不同步(幽灵实体、不同步的统计数据等),在最坏的情况下会导致崩溃。
✅此检查应作为你的首选默认方法。无论何时你有可用的
Level
,都应使用此检查。
FMLEnvironment.dist
FMLEnvironment.dist
是 Level#isClientSide()
检查的物理对应项。如果此字段为 Dist.CLIENT
,则你在物理客户端上。如果该字段为 Dist.DEDICATED_SERVER
,则你在物理服务器上。
@Mod
处理仅客户端类时,检查物理环境非常重要。推荐的分离应仅在一个物理客户端上执行的代码的方法是指定一个单独的 @Mod
注解,并将 dist
参数设置为模组类应加载到的物理侧:
@Mod("examplemod")
public class ExampleMod {
public ExampleMod(IEventBus modBus) {
// Perform logic in that should be executed on both sides
// 执行两侧都执行的逻辑
}
}
@Mod(value = "examplemod", dist = Dist.CLIENT)
public class ExampleModClient {
public ExampleModClient(IEventBus modBus) {
// Perform logic in that should only be executed on the physical client
// 执行只在物理客户端上执行的逻辑
Minecraft.getInstance().whatever();
}
}
@Mod(value = "examplemod", dist = Dist.DEDICATED_SERVER)
public class ExampleModDedicatedServer {
public ExampleModDedicatedServer(IEventBus modBus) {
// Perform logic in that should only be executed on the physical server
// 执行只在物理服务端上执行的逻辑
}
}
✅模组通常应在任何一侧都能工作。这尤其意味着,如果你正在开发一个仅客户端的模组,你应该验证该模组是否真的在物理客户端上运行,并在它没有运行时执行空操作。