概述
Minecraft和Forge本身托管了大部分的网络同步操作,不过很不幸的是,诸如Capability这种自定义数据,Forge没有提供自动同步的方法,所以仍然需要手动同步。在本部分,作者将带领读者体验Forge提供的一种十分简单方便的异步网络框架,也就是SimpleImpl。
自定义数据包
作为演示,这里教程使用“热键绑定”一节中新添加的用于显示游戏时间的代码,并对其做出修改,使得它可以显示上一节新添加的Capability对应存储的位置历史记录:
src/main/java/com/github/ustc_zzzz/fmltutor/common/EventLoader.java(部分):
@SideOnly(Side.CLIENT)
@SubscribeEvent
public void onKeyInput(InputEvent.KeyInputEvent event)
{
if (KeyLoader.showTime.isPressed())
{
EntityPlayer player = Minecraft.getMinecraft().thePlayer;
World world = Minecraft.getMinecraft().theWorld;
player.addChatMessage(new ChatComponentTranslation("chat.fmltutor.time", world.getTotalWorldTime()));
if (player.hasCapability(CapabilityLoader.positionHistory, null))
{
player.addChatMessage(new ChatComponentTranslation("commands.position.history"));
IPositionHistory histories = player.getCapability(CapabilityLoader.positionHistory, null);
for (Vec3 vec3 : histories.getHistories())
{
if (vec3 != null)
{
player.addChatMessage(new ChatComponentText(vec3.toString()));
}
}
}
}
}
代码本身很容易理解,不过很不幸的是在实际测试中,是什么位置历史记录都显示不出来的。这里的问题就是:因为对于Capability对应的存储数据的操作是在服务端完成的,而按键这一行为是在客户端完成的,自然客户端获取不到服务端的数据,反之亦然。
网络数据传输的一层就是数据包(Packet),实际上我们需要做的也就是操纵一个数据包,而数据包的本质就是字节流。直接操纵字节流显然不够明智,创建一个辅助类去序列化(Serialize)和反序列化(Deserialize)才是比较明智的选择。Forge为我们提供了一个名为IMessage
的接口,我们首先需要实现这个接口。
新建包com.github.ustc_zzzz.fmltutor.network
,并在其中创建一个MessagePositionHistory
类:
src/main/java/com/github/ustc_zzzz/fmltutor/network/MessagePositionHistory.java:
package com.github.ustc_zzzz.fmltutor.network;
import io.netty.buffer.ByteBuf;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraftforge.fml.common.network.ByteBufUtils;
import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
public class MessagePositionHistory implements IMessage
{
public NBTTagCompound nbt;
@Override
public void fromBytes(ByteBuf buf)
{
nbt = ByteBufUtils.readTag(buf);
}
@Override
public void toBytes(ByteBuf buf)
{
ByteBufUtils.writeTag(buf, nbt);
}
}
IMessage
接声明了两个方法,第一个方法用于从ByteBuf
,也就是刚刚我们说的字节流读入数据,第二个方法用于把数据写入ByteBuf
。
ByteBuf
本身提供了很多方法用于向其中写入数据,它们是一串以read
和write
开头的方法,可以用于存储整数、浮点数、字节数组等等,只要按顺序读写就可以了,比如如果我们想要往其中写入两个整数和一个浮点数,我们可能会这么做:
public int numberA;
public int numberB;
public double numberC;
@Override
public void fromBytes(ByteBuf buf)
{
numberA = buf.readInt();
numberB = buf.readInt();
numberC = buf.readDouble();
}
@Override
public void toBytes(ByteBuf buf)
{
buf.writeInt(numberA);
buf.writeInt(numberB);
buf.writeDouble(numberC);
}
不过我们也可以看得出这显然不够用,很多时候我们还需要写入字符串、ItemStack
、或者一个NBT标签(本节教程的情形)。这时候我们就需要用到Forge为我们提供的帮助类ByteBufUtils
了,上面的例子已经展示了如何使用ByteBufUtils
类写入一个NBTTagCompound
。
上面我们提到了客户端获取不到服务端的数据这一点,自然,我们需要服务端给客户端发数据包来解决这一问题,同时,客户端就必须要提供一个类来处理服务端发过来的包。Forge提供的接口是IMessageHandler
,我们在MessagePositionHistory
中创建一个内部类,并实现这个接口:
src/main/java/com/github/ustc_zzzz/fmltutor/network/MessagePositionHistory.java(部分):
public static class Handler implements IMessageHandler<MessagePositionHistory, IMessage>
{
@Override
public IMessage onMessage(MessagePositionHistory message, MessageContext ctx)
{
if (ctx.side == Side.CLIENT)
{
final NBTBase nbt = message.nbt.getTag("histories");
Minecraft.getMinecraft().addScheduledTask(new Runnable()
{
@Override
public void run()
{
EntityPlayer player = Minecraft.getMinecraft().thePlayer;
if (player.hasCapability(CapabilityLoader.positionHistory, null))
{
IPositionHistory histories = player.getCapability(CapabilityLoader.positionHistory, null);
IStorage<IPositionHistory> storage = CapabilityLoader.positionHistory.getStorage();
storage.readNBT(CapabilityLoader.positionHistory, histories, null, nbt);
}
}
});
}
return null;
}
}
我们先来说说伴随IMessageHandler
的两个泛型参数:
public static class Handler implements IMessageHandler<MessagePositionHistory, IMessage>
- 首先我们需要确定收过来的包的类型,这就是第一个泛型参数
- 一端在收到另一端的包时常常会有把包发回另一端的需要,这个发回的包的类型就是第二个泛型参数,不过这里我们不需要发回数据包
onMessage
方法就是用于处理这个数据包(第一个参数)的。它的返回值就是发回的数据包。因为这里我们不需要发回数据包,所以返回null
。
我们接着讲一讲第二个参数。MessageContext
类提供了一个side
变量,用于判定接收方是客户端还是服务端,这里自然是客户端,但是为了保险我们还是加了一个条件判断了下。
然后是里面的代码:
final NBTBase nbt = message.nbt.getTag("histories");
Minecraft.getMinecraft().addScheduledTask(new Runnable()
{
@Override
public void run()
{
EntityPlayer player = Minecraft.getMinecraft().thePlayer;
if (player.hasCapability(CapabilityLoader.positionHistory, null))
{
IPositionHistory histories = player.getCapability(CapabilityLoader.positionHistory, null);
IStorage<IPositionHistory> storage = CapabilityLoader.positionHistory.getStorage();
storage.readNBT(CapabilityLoader.positionHistory, histories, null, nbt);
}
}
});
这一段代码自然是用于同步数据到Capability的。剩下的代码都很简单,不过这行代码是怎么一回事呢?
Minecraft.getMinecraft().addScheduledTask(new Runnable()
这是因为从Minecraft 1.8开始,Minecraft的所有网络操作都是在一个单独的网络线程中进行,这会导致其没有办法和游戏中的大多数对象交互。所以这里需要调用IThreadListener
的addScheduledTask
方法,并传入一个Runnable
。客户端的Minecraft
类实现了这个接口,而服务端的MinecraftServer
和WorldServer
类分别实现了这个接口。
SimpleNetworkWrapper
接下来我们需要做的事情,就是把上面我们做出来的这些东西全都绑定到特定的频道(Channel)上,Forge简单包装了一下这个频道,也就是SimpleNetworkWrapper
类。
在包com.github.ustc_zzzz.fmltutor.network
下新建类NetworkLoader
:
src/main/java/com/github/ustc_zzzz/fmltutor/network/NetworkLoader.java:
package com.github.ustc_zzzz.fmltutor.network;
import com.github.ustc_zzzz.fmltutor.FMLTutor;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.network.NetworkRegistry;
import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler;
import net.minecraftforge.fml.common.network.simpleimpl.SimpleNetworkWrapper;
import net.minecraftforge.fml.relauncher.Side;
public class NetworkLoader
{
public static SimpleNetworkWrapper instance = NetworkRegistry.INSTANCE.newSimpleChannel(FMLTutor.MODID);
private static int nextID = 0;
public NetworkLoader(FMLPreInitializationEvent event)
{
registerMessage(MessagePositionHistory.Handler.class, MessagePositionHistory.class, Side.CLIENT);
}
private static <REQ extends IMessage, REPLY extends IMessage> void registerMessage(
Class<? extends IMessageHandler<REQ, REPLY>> messageHandler, Class<REQ> requestMessageType, Side side)
{
instance.registerMessage(messageHandler, requestMessageType, nextID++, side);
}
}
第一件事就是调用NetworkRegistry
的newSimpleChannel
方法,并传入Mod id以生成一个新的SimpleNetworkWrapper
:
public static SimpleNetworkWrapper instance = NetworkRegistry.INSTANCE.newSimpleChannel(FMLTutor.MODID);
第二件事就是用SimpleNetworkWrapper
的registerMessage
方法绑定IMessage
和其对应的IMessageHandler
,我们先看一下这个方法的声明:
public <REQ extends IMessage, REPLY extends IMessage> void registerMessage(
Class<? extends IMessageHandler<REQ, REPLY>> messageHandler, Class<REQ> requestMessageType, int discriminator, Side side)
- 第二个参数表示用于发送的
IMessage
的class - 第一个参数表示用于接收的
IMessageHandler
- 第四个参数表示接收方为服务端还是客户端,这里因为是客户端接收,所以传入一个
Side.CLIENT
- 第三个参数表示这一频道的唯一id,一个比较好的做法是静态存储下一个id的值,每注册一次就加一,正如上面提供的示例代码一样,教程就是这么做的
如果既有服务端向客户端发送信息的需要,又有客户端向服务端发送信息的需要,则可以使用Side.CLIENT
和Side.SERVER
注册两个IMessageHandler
,但仍需要保证第三个参数表示的id是唯一的。
最后扔进CommonProxy
的preInit
阶段:
src/main/java/com/github/ustc_zzzz/fmltutor/common/CommonProxy.java(部分):
public void preInit(FMLPreInitializationEvent event)
{
new ConfigLoader(event);
new CapabilityLoader(event);
new CreativeTabsLoader(event);
new FluidLoader(event);
new ItemLoader(event);
new BlockLoader(event);
new OreDictionaryLoader(event);
new PotionLoader(event);
new EntityLoader(event);
new TileEntityLoader(event);
new NetworkLoader(event);
}
发送数据包
IMessageHandler
已经接管了接收数据包的任务,那么是谁接管发送数据包的任务呢?
SimpleNetworkWrapper
提供了若干个方法用于客户端和服务端发送数据包:
sendToAll
方法用于服务端发送数据包给所有玩家sendTo
方法用于服务端发送数据包给特定玩家sendToAllAround
方法用于服务端发送数据包给特定位置和特定半径确定的范围内的所有玩家sendToDimension
方法用于服务端发送数据包给特定维度的所有玩家sendToServer
方法用于客户端发送数据包给服务器
这里作为实体(Entity
类)的Capability
,到底应该什么时候发送数据包呢?首先,我们要在实体在世界上生成(对于玩家是进入世界)的时候发送数据包:
src/main/java/com/github/ustc_zzzz/fmltutor/common/EventLoader.java(部分):
@SubscribeEvent
public void onEntityJoinWorld(EntityJoinWorldEvent event)
{
if (!event.world.isRemote && event.entity instanceof EntityPlayer)
{
EntityPlayerMP player = (EntityPlayerMP) event.entity;
if (player.hasCapability(CapabilityLoader.positionHistory, null))
{
MessagePositionHistory message = new MessagePositionHistory();
IPositionHistory histories = player.getCapability(CapabilityLoader.positionHistory, null);
IStorage<IPositionHistory> storage = CapabilityLoader.positionHistory.getStorage();
message.nbt = new NBTTagCompound();
message.nbt.setTag("histories", storage.writeNBT(CapabilityLoader.positionHistory, histories, null));
NetworkLoader.instance.sendTo(message, player);
}
}
}
还有就是数据发生变动的时候发送数据包(这里就是执行命令的时候发送):
src/main/java/com/github/ustc_zzzz/fmltutor/command/CommandPosition.java(部分):
@Override
public void processCommand(ICommandSender sender, String[] args) throws CommandException
{
if (args.length > 1)
{
throw new WrongUsageException("commands.position.usage");
}
else
{
EntityPlayerMP entityPlayerMP = args.length > 0 ? CommandBase.getPlayer(sender, args[0])
: CommandBase.getCommandSenderAsPlayer(sender);
Vec3 pos = entityPlayerMP.getPositionVector();
if (entityPlayerMP == sender && entityPlayerMP.hasCapability(CapabilityLoader.positionHistory, null))
{
sender.addChatMessage(new ChatComponentTranslation("commands.position.history"));
IPositionHistory histories = entityPlayerMP.getCapability(CapabilityLoader.positionHistory, null);
for (Vec3 vec3 : histories.getHistories())
{
if (vec3 != null)
{
sender.addChatMessage(new ChatComponentText(vec3.toString()));
}
}
histories.pushHistory(pos);
MessagePositionHistory message = new MessagePositionHistory();
IStorage<IPositionHistory> storage = CapabilityLoader.positionHistory.getStorage();
message.nbt = new NBTTagCompound();
message.nbt.setTag("histories", storage.writeNBT(CapabilityLoader.positionHistory, histories, null));
NetworkLoader.instance.sendTo(message, entityPlayerMP);
}
sender.addChatMessage(new ChatComponentTranslation("commands.position.success", entityPlayerMP.getName(),
pos, entityPlayerMP.worldObj.provider.getDimensionName()));
}
}
打开游戏试试吧~