网络

在这一章将会讲述我的世界中有关网络的应用,将会添加一个新的物品,客户端使用这个物品与服务端进行通信

创建新物品

首先需要创建一个新的物品,在这里我不提供代码,只提供一个物品材质以及物品的名字:RemoteControl(java类等记得命名为这个,方便后续说明),当运行完后具体的界面应该为:

image-20220226192412617

代码

首先需要创建文件名为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是方便在模组其他地方发送消息

流程讲解

大致流程:

  1. 客户端右击遥控器,发送消息给服务端(在这里其实消息内部不需要东西,可以新建一个专门用于客户端请求数据的消息类)
  2. 服务端接收到消息,发送客户端请求的数据(在这里为是否激活以及玩家uuid)给客户端
  3. 客户端接收到消息,打印到控制台中

消息发送流程(以物品使用这个过程为例,发送和接受):

  1. 物品被使用,调用了sendToServer
  2. sendToServer使用encode,将数据写入缓存中,发送给服务端
  3. 服务端接收到消息,调用decode,提取缓存内容到一个消息类中
  4. 解码(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());
}

加入后同时运行两个端,一个服务端一个客户端

image-20220226201324780

运行其中的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加个断点,右击遥控器,然后你就会发现在客户端里头世界仿佛停止了,这就跟在玩多人游戏时,卡或者突然断网一个道理