企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# 让你的模组成为多人模组 **难度分级:☆☆☆** 自Minecraft变成一款多人游戏后,**网络IO**变成了这个游戏中不可或缺的一部分。从Minecraft 1.3开始,Minecraft 的单人游戏也变成了模拟一台地址是 **127.0.0.1** 的 Minecraft 服务器,因此即使是在单人模式下,模组开发者依然要考虑网络IO的问题。 在实际模组开发的过程中,如果不添加额外的干预,客户端是绝对不知道服务端发生了什么的,反之亦然。比如说,一个TileEntity 在服务器上更新了业务逻辑,客户端对此是一无所知的;同样,玩家按下了键盘上的一个键,服务器也是对此毫不知情的。唯一能让两者互相知道发生了什么的方法,就是**网络通信**。 ## Netty Netty是由JBOSS提供的一个**JAVA网络工具开源框架**,为JAVA开发者提供了**异步、事件驱动的网络应用程序框架和工具**,用以快速开发高性能、高可靠性的网络服务器和客户端程序。**Minecraft的网络通讯系统基本都是基于Netty开发而成的**,因此如果你希望对该部分有更深入的理解,你可以在[这里]([https://netty.io/wiki/user-guide-for-4.x.html](https://netty.io/wiki/user-guide-for-4.x.html))了解到关于Netty的更多信息。 ## SimpleNetworkWrapper Minecraft和Forge接管了游戏中绝大部分底层以及**实体,方块实体等的单向网络交互**。但在某些情况下,我们仍然需要将一些额外的数据在客户端与服务端之间进行交互,这就需要我们自己构建网络交互的收发系统。幸运的是,Forge为我们提供了一个简便的自定义数据交互系统,它就是**SimpleNetworkWrapper**。 ## 编写自定义网络交互系统 &nbsp;&nbsp;难度分级:☆☆☆ 现在我们就使用SimpleNetworkWrapper来构建一个自定义网络交互系统。 ### **注册自定义网络传输总线对象** 首先我们需要在**游戏初始化阶段**通过独立的 **newSimpleChannel()** 方法注册一个**SimpleNetworkWrapper总线对象**,在EOK中的相关代码如下 **(这里为了方便起见还在主类中写了一个getNetwork()方法用于在别的类中调用)** : ~~~ public class EOK { public static final String MODID = "eok"; ... private SimpleNetworkWrapper network; ... @EventHandler public void preInit(FMLPreInitializationEvent event) { ... network = NetworkRegistry.INSTANCE.newSimpleChannel(MODID); } ... public static SimpleNetworkWrapper getNetwork() { return instance.network; } } ~~~ ### **创建自定义数据包类** 为了传输自定义数据,我们需要创建对应的**数据包类**。所有的数据包类都需要实现`net.minecraftforge.fml.common.network.simpleimpl.IMessage`接口,并需要**覆写fromBytes()和toBytes()两个方法**。这两个方法分别对应了**接收端从字节数据中读取NBT数据**以及**将NBT数据写为比特数据**两种方法。例如EOK中对于**清醒度**这一Capability的数据包代码如下: ~~~ public class PacketConsciousness implements IMessage { public NBTTagCompound compound; @Override public void fromBytes(ByteBuf buf) { compound = ByteBufUtils.readTag(buf); } @Override public void toBytes(ByteBuf buf) { ByteBufUtils.writeTag(buf, compound); } ... } ~~~ ### **创建数据接收类** 对于发送端发送的数据,接收端需要一个特定的类来接受自定义数据。该类需要实现`net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler`这一接口并要求覆写 **onMessage()方法** 以声明该类为数据接收类。另外,从Minecraft 1.8开始,Minecraft的所有网络操作都是在一个**单独的网络线程中**进行,这会导致其没有办法和游戏中的大多数对象交互。所以在**onMessage**方法中我们还需要调用IThreadListener的**addScheduledTask()方法**,并传入一个**Runnable()**。通常情况下,我们将该类写为自定义数据包的内部类,例如EOK中对于**清醒度**这一Capability的数据接收代码如下: ~~~ public class PacketConsciousness implements IMessage { ... public static class Handler implements IMessageHandler<PacketConsciousness, IMessage> { @Override public IMessage onMessage(PacketConsciousness message, MessageContext ctx) { //判断是否为客户端(接收端) if(ctx.side == Side.CLIENT) { //获取接受数据中"consciousness"这一NBT标识 final NBTBase nbt = message.compound.getTag("consciousness"); Minecraft.getMinecraft().addScheduledTask(new Runnable() { @Override public void run() { //获取客户端(接收端)玩家对象 EntityPlayer player = Minecraft.getMinecraft().player; //获取玩家的Capability并将接收到的consciousness数据写入玩家在客户端的Capability中 if (player.hasCapability(CapabilityHandler.capConsciousness, null)) { IConsciousness consciousness = player.getCapability(CapabilityHandler.capConsciousness, null); Capability.IStorage<IConsciousness> storage = CapabilityHandler.capConsciousness.getStorage(); storage.readNBT(CapabilityHandler.capConsciousness, consciousness, null, nbt); } } }); } return null; } } } ~~~ ### **注册数据包** 接下来,我们要将自定义数据包注册至网络传输总线上。和事件总线类似,SimpleNetworkWrapper类提供了一个名为 **registerMessage()** 的方法,该方法要求我们传入一个**数据接收类对象**,一个**数据包类**,一个**辨别标识符**,以及**该数据包要发送到的物理端**。EOK中对**清醒度Capability**的数据包注册代码如下: ~~~ public class EOK { ... @EventHandler public void preInit(FMLPreInitializationEvent event) { ... network.registerMessage(new PacketConsciousness.Handler(), PacketConsciousness.class, 1, Side.CLIENT); } ... } ~~~ ### **发送数据包** 最后,我们只需要在合适的事件调用自定义网络总线发送相应的数据包即可。 > SimpleNetworkWrapper类提供了若干个方法用于客户端和服务端发送数据包: > * `sendToAll`方法用于**服务端**发送数据包给所有玩家 > * `sendTo`方法用于**服务端**发送数据包给特定玩家 > * `sendToAllAround`方法用于**服务端**发送数据包给特定位置和特定半径确定的范围内的所有玩家 > * `sendToDimension`方法用于**服务端**发送数据包给特定维度的所有玩家 > * `sendToServer`方法用于**客户端**发送数据包给服务器 > (该段来自utsc_zzzz的模组开发教程) 对于玩家的附加Capability,我们可以通过监听**onPlayerLoggedIn事件**在玩家登陆时发送数据包。例如EOK中对**清醒度Capability**的数据包发送代码如下: ~~~ public class AnotherEventHandler { ... @SubscribeEvent public static void onPlayerLoggedIn(net.minecraftforge.fml.common.gameevent.PlayerEvent.PlayerLoggedInEvent event){ if (!event.player.world.isRemote) { EntityPlayer player = event.player; if(player.hasCapability(CapabilityHandler.capConsciousness, null)){ //新建数据包 PacketConsciousness message = new PacketConsciousness(); IConsciousness consciousness = player.getCapability(CapabilityHandler.capConsciousness, null); Capability.IStorage<IConsciousness> storage = CapabilityHandler.capConsciousness.getStorage(); //将服务端玩家的consciousness数据写入数据包 message.compound = new NBTTagCompound(); message.compound.setTag("consciousness", storage.writeNBT(CapabilityHandler.capConsciousness, consciousness, null)); //发送数据包至玩家的客户端 EOK.getNetwork().sendTo(message, (EntityPlayerMP) player); } } } } ~~~ 现在,我们就可以在客户端正常调用相应的Capability了。 ~~~ @SideOnly(Side.CLIENT) public class PlayerVitalSigns { ... @SubscribeEvent public void render(RenderGameOverlayEvent.Pre event){ if(event.getType() == RenderGameOverlayEvent.ElementType.AIR){ ... double conV = 0.0D; ... if(player.hasCapability(CapabilityHandler.capConsciousness, null)) { IConsciousness consciousness = (IConsciousness) player.getCapability(CapabilityHandler.capConsciousness, null); conV = consciousness.getConsciousnessValue(); } ... } } ... } ~~~ ## 使用GUI按钮对服务端对象进行控制 &nbsp;&nbsp;难度分级:☆ 在前一章的图中我们可以看到,Forge并没有接管**GuiContainer至Container的同步工作**,因此直接通过GUI中的元素对服务端游戏对象进行修改是不可行的。这里我们便需要通过发包来手动完成这一同步操作。由于这里和前文所举的形如Capability的同步操作略有差异,故单独进行举例,读者可以自行体会。 **首先我们新建一个IButtonHandler接口类,并让需要接受按钮发包数据的Container类实现这一接口,以方便我们后续在发包代码中对Container进行识别。(这一操作通常被俗称为“打标记”,在需要过滤特定对象时被广泛使用,希望读者能理解并模仿运用)** 例如EOK中对**折射望远镜**这一Container的标记代码: ~~~ public interface IButtonHandler { void onButtonPress(int buttonID); } ~~~ ~~~ public class ContainerRefractingTelescope extends Container implements IButtonHandler{ ... @Override public void onButtonPress(int buttonID) { ... } } ~~~ 随后,和前文类似,我们为GuiButton新建一个自定义数据包,并新建一个数据接收内部类。这里我们通过调用对应Container中覆写的IButtonHandler接口类中的方法,使得接收类在接收到发包数据后可以将数据**传入对应的Container对象中**。这里我们使用EOK中的代码进行解释: ~~~ public class PacketGuiButton implements IMessage { private int buttonID; private NBTTagCompound compound; //带NBT的构造函数 public PacketGuiButton(int buttonID, @Nullable NBTTagCompound compound){ this.buttonID = buttonID; this.compound = compound; } //不带NBT的构造函数 public PacketGuiButton(int buttonID){ this(buttonID, null); } //接收端从ByteBuffer里读取数据的方法 @Override public void fromBytes(ByteBuf buf) { buttonID = buf.readInt(); } //发送端将对应的数据写入ByteBuffer的方法 @Override public void toBytes(ByteBuf buf) { buf.writeInt(buttonID); } //接收端用于承接网络数据包的Handler内部类 public static class Handler implements IMessageHandler<PacketGuiButton, IMessage>{ @Override public IMessage onMessage(PacketGuiButton message, MessageContext ctx) { //获取接收端的玩家对象 EntityPlayer player = EOK.getProxy().getPlayer(ctx); if(player != null){ //新建线程任务(从Minecraft 1.8开始,Minecraft的所有网络操作都是在一个单独的网络线程中进行,所以这里需要调用IThreadListener的addScheduledTask方法,并传入一个Runnable) EOK.getProxy().getThreadListener(ctx).addScheduledTask(() -> { //判断玩家打开的Container是否带有IButtonHandler标记 if(player.openContainer instanceof IButtonHandler){ //调用玩家打开的Container中的onButtonPress方法 ((IButtonHandler) player.openContainer).onButtonPress(message.buttonID); } }); } return null; } } } ~~~ 随后我们需要将新的数据包类注册至**网络传输总线**上: ~~~ public class EOK { ... @EventHandler public void preInit(FMLPreInitializationEvent event) { network.registerMessage(new PacketGuiButton.Handler(), PacketGuiButton.class, 0, Side.SERVER); ... } } ~~~ 现在,我们只需要在按钮按下( **actionPerfomed()** 方法被触发)时向服务端发送相应的数据包即可实现对服务端的更新通知: ~~~ public class GUIRefractingTelescope extends GuiContainer { ... @Override protected void actionPerformed(GuiButton button) throws IOException { if(button.id == 0){ EOK.getNetwork().sendToServer(new PacketGuiButton(button.id)); } } } ~~~ ~~~ public class ContainerRefractingTelescope extends Container implements IButtonHandler{ ... @Override public void onButtonPress(int buttonID) { if(buttonID == 0){ updateSlot(); } } ~~~