Minecraft中的Side
要想理解Minecraft Mod制作,Side是一个非常重要的概念。一共有两个Side:客户端(Client)和服务端(Server)。关于Side有很多很多常见的误解和错误,这些错误可能不会崩溃游戏,但是能产生一些不可预料效果。
不同的Side
当我们说“客户端”和“服务端”的时候,应该能很明确地知道我们说的是游戏的哪一部分。如果你还是不能理解,客户端是用户互动的地方,服务端是用户连接多人游戏的地方。
然而事实证明这两个术语仍然会有些模糊。为了消除歧义,在这里我们区分“客户端”和“服务端”四个可能的定义:
- 物理客户端(Physical Client):物理客户端就是你启动Minecraft时运行的整个程序。游戏运行周期中所有的线程,进程,服务,都是物理客户端的一部分。
- 物理服务端(Physical Server):通常称之为专用服务器(Dedicated Server)。物理服务端就是你运行
minecraft_server.jar
之类服务器时的整个程序,它不会有可操作的GUI。 - 逻辑服务端(Logical Server):逻辑服务端运行着游戏的逻辑:生物生成、天气、更新背包、生命、AI等游戏机制。逻辑服务端存在于物理服务端之中,但它也可以和逻辑客户端一齐运行在物理客户端之中,作为一个单人游戏世界。逻辑服务端永远运行在一个叫做
Server Thread
的线程中。 - 逻辑客户端(Logical Client):逻辑客户端从玩家那里接收输入,并将其转发至逻辑服务端中。除此之外,它也同样从逻辑服务端那里接收信息,并将其以图形化的方式呈现给玩家。逻辑客户端运行在
Client Thread
当中,当然通常一些其它的线程也会生成来处理音频和区块渲染等工作。
执行单端(Side-Specific)操作
world.isRemote
这个boolean检查是最常用的检查Side的方式。对一个 World
对象查询这个值将会得到这个世界所处的逻辑端。也就是说,当这个值为 true
的时候,世界正在运行在逻辑客户端内。如果这个值为 false
,世界正在运行在逻辑服务器上。在物理服务端中这个值永远为 false
,但是 false
并不意味着世界一定在物理服务端中,因为这个值也可以在物理客户端的逻辑服务端内为 false
(即单人游戏世界中)。
你可以使用这个来检查游戏逻辑和其他机制是否需要被运行。比如说,如果你想要让玩家在点击你的方块时受到伤害,或者让你的机器加工泥土为钻石,你首先需要确认 world.isRemote
为 false
。将游戏逻辑应用到逻辑客户端将轻则造成不同步(幽灵实体、数据不同步等),重则崩溃游戏。
这个检查将会成为你的首选。除了Proxy之外,很少你会需要其它判定Side和调整行为的方式。
@SidedProxy
考虑一下,客户端和服务端mod只使用了一个单独的jar,而物理端却又分成了两个jar,一个很重要的问题产生了:我们怎样使用只在一个物理端存在的代码?所有在 net.minecraft.client
包中的代码只存在于物理客户端中,所有在 net.minecraft.server.dedicated
包中的代码只存在于物理服务端中。如果你引用了这些包里的类,当这个类加载在特定的环境时游戏将会崩溃。新手最常见的错误就是在方块或者实体类中调用 Minecraft.getMinecraft().<doStuff>()
,一旦这种类加载了,他们就会使物理服务端崩溃。
我们该怎么解决这个问题呢?幸运的是,FML提供了 @SidedProxy
这个注解(Annotation),我们只需要给它两个类的名字(一个在 serverSide
,另一个在 clientSide
),并且用这个注解修饰一个字段。当mod启动时,FML将根据物理端实例化其中一个类。
提示
非常重要的是,你需要理解FML实例化挑选的Proxy是基于物理端的。一个单人游戏世界(逻辑服务端 + 逻辑客户端在同一个物理客户端中),Proxy将仍然会使用 clientSide
中的类型。
一个很常见的使用情况就是注册渲染器(Renderer)和模型(Model),它们必须在主初始化方法(preInit
,init
,或 postInit
)中调用。然而,许多渲染相关的类和注册在物理服务端上都不存在,调用它们还会使游戏崩溃。因此,我们将这些行为都放在客户端的Proxy中,保证他们只会在物理客户端中运行。
记住,指定的两个Proxy都需要能被赋值到被注解为 @SidedProxy
的一个字段上。通常我们需要有一个 IProxy
接口作为字段类型,和两个实现,ClientProxy
和 ServerProxy
分别对应两个物理端。
getEffectiveSide
FMLCommonHandler.getEffectiveSide()
调用时将返回逻辑端,它应该在你没法获得 World
对象并检查 isRemote
的时候被调用。这个方法通过检测当前正在运行的线程名称猜测(Guess)你所在的逻辑端。由于它只是一个猜测,这个方法只应该在其他选项都不可行的时候才应使用。几乎任何情况下你都应优先检查 world.isRemote
而不是这个方法。
getSide
和 @SideOnly
FMLCommonHandler.getSide()
可以用来获取你代码所运行的物理端。由于这个是在启动时就决定的,它不依赖于猜测以得到结果。然而这个方法的调用情况很少。
用 @SideOnly
注解一个方法或字段将告诉加载器(Loader)对应的成员应当在指定的物理端中不要定义。通常情况下,这些注解将会只在浏览反编译后的Minecraft源码时能看到,标识Mojang混淆器所剥离出来的方法。这个注解不应直接使用在你的代码中。只有你在重写(Override)已经使用 @SideOnly
定义的原版方法时你能使用它。其它的大部分情况下请使用 @SidedProxy
或者检查 getSide()
。
常见错误
越逻辑端访问
每当你想从一个逻辑端传输信息到另一个时,你必须使用网络数据包(Packet)。尽管在单人游戏情况下直接从逻辑服务端到逻辑客户端传输数据很方便。
这其实经常是不经意间通过静态字段(Static Field)造成的。由于单人游戏中逻辑客户端和逻辑服务端共享同一个JVM,两个线程读写同一个静态字段将会导致各种各样的竞争情况和多线程的经典问题。
这个错误也会由于从运行在或能运行在逻辑服务端上的共享代码访问仅限物理客户端的类所造成。这个错误经常会被新手所忽略,因为他们常常只在物理客户端上调试。尽管代码可以运行在物理客户端上,但它们会在物理服务器上崩溃。