网络包
与其他教程可能略有不同的是,我们将会使用T88的网络包注解处理器来完成这一部分。
如果你更喜欢使用纯原版内容来编写网络包,Fledge的Boson教程是一个非常不错的选择。不过你需要注意其中使用的是mcp表。
在服务端与客户端
一章提到过,数据并不会自己在两端之间同步,而在MC没有提供对应的同步方法的情况下,我们就需要编写自己的网络包来实现同步。
- 1.20.4
随着Minecraft的改动,NeoForge对网络包系统进行了重构,现在网络包必须实现CustomPacketPayload
接口,而且采用了与之前不同的注册系统。
幸运的是,T88的注解处理器将会为你 完成大部分的变更,你也不需要自己实现CustomPacketPayload
(不过这样会使得你只能使用NetWorkHelper
来帮助你把网络包转换为生成的代理网络包类来进行发包)。需要你手动进行的变更将在下文提到。
将你的网络包转换为生成的代理网络包的过程可能会出现问题,你需要在开发时关注日志中有无报错信息。另外这个转换步骤将会消耗一点点性能,你可能需要关注其具体的影响。
需要注意,T88的注解处理器目前只支持play阶段的网络包,暂不支持配置阶段的网络包。
准备工作
你可以先阅读-5 lib-mod
的使用这个lib-mod
部分,了解更多关于添加依赖的知识。
首先你需要在T88的CurseForge文件页面找到右侧的项目编号Project ID
,它应该始终是663112
。然后在文件列表中找到最新的不带source
字样的jar,将鼠标置于其上,在浏览器的下方便会显示出这个jar文件的链接,链接的最后是它的编号,记下它。
对于正在写这篇教程的我,现在是2023年3月6日,此时最新版本的T88是0.2.34,它对应的t88-0.2.34.jar
编号是4404259
,没有对应的source
jar。对于你而言,一般选择最新版本的T88即可。
如果你 阅读了lib-mod
一节,你就会知道如何在有相同版本号的带source
字样的jar时,将它也加入你的依赖之中。
打开你项目的build.gradle
,添加
repositories {
maven {
url "https://cursemaven.com"
content {
includeGroup "curse.maven"
}
}
}
dependencies {
minecraft 'net.minecraftforge:forge:1.18.2-40.1.86'
annotationProcessor 'org.spongepowered:mixin:0.8.5:processor'
implementation fg.deobf('curse.maven:t88-663112:4404259')
annotationProcessor 'curse.maven:t88-663112:4404259'
}
刷新gradle项目,你应该就能在外部库
中找到T88了。
由于我们只在这里使用T88的注解及其处理器,而网络包注册类生成是在编译阶段就需要完成,故在生产环境进行游戏时并不需要T88。也就是说,我们在这一章不需要向mods.toml
里写入依赖要求。
编写网络包
在R6MS中,当玩家点击加入或退出排队时,我们需要发包告知服务器这个变化;当服务器由于某种原因将玩家踢出队列时,客户端也需要及时地知道这个变化。
这是我们的PlayerQueuePack
类,它负责完成上述的工作:
@NetPacket(modid = R6Constants.MOD_ID)
public class PlayerQueuePack {
String event;
public PlayerQueuePack(String event) {
this.event = event;
}
@Decoder
public PlayerQueuePack(FriendlyByteBuf buf) {
this.event = buf.readUtf();
}
@Encoder
public void writeToNet(FriendlyByteBuf buf) {
buf.writeUtf(event);
}
@Consumer
public void handler(Supplier<NetworkEvent.Context> context) {
context.get().enqueueWork(
() -> {
if (context.get().getDirection().equals(NetworkDirection.PLAY_TO_SERVER)) {
serverHandler();
} else {
clientHandler();
}
}
);
context.get().setPacketHandled(true);
}
public void serverHandler() {
switch (event){
case "joinQueue":
...
}
@OnlyIn(Dist.CLIENT)
public void clientHandler() {
...
}
}
注解@NetPacket
标记了这个类需要在编译时由T88对其生成对应的注册内容。@NetPacket
包含两个值,modid
和version
。version
通常不需要你关心,而我强烈建议你给modid
重新指定为你自己的modid,而不是用默认的"t88"
。
随后,(多数情况下)你所有需要传输的内容都应该写在成员变量中,在这里我们只需要一个字符串就足够了。接下来是一个简单的构造方法,我们在其他代码中创建包时会用到。
一个网络包还需要另外三个方法,分别用于把一串字节解码组装成包、把网络包里面的内容打包成一串字节,以及在收到网络包之后执行的动作。他们对应需要的注解分别是@Decoder
、@Encoder
和@Consumer
,不需要任何额外的参数。
很明显,FriendlyByteBuf
提供分别地提供了对应的读写方法,我们只需要简单调用即可。稍加探索你就会发现,它提供的可以编/解码的对象十分多样。
你的@Decoder
和@Encoder
中对各字段的读写顺序应该相同。
最后一个就是负责最终执行的方法了。上面展示的是一个常见的写法,将服务器收到客户端发来的包时需要做的事情写在serverHandler()
里,将客户端收到服务器发来的包时需要做的事情写在clientHandler()
里。在clientHandler()
中,由于需要做的事情往往以Minecraft minecraft = Minecraft.getInstance()
打头,所以我会习惯性地加上@OnlyIn(Dist.CLIENT)
。如果你的包只是单向发送,或许你就可以都写在一个方法里面。
所有需要进行的动作都需要写在context.get().enqueueWork()
里:负责收发网络包的显然不是主线程,你也不能在收到网络包后立即对主线程进行操作。
参数context
值得一探,其中含有许多对你有用的方法。
- 1.19.4
- 1.20.4
Forge将SimpleChannel.consumer
标记为了弃用,其被细分为了consumerMainThread
和consumerNetworkThread
,consumer
也被定向至consumerMainThread
。T88默认使用consumerMainThread
。所以你不再需要context.get().enqueueWork()
来包装你需要执行的的逻辑;也不再需要context.get().setPacketHandled(true);
。
你的handler
方法现在可以简化为这样:
@Consumer
public void handler(Supplier<NetworkEvent.Context> context) {
if (context.get().getDirection().equals(NetworkDirection.PLAY_TO_SERVER)) {
serverHandler();
} else {
clientHandler();
}
}
在新版网络包系统,不再有一个统一的@Consumer
,取而代之的是分别的@ClientHandler
和@ServerHandler
。这两个方法需要接收一个PlayPayloadContext
对象为形参,并在主线程中执行。现在一个网络包大概像这样:
@NetPacket
public class RecommendOrbCheckPacket {
...
public RecommendOrbCheckPacket(...) {
...
}
@Decoder
public RecommendOrbCheckPacket(FriendlyByteBuf buf) {
...
}
@Encoder
public void write(FriendlyByteBuf buf) {
...
}
@ClientHandler
public void clientHandler(PlayPayloadContext context) {
...
}
@ServerHandler
public void serverHandler(PlayPayloadContext context) {
...
}
}
@Encoder
方法并不需要一定名为write
。注解处理器会为你收拾妥当。
发送网络包
由于服务器和客户端是一对多的关系,从客户端向服务器发包只需要指定发送的内容即可:
PacketProxy.getChannel(PlayerQueuePack.class).sendToServer(new PlayerQueuePack("joinQueue"));
由于T88将每个网络包对应的SimpleChannel
从其网络包类中分离,所以你需要通过PacketProxy.getChannel
获取对应的SimpleChannel
后进行操作。
而从服务器向客户端发包则略复杂一些。假设我们已经通过某种途径获得了一个ServerPlayer对象,想要向他发包:
PacketProxy.getChannel(PlayerQueuePack.class).send(PacketDistributor.PLAYER.with(() -> serverPlayer), new PlayerQueuePack("kick"));
- 1.19.4
- 1.20.4
PacketProxy
已更名为PacketHelper
,并提供几个更简化的常用方法。
PacketHelper
已更名为NetworkHelper
,不再需要先获取SimpleChannel
,直接调用其中的工具方法即可。