利用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个字段:
- public int packetType;
- public String[] dataString;
- public int[] dataInt;
- public byte[] dataByte;
- public myFristModPacket()
- {
- this.packetType = 0;
- this.dataString = null;
- this.dataInt = null;
- this.dataByte = null;
- }
- public Packet toPacket()
- {
- //因为最终我们需要Byte数组,所以使用ByteArrayOutputStream。
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
- //因为我们要写入其他类型的数据,所以使用DataOutputStream。
- DataOutputStream data = new DataOutputStream(bytes);
- //新建一个MC的封包。
- Packet250CustomPayload pkt = new Packet250CustomPayload();
- try
- {
- //通过这个函数来写入数据,等会再实现。
- writeData(data);
- }
- catch (IOException e)
- {
- e.printStackTrace();
- }
- //封包使用的管道
- pkt.channel = "myFirstMod";
- //数据
- pkt.data = bytes.toByteArray();
- //封包大小
- pkt.length = pkt.data.length;
- return pkt;
- }
- public static myFristModPacket parse(byte[] bytes) throws IOException
- {
- //我们需要DataInputStream作为输入。
- DataInputStream data = new DataInputStream(new ByteArrayInputStream(bytes));
- //新建一个我们自己的封包。
- myFristModPacket pkt = new myFristModPacket();
- //通过这个函数来读取数据,等会再实现。
- pkt.readData(data);
- return pkt;
- }
- private void writeData(DataOutputStream data) throws IOException
- {
- //首先写入封包类型
- data.writeInt(this.packetType);
- if (this.dataString != null)
- {
- //dataString不为null则写入其长度
- data.writeByte(this.dataString.length);
- }
- else
- {
- //dataString为null则写入0
- data.writeByte(0);
- }
- if (this.dataInt != null)
- {
- //dataInt不为null则写入其长度
- data.writeByte(this.dataInt.length);
- }
- else
- {
- //dataInt为null则写入0
- data.writeByte(0);
- }
- if (this.dataByte != null)
- {
- //dataByte不为null则写入其长度
- data.writeByte(this.dataByte.length);
- }
- else
- {
- //dataByte为null则写入0
- data.writeByte(0);
- }
- //写入dataString数组的所有内容
- if (this.dataString != null)
- {
- for (String s : this.dataString)
- {
- data.writeUTF(s);
- }
- }
- //写入dataInt数组的所有内容
- if (this.dataInt != null)
- {
- for (int i : this.dataInt)
- {
- data.writeInt(i);
- }
- }
- //写入dataByte数组的所有内容
- if (this.dataByte != null)
- {
- for (byte b : this.dataByte)
- {
- data.writeByte(b);
- }
- }
- }
- private void readData(DataInputStream data) throws IOException
- {
- //首先读取封包类型
- this.packetType = data.readInt();
- //读取String数组长度
- byte nString = data.readByte();
- //读取int数组长度
- byte nInt = data.readByte();
- //读取byte数组长度
- byte nByte = data.readByte();
- if ((nString > 128) || (nInt > 128) || (nByte > 128) || (nString < 0) || (nInt < 0) || (nByte < 0))
- {
- //如果其中任何一个长度大于128或小于0,则抛出IOException异常
- throw new IOException("");
- }
- if (nString == 0)
- {
- //如果String长度为0,则dataString为null
- this.dataString = null;
- }
- else
- {
- //如果String长度不为0,则创建一个String数组,长度为nString
- this.dataString = new String[nString];
- //读取String数据
- for (int k = 0; k < nString; k++)
- {
- this.dataString[k] = data.readUTF();
- }
- }
- if (nInt == 0)
- {
- //如果int长度为0,则dataInt为null
- this.dataInt = null;
- }
- else
- {
- //如果int长度不为0,则创建一个int数组,长度为nInt
- this.dataInt = new int[nInt];
- //读取int数据
- for (int k = 0; k < nInt; k++)
- {
- this.dataInt[k] = data.readInt();
- }
- }
- if (nByte == 0)
- {
- //如果byte长度为0,则dataByte为null
- this.dataByte = null;
- }
- else
- {
- //如果byte长度不为0,则创建一个byte数组,长度为nByte
- this.dataByte = new byte[nByte];
- //读取byte数据
- for (int k = 0; k < nByte; k++)
- {
- this.dataByte[k] = data.readByte();
- }
- }
- }
在第二章时,我们留下了一个没有完成的类:PacketHandler,在Forge中,通过在MOD启动类中添加NetworkMod注解,并设置其channels和packetHandler属性的方式来注册一个通信管道,并和packetHandler关联,来实现接收数据。
首先在myFirstMod.Network包下新建类PacketHandler,并使其实现IPacketHandler接口。然后实现onPacketData函数:
- public void onPacketData(INetworkManager manager, Packet250CustomPayload _packet, Player player)
- {
- try
- {
- //首先装换Packet250CustomPayload到我们自己的封包
- myFristModPacket packet = myFristModPacket.parse(_packet.data);
- if(player instanceof EntityPlayerMP)
- {
- //如果player参数是EntityPlayerMP的实例,那么调用handlePacketFromClient方法
- mod_myFirstMod.handlePacketFromClient(packet, (EntityPlayerMP)player);
- }
- else
- {
- //反之调用handlePacketFromServer方法
- mod_myFirstMod.handlePacketFromServer(packet);
- }
- }
- catch (Exception e)
- {
-
- }
- }
- public static void handlePacketFromClient(myFristModPacket packet, EntityPlayerMP player)
- {
-
- }
- @SideOnly(Side.CLIENT)
- public static void handlePacketFromServer(myFristModPacket packet)
- {
-
- }
这一章里,我们为我们的MOD建立了一个完整的网络通信环境,在下一章里我们将实现刷怪笼的GUI,并运用这个环境来实现数据通信。