客户端与服务端
和许多程序一样,Minecraft 采用客户端与服务端架构:客户端负责显示数据,服务端负责更新数据。乍看之下,这两个术语似乎很好理解。
但实际情况没有这么简单。很多困惑来自 Minecraft 中两种不同的“端”:物理端和逻辑端。它们的含义取决于讨论的上下文。
逻辑端与物理端
物理端
打开 Minecraft 启动器,选择一个 Minecraft 实例并点击开始游戏,启动的就是物理客户端。这里的“物理”强调它是一个客户端程序,也就意味着渲染等客户端功能都可在这个程序中使用。与之对应,运行 Minecraft 服务端 JAR 启动的是物理服务端,也称独立服务器。虽然 Minecraft 服务端带有一个简单的图形界面,但它不包含仅客户端功能。尤其需要注意,服务端 JAR 中缺少各种客户端类。在物理服务端调用这些类会产生类缺失错误,也就是导致崩溃,因此必须做好隔离。
逻辑端
逻辑端主要描述 Minecraft 程序内部的结构。逻辑服务端运行游戏逻辑,例如时间和天气变化、实体刻更新、实体生成等。物品栏内容之类的各种数据也由服务端负责。逻辑客户端则负责显示所有需要呈现给玩家的内容。Minecraft 将客户端代码放在独立的 net.minecraft.client 包中,并在名为 Render Thread(渲染线程)的独立线程上运行;其他代码则被视为客户端和服务端共用的代码。
两者有何区别?
下面两个场景最能说明物理端和逻辑端的差别:
- 玩家加入多人游戏世界。此时比较直观:玩家的物理客户端(同时也是逻辑客户端)连接到另一处物理服务端(同时也是逻辑服务端)。服务端具体运行在哪里并不重要;只要能连接,客户端就不需要了解更多。
- 玩家加入单人游戏世界。此时,玩家的物理客户端会启动一个逻辑服务端,然后以逻辑客户端的身份连接到同一台计算机上的逻辑服务端。如果你熟悉网络,可以将它理解为连接
localhost。不过这只是概念上的类比,实际并不涉及套接字之类的网络连接。
这也带来了一个主要问题:代码能够在逻辑服务端运行,并不代表它也能在物理服务端运行。因此,你应始终使用独立服务器测试,检查是否存在意外行为。客户端和服务端隔离不当引发的 NoClassDefFoundError 和 ClassNotFoundException,是 Mod 开发中最常见的错误之一。另一个常见错误是在两个逻辑端访问同一个静态字段;这类问题尤其难排查,因为通常看不到明显的异常迹象。
建议 如果需要在不同端之间传递数据,必须发送数据包。
NeoForge 代码中使用 Dist 枚举表示物理端,使用 LogicalSide 枚举表示逻辑端。
说明 早期服务端 JAR 中曾存在客户端没有的类。现代版本已不再如此;可以将物理服务端理解为物理客户端的子集。
执行特定端的操作
Level#isClientSide()
这个布尔值检查会是你判断端类型时最常用的方式。在 Level 对象上进行检查,可以判断该世界所在的逻辑端:值为 true 时,世界运行于逻辑客户端;为 false 时,运行于逻辑服务端。因此,物理服务端中的这个值始终为 false。但不能反过来认为 false 就表示物理服务端,因为物理客户端内部的逻辑服务端,也就是单人游戏世界,同样会返回 false。
在判断是否应执行游戏逻辑或其他机制时,应使用这项检查。例如,让玩家每次点击方块时受到伤害,或让机器把泥土加工成钻石,都应先确认 #isClientSide() 为 false。在逻辑客户端执行游戏逻辑,轻则造成不同步,例如幽灵实体或数值不一致,重则导致崩溃。
建议 将这项检查作为默认选择。只要能访问
Level,就使用它判断逻辑端。
FMLEnvironment#getDist()
FMLEnvironment#getDist() 用于判断物理端,与判断逻辑端的 Level#isClientSide() 相对应。值为 Dist.CLIENT 时,程序运行于物理客户端;为 Dist.DEDICATED_SERVER 时,运行于物理服务端。
@Mod
使用仅客户端类时,检查物理运行环境十分重要。要隔离只应在某个物理端执行的代码,推荐单独声明一个带有 @Mod 注解的类,并通过 dist 参数指定该 Mod 类应在哪个物理端加载:
@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
}
}
@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
}
}
建议 通常应让 Mod 在任意一端都能正常加载。尤其是开发仅客户端 Mod 时,应先确认程序确实运行于物理客户端;如果不是,就不执行相关操作。
来源: NeoForged/Documentation(26.1)。中文翻译依据提交
4a680cdd4d15。