客户端与服务端
与许多程序一样,Minecraft 采用客户端与服务端架构:客户端负责显示数据,服务端负责更新数据。不过,Minecraft 中的“端”有两套不同含义,必须结合上下文区分:物理端与逻辑端。
逻辑端与物理端
物理端
打开 Minecraft 启动器、选择游戏实例并点击开始游戏后,启动的是物理客户端。“物理”在这里表示这是一个客户端程序,因此能够使用渲染等客户端功能。对应的物理服务端也称专用服务端,通过运行 Minecraft 服务端 JAR 启动。虽然服务端提供一个简单的图形界面,但它没有客户端专用功能。尤其是服务端 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.dist
FMLEnvironment.dist 用于判断物理端,与判断逻辑端的 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
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
}
}
建议 通常应确保 Mod 在两种物理端都能正确加载。因此,即使开发的是客户端专用 Mod,也应检查当前是否确实运行在物理客户端;若不是,则不执行任何操作。
来源: NeoForged/Documentation(1.21.1)。中文翻译依据提交
4a680cdd4d15。