网络
在这一章将会讲述我的世界中有关网络的应用,将会添加一个新的物品,客户端使用这个物品与服务端进行通信
创建新物品
首先需要创建一个新的物品,在这里我不提供代码,只提供一个物品材质以及物品的名字:RemoteControl(java类等记得命名为这个,方便后续说明),当运行完后具体的界面应该为:

代码
首先需要创建文件名为MyMessageHandler,放在examplemod/network下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| public class MyMessageHandler {
private static final String PROTOCOL_VERSION = "1"; protected static final SimpleChannel INSTANCE = NetworkRegistry.newSimpleChannel( new ResourceLocation(Reference.MOD_ID,"main"), ()->PROTOCOL_VERSION, PROTOCOL_VERSION::equals, PROTOCOL_VERSION::equals ); protected int id = 0;
public void sendOnePlayer(ServerPlayer serverPlayer,IMessage message){ INSTANCE.send(PacketDistributor.PLAYER.with(()->serverPlayer),message); }
public void sendToLevelChunk(LevelChunk levelChunk, IMessage message){ INSTANCE.send(PacketDistributor.TRACKING_CHUNK.with(()->levelChunk),message); } public void sendAll(IMessage message){ INSTANCE.send(PacketDistributor.ALL.noArg(),message); }
public void sendMessageToServer(IMessage msg) { INSTANCE.sendToServer(msg); }
public final void sendToAllAround(IMessage message, PacketDistributor.TargetPoint point) { INSTANCE.send(PacketDistributor.NEAR.with(() -> point), message); }
public int addId(){ return id++; }
public void register(){ INSTANCE.registerMessage(addId(), PlayerEventPacket.class, PlayerEventPacket::encode, PlayerEventPacket::decode, PlayerEventPacket::handle); }
}
|
其次创建IMessage接口以及PlayerEventPacket类,同样在network下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public interface IMessage { }
public class PlayerEventPacket implements IMessage{
static void encode(PlayerEventPacket msg, FriendlyByteBuf buf) { buf.writeBoolean(msg.isActivate); buf.writeUUID(msg.uuid); }
static PlayerEventPacket decode(FriendlyByteBuf buf) { return new PlayerEventPacket(buf.readUUID(),buf.readBoolean()); }
public static void handle(final PlayerEventPacket msg, Supplier<NetworkEvent.Context> contextSupplier) { final NetworkEvent.Context ctx = contextSupplier.get(); ctx.enqueueWork(() -> ExampleMod.proxy.handlePlayerEventPacket(msg,ctx.getSender())); ctx.setPacketHandled(true); }
public final boolean isActivate; public final UUID uuid;
public PlayerEventPacket(UUID uuid,boolean isActivate) { this.isActivate = isActivate; this.uuid = uuid; }
}
|
修改proxy下的CommonProxy,ClientProxy与ServerProxy,分别添加一个方法,从上往下为Common,Client,Server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public void handlePlayerEventPacket(PlayerEventPacket msg, ServerPlayer player) { }
@Override public void handlePlayerEventPacket(PlayerEventPacket msg, ServerPlayer player) { if(msg.isActivate){ LOGGER.atInfo().log(Minecraft.getInstance().player.getName().getContents()+" is activate"); }else{ LOGGER.atInfo().log(Minecraft.getInstance().player.getName().getContents()+" is not activate"); } }
@Override public void handlePlayerEventPacket(PlayerEventPacket msg,ServerPlayer player) { final Boolean[] isActivate = new Boolean[1]; LazyOptional<IPlayer> capability = player.getCapability(SuperPlayer.CAPABILITY); capability.ifPresent((cap) -> { isActivate[0] = cap.isActivated(); }); dispatcher.sendOnePlayer(player,new PlayerEventPacket(msg.uuid,isActivate[0])); }
|
在ClientProxy上添加LOGGER,为
1
| private static final Logger LOGGER = LogManager.getLogger();
|
最后,在ExampleMod添加/覆盖
1 2 3 4 5
| public static final MyMessageHandler dispatcher = new MyMessageHandler(); public ExampleMod() { this.init(); dispatcher.register(); }
|
代码分析
MyMessageHandler
这一个类为最重要的一个类,forge将网络抽象成了消息队列,MyMessageHandler中的INSTANCE即为创建了一个简单的通信,通过这个可以与服务端/客户端进行通信,上方的PROTOCOL_VERSION为你定义的通信协议版本,不同版本会拒绝访问
在类中的方法上面已经写好了发送消息的解释,最下面的register为注册消息类,每个消息类需要一个唯一的id传入,在这里可以直接id++。消息类需要传入一个静态的编码,解码,和处理的方法
PlayerEventPacket
- encode: 将一个消息写入缓存中
- decode:从缓存中取出内容
- handle:当接收到消息后,如何对消息进行处理,在这里是直接交给了代理
备注:IMessage只是用来抽象消息类的,方便定义自己的消息
代理
不同端需要不同的代理,在服务端(物理)中的代理为ServerProxy,在这里对消息的处理是发送给发送方关于他是否激活的信息,在客户端(物理)中的代理为ClientProxy,在这里获得了玩家名字以及消息中的是否激活,直接打印出来
主类中的dispatcher
主类中的dispatcher是方便在模组其他地方发送消息
流程讲解
大致流程:
- 客户端右击遥控器,发送消息给服务端(在这里其实消息内部不需要东西,可以新建一个专门用于客户端请求数据的消息类)
- 服务端接收到消息,发送客户端请求的数据(在这里为是否激活以及玩家uuid)给客户端
- 客户端接收到消息,打印到控制台中
消息发送流程(以物品使用这个过程为例,发送和接受):
- 物品被使用,调用了sendToServer
- sendToServer使用encode,将数据写入缓存中,发送给服务端
- 服务端接收到消息,调用decode,提取缓存内容到一个消息类中
- 解码(decode)后,调用handler对接收到的数据进行处理(在这里可以是用enqueueWork,这个将会在之后的某个时间调用里面的方法)
给物品添加发送消息功能
将以下代码放入RemoteControl中
1 2 3 4 5 6 7 8 9 10
| @Override public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) { LazyOptional<IPlayer> capability = player.getCapability(SuperPlayer.CAPABILITY); capability.ifPresent((cap) -> { if(level.isClientSide){ ExampleMod.dispatcher.sendMessageToServer(new PlayerEventPacket(player.getUUID(),cap.isActivated())); } }); return InteractionResultHolder.consume(player.getMainHandItem()); }
|
加入后同时运行两个端,一个服务端一个客户端

运行其中的runServer和runClient,推荐以Debug形式运行,运行后客户端需要加入127.0.0.1:25565,加入后记得服务端给op权限: /op xxx
右击新物品Remote Control,能在runClient看见一个消息,如果在服务端的数据中,玩家已经被激活,则是Dev is activate,如果没有激活,则为 Dev is not activate,其中的Dev为客户端玩家名字,一般来讲跑的时候是Dev
一些坑及有趣的地方
在学习过程时在ClientProxy中打印是否激活的方法里头,我直接使用了本地的player,导致是否激活和服务端不匹配,后来发现,这个数据并没有被同步,所有数据应该为服务端发送,客户端接受,客户端不应对一些世界数据进行操作,在给玩家一些物品,buff等也应判断是否是服务端
在Server运行时,在ServerProxy中的handlePlayerEventPacket
加个断点,右击遥控器,然后你就会发现在客户端里头世界仿佛停止了,这就跟在玩多人游戏时,卡或者突然断网一个道理