本期教程我们将探讨网络通信。网络用于服务器与客户端之间的数据同步。有时,我们无法直接从游戏中获取数据,比如上次提到的覆盖层(overlay)。覆盖层的参数没有直接获取游戏内相关信息的接口。这些数据与游戏逻辑无关,因为覆盖层类是游戏客户端的属性,数据存储在服务器端,因此无法直接获取。
上次我们想到了一个解决办法,即使用静态变量保存数据,在数据发生更改时更新。虽然我没有详细描述,但提到了每次数据更改时都要更新。这样,我们可以直接使用静态变量,但会引发一个问题:如果连接的是服务器,数据仍存储在服务器上,而客户端不运行逻辑,因此数据始终为零。这导致覆盖层在服务器环境下始终显示为零。要解决这个问题,需要通过网络将服务器端数据同步到客户端。
在此之前,我们还注意到 Parchment MC 和 New Forge 需要更新。Parchment MC 的地图版本是 8 月 13 日版,New Forge 的版本是 47.1.70。我们可以顺便更新一下,操作起来不会太麻烦。将地图版本改为 8.13,将 Forge 版本改为 47.1.70,然后加载 GRADLE 进行更改,出现“build successful”提示后,更新完成。
更新完成后,我们来实现网络通信。首先创建一个软件包,然后创建一个 Java 类,命名为 Channel。稍后我们会继续编写该类。接着创建另一个软件包,命名为 packet.paci,其中创建一个 Java 类,命名为 FarmXP,用于同步服务端数据到客户端。
首先,在packet包下创建一个新的软件包,命名为"server to player"。然后将 FarmXPPacket.java 文件拖拽到该包中。
现在,我们需要自己实现一些方法来处理这个packet。首先,我们需要定义 packet 中要使用的参数。由于我们只需要一个参数,即农场经验,因此我们可以定义一个私有的 final int 变量,命名为 farmXP。
接下来,我们创建构造函数。这里我们需要两个方法,首先是解码器。我们可以直接编写一个构造函数,命名为 public FarmXPPacket,参数为一个 friendly bad Buff。这个 Buff 是 Minecraft 提供的一种专门用于网络传输的工具,里面包含了许多现成可用的方法。我们可以直接使用这些方法,例如,我们可以写 farmXP = Buff.re() 来读取数据。除了我们目前需要的 read 方法外,如果我们想传输更复杂的数据,Buff 中还有其他有用的方法,比如 readBlogPost 和 readNBT 等。但在这里,我们只需使用 read 方法即可。
接下来,我们需要一个编码器。我们编写一个名为 public void encode 的方法,参数同样是一个 friendly bad Buff,但这次我们需要将数据写入 Buff 中。我们使用 Buff.right 方法来写入 int 类型的 farmXP。
这两个方法编写完成后,我们还需要一个处理数据的方法。我们编写一个名为 public void handle 的方法,参数为一个 supplier,其泛型为 network event 的 context。在这个方法中,我们可以对数据包进行处理。首先,我们可以通过 context.getPlayer() 来获取玩家对象。然后,context 中还包含了一些其他有用的方法,比如 getPlayer()。但由于我们是从服务端发送数据包,因此服务端只有一个,所以我们不需要使用 getPlayer() 方法。
另外,还有一点需要注意的是,这个网络系统是在另一个线程上运行的。它和MINECRAFT客户端或逻辑并不在同一个线程上。是的,它是多线程的。因此,我们不能直接与游戏的某些信息进行交互。但是,许多人都有这样的需求。因此,我们提供了一个名为"in cool work"的解决方案。通过传入一个RUNNABLE,在下一个游戏主线程的时刻执行。
比如,"Player fx p provider"执行"点XPCLINT等于放XP",然后调用"context点set pack handled",最后直接返回true。因为在这里,我们没有复杂的逻辑,所以不需要判断操作是否成功。如果这种简单逻辑失败的可能性较小。当然,如果你的逻辑可能出错,就需要使用try-catch进行包裹,或者定义一些判断条件。最后,如果失败,就返回false。handle部分就完成了。
现在,我们需要在channel这一部分进行操作。我们可以看到文档里提供了许多关于network的内容。我们直接复制这些内容,并导入。对于"resource location",我们直接使用我们自己的mod地。因此,我们将其命名为"FMXP"。接下来,我们使用public static void register方法。我们需要注册我们的频道,所以我们必须对频道进行初始化,包括注册我们的packet。我们使用instance来注册消息。
这里有一些参数,比如Message、Encoder、Decoder、Consumer和network direction。但是,这种写法可读性较差,有时候会忘记参数的顺序。因此,我们可以使用另一种方法,即使用message builder。使用message builder可以增强可读性。首先,第一个参数是我们的firm xp packet的class。然后,我们在这里定义一个id,让其初始值为零。每次使用时,id都会自增,这样就不会重复。
第三个参数是网络方向,lay to clint。然后是decoder,即我们之前写的构造函数。我们将其实例化为cos函数。编码器是我们之前编写的。最后,点consumer,这里使用了之前讲过的"my sweet network sweet"。还有一些自动生成的方法可供使用,比如method network three和farm x p take it handle。最后,点击ADD即可完成注册。
下面是我们自定义的方法。在这里,我们继续编写public static void sendToPlayer方法。关于参数,我们可以看到这个instance,在这个方法中我们可以发送任何内容。因此,我们可以直接仿照这个方法编写。这个方法中的message是在这个位置定义的,所以我们也在这里定义一个message参数。接着我们需要另一个参数,即要发送给谁。
因此我们需要添加一个player参数。在这里,我们可以根据文档上的packet Distribute,点选player,然后将message发送给对应的玩家。对于farm xp overlay,我们可以在这里添加player、FarmXPProvider,然后点选XPCLIENT,将文字改为farm xp:。回到我们的farmXP类,我们需要在XP发生更改时更新这个数值。我们可以找一个合适的时机,在这个时机同步这个数据。如果找不到我们的这个event listener,我们可以在使用XP的时候进行同步。因为在使用之后就会发生更改。
在这里,我们可以编写channel.sendToPlayer(newBMXPitXP, player),然后从event中获取玩家并转换成服务器玩家。当然,在这里可能会遇到一些潜在的问题。同时,player clone也需要同步。我们可以直接复制这个内容。同样地,在玩家加入服务器时也需要同步这个数据。可以使用PlayerLoginEvent,我们之前应该已经写过了。
完成了这些工作之后,我们需要将其注册进去。在FMLCommonSetupEvent中,调用channel.register方法注册。完成这一系列步骤后,我们进行测试。由于这次测试涉及到网络,所以我们需要运行一个服务器。服务器启动完成后,我们运行lint,然后在多人游戏中连接到本地主机。现在我们的XP是零。点击右键后,程序崩溃了。但不用担心,这是意料之中的。
在这一段中,我们要探讨一个重要的知识点,即如何根据错误报告来识别问题。当我们在处理代码时,有时会遇到类似于 "net.minecraft"、"clean"、"player"、"local there" 这样的提示信息。这些信息表明在客户端和服务端都存在一些差异。在客户端,"player" 实际上是指本地玩家,而在服务端,它指的是服务器上的玩家。因此,有些代码只能在服务端执行,而在客户端则不应该执行。如果错误的代码在客户端运行,程序就会崩溃。我们可以通过一些方法来确定当前是在哪个端,比如使用 "level.isClientSide" 或者 "Dist.isClient" 这样的方法。
一般来说,我们优先使用前两种方法,而最后一种方法只是在前两种方法无法使用时才考虑。在代码中,我们可以这样写:如果事件发生在客户端,那么就不执行特定的代码,只有在服务端才执行。同样的逻辑也适用于其他情况。通过这样的方式,我们可以确保代码在不同端的正确执行。在本段的最后,我们还介绍了如何在获取经验时同步数据,以及如何在客户端和服务端之间发送数据的方法。那么本期教程就到此结束。