本篇教程的视频

(待发布)

本篇教程的源代码

(待发布)

本篇教程目标

通过自定义网络包实现机器的开关

介绍

在前面的教程中,我们已经完成了一个完整的方块实体——矿机

现在,我们来初步了解一下自定义网络包的编写

我们将通过自定义网络包来实现矿机的开关功能,确切来说,我们将在GUI上加一个按钮,点击按钮后,客户端会发送一个自定义网络包到服务端,服务端接收到这个包后,会改变矿机的工作状态,并将新的状态同步给客户端

方块实体

首先,我们要改写一下之前写的PortableOriginiumRigBlockEntity

添加字段

这里我们先添加一个布尔值类型的字段,将由它来决定机器的开关

1
2
3
4
5
6
7
8
9
10
11
12
13
private boolean enable = true;

public void setEnable(boolean enable) {
this.enable = enable;
markDirty();
if (world != null) {
world.updateListeners(pos, world.getBlockState(pos), world.getBlockState(pos), 3);
}
}

public boolean getEnable() {
return this.enable;
}

另外,我们准备两个方法,分别用于获取和设置这个字段

特别的,设置字段方法需要额外设置一个触发数据同步的方法,以保证客户端这边的正常运行

改写 propertyDelegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
this.propertyDelegate = new PropertyDelegate() {
@Override
public int get(int index) {
return switch (index) {
...
case 2 -> PortableOriginiumRigBlockEntity.this.enable ? 1 : 0;
default -> 0;
};
}

...

@Override
public int size() {
return 3;
}
};

我们在propertyDelegate中增加了一个用于返回enable的值的语句,不过由于PropertyDelegate只能返回int类型,所以我们这里用三元运算符来判断enable的值,返回1或者0

后面GUI那边需要获取这个数据

改写tick方法

接下来我们在tick方法中增加以下语句

1
2
3
4
5
6
if (!be.getEnable()) {
be.isWorking = false;
world.updateListeners(pos, state, state, 3);
be.markDirty();
return;
}

这样我们矿机将根据enable的值,设置机器是否能工作,并触发数据同步

数据持久化

这里因为增加了一个新字段,我们也要将其进行数据持久化保存,不然重新进入游戏就默认是开启状态了

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void writeNbt(NbtCompound nbt) {
...
nbt.putBoolean("enable", this.enable);
}

@Override
public void readNbt(NbtCompound nbt) {
...
this.enable = nbt.getBoolean("enable");
}

自定义网络包

接下来我们就先注册一个自定义网络包,等会到GUI上会用到

这里我们先创建一个ModNetworking

1
2
3
public class ModNetworking {

}

我们先创建一个Identifier类型的字段,这个是数据包的ID,因为每个数据包都必须有自己独立的标识,这和那些物品、方块注册一样

1
public static final Identifier SWITCH_PACKET_ID = new Identifier(TutorialModRe.MOD_ID, "switch_packet");

然后,我们写一个register方法,模组主类将调用这个方法

1
2
3
4
5
6
7
8
9
10
11
12
public static void register() {
ServerPlayNetworking.registerGlobalReceiver(SWITCH_PACKET_ID, (minecraftServer, serverPlayerEntity, serverPlayNetworkHandler, packetByteBuf, packetSender) -> {
BlockPos pos = packetByteBuf.readBlockPos();
boolean enable = packetByteBuf.readBoolean();
minecraftServer.execute(() -> {
BlockEntity be = serverPlayerEntity.getWorld().getBlockEntity(pos);
if (be instanceof PortableOriginiumRigBlockEntity rig) {
rig.setEnable(enable);
}
});
});
}

这是一个很简单的网络包,简单来说就是当服务端收到SWITCH_PACKET_ID这个包时,执行后面的回调函数

ServerPlayNetworkingFabric为我们提供的API

在收到包后,我们先从packetByteBuf中读取数据,这里我们读取了一个BlockPos和一个布尔值

而后我们把后续对方块实体的操作通过minecraftServer.execute包起来,扔到服务器执行队列里

这是因为Minecraft的网络回调不一定运行在主线程中,但对方块实体、世界的操作必须在主线程中执行,所以这里的操作能避免线程安全问题

最后,我们判断这个方块实体是否是我们矿机的方块实体,如果是,就调用我们之前写的setEnable方法,设置矿机的开关状态

另外,模组主类的调用不要忘记

1
ModNetworking.register();

自定义GUI开关类

在写GUI相关逻辑之前,我们先创建一个自定义的按钮类,这样我们就可以用自定义的材质做我们的按钮

我们先创建一个SwitchButton类,继承自PressableWidget,并重写相应的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SwitchButton extends PressableWidget {
public SwitchButton(int i, int j, int k, int l, Text text) {
super(i, j, k, l, text);
}

@Override
public void onPress() {

}

@Override
protected void appendClickableNarrations(NarrationMessageBuilder builder) {

}
}

我们先添加四个字段

1
2
3
4
5
private static final Identifier SWITCH_ENABLE = new Identifier(TutorialModRe.MOD_ID, "textures/gui/button/switch_enable.png");
private static final Identifier SWITCH_DISABLE = new Identifier(TutorialModRe.MOD_ID, "textures/gui/button/switch_disable.png");

private final BooleanSupplier booleanSupplier;
private final Consumer<Boolean> onToggle;

上面两个是我们按钮的材质,分别是开启和关闭状态的按钮

下面两个是我们自定义的函数式接口,分别用于获取当前按钮的状态,以及点击按钮后执行的操作

接下来改写构造函数

1
2
3
4
5
public SwitchButton(int i, int j, BooleanSupplier booleanSupplier, Consumer<Boolean> onToggle) {
super(i, j, 16, 16, Text.empty());
this.booleanSupplier = booleanSupplier;
this.onToggle = onToggle;
}

我们可以将前面的kl两个参数去掉,直接指定我们按钮的大小为16*16,然后将booleanSupplieronToggle赋值给我们之前定义的字段

然后重写onPress方法

1
2
3
4
5
@Override
public void onPress() {
boolean newState = !booleanSupplier.getAsBoolean();
onToggle.accept(newState);
}

简单来说就是当按钮被点击时,获取当前按钮的状态取反,然后调用onToggle方法,将新的状态传入

另外我们还要重写renderButton方法,这个方法用于渲染按钮

1
2
3
4
5
6
7
8
9
@Override
protected void renderButton(DrawContext context, int mouseX, int mouseY, float delta) {
Identifier texture = booleanSupplier.getAsBoolean() ? SWITCH_ENABLE : SWITCH_DISABLE;
context.drawTexture(texture, getX(), getY(), 0, 0, this.width, this.height, 16, 16);

if (isHovered()) {
context.fill(getX(), getY(), getX() + this.width, getY() + this.height, 0x40FFFFFF);
}
}

首先要获取当前按钮的状态,根据状态选择不同的材质,然后调用context.drawTexture方法绘制按钮

另外在鼠标悬停时,我们还要绘制一个半透明的白色矩形,来提示玩家这个按钮是可以点击的

最后还有一个appendClickableNarrations方法,这个方法用于为按钮添加可点击的叙述,不过它是在无障碍模式下使用的,我们这里就不写了,直接空着

添加GUI按钮

我们先到PortableOriginiumRigScreenHandler类中添加一个方法,用于获取矿机的开关状态

1
2
3
public boolean isEnabled() {
return propertyDelegate.get(2) == 1;
}

另外记得吧构造函数的ArrayPropertyDelegate大小改一下,否则会崩溃

1
2
3
4
public PortableOriginiumRigScreenHandler(int syncId, PlayerInventory playerInventory, PacketByteBuf buf) {
this(syncId, playerInventory, Objects.requireNonNull(getClientEntity(playerInventory, buf)),
new ArrayPropertyDelegate(3));
}

接下来我们到PortableOriginiumRigScreen类中添加按钮

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void init() {
super.init();
this.addDrawableChild(new SwitchButton(x + 150, y + 30, handler::isEnabled,
button -> {
PacketByteBuf buf = new PacketByteBuf(Unpooled.buffer());
buf.writeBlockPos(entity.getPos());
boolean newEnableState = !handler.isEnabled();
buf.writeBoolean(newEnableState);
ClientPlayNetworking.send(ModNetworking.SWITCH_PACKET_ID, buf);
}));
}

这里我们重写了init方法,在里面添加了一个SwitchButton按钮,按钮的位置是相对于GUI的左上角的偏移量

根据handler::isEnabled来获取当前矿机的开关状态,取反后将新的状态写入到PacketByteBuf中,并发送给服务端

服务端接收到这个包后,会调用我们之前写的ModNetworking.register方法中的回调函数,改变矿机的开关状态,并同步给客户端