Skip to main content
Version: 1.21.4

侧(Sides)

和许多其他程序类似,Minecraft 采用了客户端-服务器(client-server)架构,其中客户端负责数据的显示,服务器则负责数据的更新。我们在使用这些术语时,往往会觉得它们很直观,大家都懂……对吧?

实际上,并没有那么简单。很多困惑都源自于 Minecraft 在不同上下文下有两种不同的“侧(side)”的概念:物理侧(physical side)和逻辑侧(logical side)。

逻辑侧与物理侧(Logical vs. Physical Side)

物理侧(The Physical Side)

当你打开 Minecraft 启动器,选择一个 Minecraft 安装版本并点击“play”时,你启动了一个物理客户端(physical client)。这里的“物理”是指“这是一个客户端程序”。这意味着所有客户端相关的功能,比如渲染(rendering)等,都可以在这里使用。相比之下,物理服务器(physical server),也就是我们常说的“专用服务器(dedicated server)”,是在你运行 Minecraft 服务器 JAR 文件时启动的。虽然 Minecraft 服务器带有一个基础的图形界面(GUI),但它缺少所有仅限客户端的功能。尤其需要注意的是,许多客户端专用的类在服务器 JAR 中是不存在的。如果你在物理服务器上调用这些类,会导致类缺失错误(missing class errors),也就是崩溃,因此我们需要对这种情况进行防护。

逻辑侧(The Logical Side)

逻辑侧主要关注于 Minecraft 的内部程序结构。逻辑服务器(logical server) 是游戏逻辑运行的地方。比如时间和天气的变化、实体(entity)的 tick、实体生成等,所有这些都在服务器端执行。各种数据,例如背包(inventory)内容,也都由服务器负责。相对地,逻辑客户端(logical client) 负责显示所有需要展示的内容。Minecraft 将所有客户端代码都放在独立的 net.minecraft.client 包中,并在一个名为 Render Thread(渲染线程)的独立线程中运行,而其他部分则被视为通用代码(即客户端和服务器共享的代码)。

区别是什么?(What's the Difference?)

物理侧和逻辑侧的区别可以通过以下两个场景来说明:

  • 玩家加入一个**多人(multiplayer)**世界。这种情况比较直接:玩家的物理(和逻辑)客户端连接到某处的物理(和逻辑)服务器——玩家并不关心服务器在哪里;只要能连接上,这就是客户端所知道和需要知道的一切。
  • 玩家加入一个**单人(singleplayer)**世界。这就有趣了。玩家的物理客户端会启动一个逻辑服务器,然后,作为逻辑客户端,再连接到本机上的这个逻辑服务器。如果你熟悉网络编程,可以把它想象成连接到 localhost(仅仅是概念上的;实际上并没有真正的 socket 或类似的东西参与)。 这两种场景同时也揭示了主要的问题:如果逻辑服务器(logical server)能够正常运行你的代码,这本身并不能保证物理服务器(physical server)同样能够正常工作。因此,你始终应该通过专用服务器(dedicated servers)进行测试,以检查是否存在意外行为。由于客户端和服务器分离不当导致的 NoClassDefFoundErrorClassNotFoundException 是模组开发中最常见的错误之一。另一个常见的错误是操作静态字段(static fields)并在两个逻辑端都访问它们;这种问题尤其隐蔽,因为通常没有明显的迹象表明出现了错误。
tip

如果你需要在两个端之间传递数据,必须发送一个数据包

在 NeoForge 代码库中,物理端(physical side)通过一个名为 Dist 的枚举(enum)表示,而逻辑端(logical side)则通过一个名为 LogicalSide 的枚举表示。

info

历史上,服务器 JAR 文件中拥有客户端所没有的类。但在现代版本中,情况已经不同;可以认为物理服务器是物理客户端的一个子集。

执行端特定操作(Performing Side-Specific Operations)

Level#isClientSide()

这个布尔值检查会是你判断端的最常用方式。在一个 Level 对象上查询这个字段,可以判断该世界属于哪个逻辑端:如果这个字段为 true,说明该世界运行在逻辑客户端(logical client);如果为 false,则运行在逻辑服务器(logical server)。由此可知,物理服务器上的该字段始终为 false,但不能据此假设 false 就一定代表物理服务器,因为在物理客户端(如单人世界)中的逻辑服务器,这个字段同样可能为 false

每当你需要判断是否应运行游戏逻辑或其他机制时,都应使用此检查。例如,如果你希望每次玩家点击你的方块时受到伤害,或让你的机器把泥土加工成钻石,都应确保只有在 #isClientSidefalse 时才执行。将游戏逻辑应用到逻辑客户端可能导致数据不同步(如幽灵实体、不同步的统计数据等),严重时甚至会导致崩溃。

tip

这个检查应作为你的默认选择。只要你能获取到 Level,就用这个检查。

FMLEnvironment.dist

FMLEnvironment.distLevel#isClientSide() 检查的物理端对应方式。如果该字段为 Dist.CLIENT,说明当前处于物理客户端;如果为 Dist.DEDICATED_SERVER,则处于物理服务器。

@Mod

在处理仅限客户端的类时,检测物理环境非常重要。推荐的做法是通过指定单独的 @Mod 注解,并将 dist 参数设置为该模组类应加载的物理端(physical side),以实现仅在物理客户端执行特定代码:

@Mod("examplemod")
public class ExampleMod {
public ExampleMod(IEventBus modBus) {
// 执行需要在客户端和服务端都运行的逻辑
}
}

@Mod(value = "examplemod", dist = Dist.CLIENT)
public class ExampleModClient {
public ExampleModClient(IEventBus modBus) {
// 执行只应在物理客户端上运行的逻辑
Minecraft.getInstance().whatever();
}
}

@Mod(value = "examplemod", dist = Dist.DEDICATED_SERVER)
public class ExampleModDedicatedServer {
public ExampleModDedicatedServer(IEventBus modBus) {
// 执行只应在物理服务端上运行的逻辑
}
}
tip

通常情况下,模组(mod)应当能够在客户端或服务端任一端运行。特别是如果你正在开发一个仅限客户端的模组,你需要确保该模组确实只会在物理客户端上运行,并且在非客户端环境下应不执行任何操作(no-op)。