本帖最后由 yuxuanchiadm 于 2013-8-4 17:14 编辑

利用Forge API开发联机MOD【基础篇】【第八章】
为你的MOD添加网络通信能力
作者:yuxuanchiadm

索引贴地址:http://www.mcbbs.net/thread-38211-1-1.html

请确定你已经阅读完成第七章的内容:
http://www.mcbbs.net/thread-38770-1-1.html
否则不要阅读此贴!

序:
在上一章里,我们让我们的刷怪笼能够像普通刷怪笼一样,刷出生物,但还不能自定义所刷生物和刷怪间隔,所以这一章本应该用来制作GUI,但由于我们开发的是联机MOD,而自定义刷怪笼需要网络通信,所以,让我们一起来制作我们MOD的网络通信框架吧 XD。

原版Minecraft是如何进行网络通信的:
在Minecraft中,不管是单机还是联机,都使用同样的框架进行数据通信(因为单人游戏需要允许其他玩家从局域网加入)。

如果是客户端发送数据到服务端,客户端一般会获取到EntityClientPlayerMP的实例(例如通过Minecraft.getMinecraft().thePlayer(确实这没任何问题且通用,但一般会采取别的更简单,但不通用的方法)),然后通过这个实例的sendQueue属性获取到客户端网络通信句柄(NetClientHandler类的实例),接着会创建一个数据封包(继承自Packet类),并通过NetClientHandler类的addToSendQueue方法发送封包到服务端。

如果是服务端发送数据到单个客户端,那么服务端会获取到那个客户端的EntityPlayerMP的实例,然后通过这个实例的playerNetServerHandler属性获取到服务端网络句柄(NetServerHandler类的实例),然后创建数据封包,最后调用NetServerHandler类的sendPacketToPlayer方法发送给客户端。

如果是服务端发送数据到所有客户端,那么服务端一般会先获取到ServerConfigurationManager类的实例(可以通过MinecraftServer类的getConfigurationManager方法获取到),然后创建封包,最后调用ServerConfigurationManager类的sendPacketToAllPlayers方法。

使用Forge API的MOD如何进行网络通信:
在Forge中,也使用Minecraft的那套通信机制,但是,几乎所有的MOD都不会创建新的封包,而是使用Packet250CustomPayload这个封包。那么Forge是如何通过这一个封包区分是哪个MOD发送的数据,和数据的用途呢?其实Forge只帮助Modder区分MOD,使用channel(管道)的方式来判断哪个MOD应该接受此数据,并不帮助我们区分数据的用途。所以,一般我们发送数据时都要自行包含一个ID,用于更好的判断数据的用途。

几个关键类的继承结构:
NetHandler:
NetHandler
┗    NetClientHandler
      NetLoginHandler
      NetServerHandler

EntityPlayer:
Entity
┗    EntityLiving
      ┗    EntityPlayer
            ┗    EntityOtherPlayerMP
                  EntityPlayerMP
                  EntityPlayerSP
                  ┗    EntityClientPlayerMP

INetworkManager:
INetworkManager
┗    MemoryConnection
      TcpConnection

建立一个我们自己的Packet:
在Forge中实现网络通信的手段有很多,在这里,我介绍一种方式来进行网络通信。既然我们使用Packet250CustomPayload这个封包来发送消息,为什么我们还需要创建自己的封包呢?其实,我们并不准备创建Minecraft原本的封包,而是准备对Packet250CustomPayload这个封包这个封包进行包装,使其更容易使用,并在发送和接收时自动对其进行转换。
在myFirstMod包下新建包Network,并新建类myFristModPacket。首先,我们需要发送三种类型的数据,和数据类型,所以先新建4个字段:
  1. public int packetType;
  2. public String[] dataString;
  3. public int[] dataInt;
  4. public byte[] dataByte;
复制代码
然后添加构造函数(别问我为啥要把那三个赋值null)
  1. public myFristModPacket()
  2. {
  3.     this.packetType = 0;
  4.     this.dataString = null;
  5.     this.dataInt = null;
  6.     this.dataByte = null;
  7. }
复制代码
现在,我们开始实现转换我们自己的封包到Minecraft的封包的方法:
  1. public Packet toPacket()
  2. {
  3.     //因为最终我们需要Byte数组,所以使用ByteArrayOutputStream。
  4.     ByteArrayOutputStream bytes = new ByteArrayOutputStream();
  5.     //因为我们要写入其他类型的数据,所以使用DataOutputStream。
  6.     DataOutputStream data = new DataOutputStream(bytes);
  7.     //新建一个MC的封包。
  8.     Packet250CustomPayload pkt = new Packet250CustomPayload();
  9.     try
  10.     {
  11.         //通过这个函数来写入数据,等会再实现。
  12.         writeData(data);
  13.     }
  14.     catch (IOException e)
  15.     {
  16.         e.printStackTrace();
  17.     }
  18.     //封包使用的管道
  19.     pkt.channel = "myFirstMod";
  20.     //数据
  21.     pkt.data = bytes.toByteArray();
  22.     //封包大小
  23.     pkt.length = pkt.data.length;
  24.     return pkt;
  25. }
复制代码
然后,我们实现转换Minecraft的封包到我们自己的封包的方法:
  1. public static myFristModPacket parse(byte[] bytes) throws IOException
  2. {
  3.     //我们需要DataInputStream作为输入。
  4.     DataInputStream data = new DataInputStream(new ByteArrayInputStream(bytes));
  5.     //新建一个我们自己的封包。
  6.     myFristModPacket pkt = new myFristModPacket();
  7.     //通过这个函数来读取数据,等会再实现。
  8.     pkt.readData(data);
  9.     return pkt;
  10. }
复制代码
接着创建writeData函数:
  1. private void writeData(DataOutputStream data) throws IOException
  2. {
  3.     //首先写入封包类型
  4.     data.writeInt(this.packetType);
  5.     if (this.dataString != null)
  6.     {
  7.         //dataString不为null则写入其长度
  8.         data.writeByte(this.dataString.length);
  9.     }
  10.     else
  11.     {
  12.         //dataString为null则写入0
  13.         data.writeByte(0);
  14.     }
  15.     if (this.dataInt != null)
  16.     {
  17.         //dataInt不为null则写入其长度
  18.         data.writeByte(this.dataInt.length);
  19.     }
  20.     else
  21.     {
  22.         //dataInt为null则写入0
  23.         data.writeByte(0);
  24.     }
  25.      if (this.dataByte != null)
  26.     {
  27.         //dataByte不为null则写入其长度
  28.         data.writeByte(this.dataByte.length);
  29.     }
  30.     else
  31.     {
  32.         //dataByte为null则写入0
  33.         data.writeByte(0);
  34.     }
  35.     //写入dataString数组的所有内容
  36.     if (this.dataString != null)
  37.     {
  38.         for (String s : this.dataString)
  39.         {
  40.             data.writeUTF(s);
  41.         }
  42.     }
  43.     //写入dataInt数组的所有内容
  44.     if (this.dataInt != null)
  45.     {
  46.         for (int i : this.dataInt)
  47.         {
  48.             data.writeInt(i);
  49.         }
  50.     }
  51.     //写入dataByte数组的所有内容
  52.     if (this.dataByte != null)
  53.     {
  54.         for (byte b : this.dataByte)
  55.         {
  56.             data.writeByte(b);
  57.         }
  58.     }
  59. }
复制代码
最后创建readData函数:
  1. private void readData(DataInputStream data) throws IOException
  2. {
  3.     //首先读取封包类型
  4.     this.packetType = data.readInt();
  5.     //读取String数组长度
  6.     byte nString = data.readByte();
  7.     //读取int数组长度
  8.     byte nInt = data.readByte();
  9.     //读取byte数组长度
  10.     byte nByte = data.readByte();
  11.     if ((nString > 128) || (nInt > 128) || (nByte > 128) || (nString < 0) || (nInt < 0) || (nByte < 0))
  12.     {
  13.         //如果其中任何一个长度大于128或小于0,则抛出IOException异常
  14.         throw new IOException("");
  15.     }
  16.     if (nString == 0)
  17.     {
  18.         //如果String长度为0,则dataString为null
  19.         this.dataString = null;
  20.     }
  21.     else
  22.     {
  23.         //如果String长度不为0,则创建一个String数组,长度为nString
  24.         this.dataString = new String[nString];
  25.         //读取String数据
  26.         for (int k = 0; k < nString; k++)
  27.         {
  28.             this.dataString[k] = data.readUTF();
  29.         }
  30.     }
  31.     if (nInt == 0)
  32.     {
  33.         //如果int长度为0,则dataInt为null
  34.         this.dataInt = null;
  35.     }
  36.     else
  37.     {
  38.         //如果int长度不为0,则创建一个int数组,长度为nInt
  39.         this.dataInt = new int[nInt];
  40.         //读取int数据
  41.         for (int k = 0; k < nInt; k++)
  42.         {
  43.             this.dataInt[k] = data.readInt();
  44.         }
  45.     }
  46.     if (nByte == 0)
  47.     {
  48.         //如果byte长度为0,则dataByte为null
  49.         this.dataByte = null;
  50.     }
  51.     else
  52.     {
  53.         //如果byte长度不为0,则创建一个byte数组,长度为nByte
  54.         this.dataByte = new byte[nByte];
  55.         //读取byte数据
  56.         for (int k = 0; k < nByte; k++)
  57.         {
  58.             this.dataByte[k] = data.readByte();
  59.         }
  60.     }
  61. }
复制代码
建立数据接收转换系统:
在第二章时,我们留下了一个没有完成的类:PacketHandler,在Forge中,通过在MOD启动类中添加NetworkMod注解,并设置其channels和packetHandler属性的方式来注册一个通信管道,并和packetHandler关联,来实现接收数据。
首先在myFirstMod.Network包下新建类PacketHandler,并使其实现IPacketHandler接口。然后实现onPacketData函数:
  1. public void onPacketData(INetworkManager manager, Packet250CustomPayload _packet, Player player)
  2. {
  3.     try
  4.     {
  5.         //首先装换Packet250CustomPayload到我们自己的封包
  6.         myFristModPacket packet = myFristModPacket.parse(_packet.data);
  7.         if(player instanceof EntityPlayerMP)
  8.         {
  9.             //如果player参数是EntityPlayerMP的实例,那么调用handlePacketFromClient方法
  10.             mod_myFirstMod.handlePacketFromClient(packet, (EntityPlayerMP)player);
  11.         }
  12.         else
  13.         {
  14.             //反之调用handlePacketFromServer方法
  15.             mod_myFirstMod.handlePacketFromServer(packet);
  16.         }
  17.     }
  18.     catch (Exception e)
  19.     {
  20.         
  21.     }
  22. }
复制代码
然后在mod_myFirstMod类中添加函数:
  1. public static void handlePacketFromClient(myFristModPacket packet, EntityPlayerMP player)
  2. {
  3.    
  4. }
  5. @SideOnly(Side.CLIENT)
  6. public static void handlePacketFromServer(myFristModPacket packet)
  7. {
  8.    
  9. }
复制代码
总结:
这一章里,我们为我们的MOD建立了一个完整的网络通信环境,在下一章里我们将实现刷怪笼的GUI,并运用这个环境来实现数据通信。