实体数据与网络同步

实体数据与网络同步

没有数据的实体几乎没有用途,因此为实体保存数据至关重要。所有实体都会保存一些默认数据,例如类型和位置。本文介绍如何添加自己的数据,以及如何同步这些数据。

最简单的方式是在 Entity 类中添加字段,然后按需读写。但一旦需要同步,就会变得麻烦:大多数实体逻辑仅在服务端运行,只有每隔一段时间才会向客户端发送更新,间隔由 EntityType 的 clientUpdateInterval 决定。这也是服务端每秒游戏刻数过低时,实体容易出现明显卡顿的原因。

为此,原版提供了几个各有用途的系统。必要时,也始终可以发送自定义数据。

SynchedEntityData

SynchedEntityData 用于在运行时保存值,并通过网络同步。它分为三个类:

  • EntityDataSerializer 基本上是对 StreamCodec 的封装。
    • Minecraft 使用硬编码的序列化器映射;NeoForge 将其转换成了注册表,因此新增 EntityDataSerializer 时必须进行注册。
    • Minecraft 在 EntityDataSerializers 中定义了多种默认序列化器。
  • EntityDataAccessor 由实体持有,用于获取和设置数据值。
  • SynchedEntityData 本身保存实体的所有 EntityDataAccessor,并按需自动调用 EntityDataSerializer 同步数值。

首先,在实体类中创建 EntityDataAccessor:

public class MyEntity extends Entity {
    // The generic type must match the one of the second parameter below.
    public static final EntityDataAccessor<Integer> MY_DATA =
        SynchedEntityData.defineId(
            // The class of the entity.
            MyEntity.class,
            // The entity data accessor type.
            EntityDataSerializers.INT
        );
}

重要警告 编译器虽然允许在 SynchedEntityData#defineId() 的第一个参数中传入非所属实体的类,但这样做会导致难以调试的问题,必须避免。通过 mixin 或类似方式向其他实体添加字段也属于这种情况。

随后,在 defineSynchedData 中定义默认值:

public class MyEntity extends Entity {
    public static final EntityDataAccessor<Integer> MY_DATA = SynchedEntityData.defineId(MyEntity.class, EntityDataSerializers.INT);

    @Override
    protected void defineSynchedData(SynchedEntityData.Builder builder) {
        // Our default value is zero.
        builder.define(MY_DATA, 0);
    }
}

最后,可以按以下方式获取和设置实体数据,假设当前代码位于 MyEntity 的方法中:

int data = this.getEntityData().get(MY_DATA);
this.getEntityData().set(MY_DATA, 1);

readAdditionalSaveData 与 addAdditionalSaveData

这两个方法用于从磁盘读取和向磁盘写入数据,具体通过值输入输出加载和保存数值:

// Assume that an `int data` exists in the class.
@Override
protected void readAdditionalSaveData(ValueInput input) {
    this.data = input.getIntOr("my_data", 0);
}

@Override
protected void addAdditionalSaveData(ValueOutput output) {
    output.putInt("my_data", this.data);
}

自定义生成数据

有些数据只需在实体生成时提供给客户端,之后不会变化。这种情况下,可以让实体实现 IEntityWithComplexSpawn,通过 #writeSpawnData 和 #readSpawnData 方法向网络缓冲区写入或从中读取数据:

@Override
public void writeSpawnData(RegistryFriendlyByteBuf buf) {
    buf.writeInt(1234);
}

@Override
public void readSpawnData(RegistryFriendlyByteBuf buf) {
    int i = buf.readInt();
}

此外,也可以在生成实体时发送自己的数据包。重写 IEntityExtension#sendPairingData,在其中像平常一样发送数据包即可:

@Override
public void sendPairingData(ServerPlayer player, Consumer<CustomPacketPayload> packetConsumer) {
    // Call super for some base functionality.
    super.sendPairingData(player, packetConsumer);
    // Add your own packets.
    packetConsumer.accept(new MyPacket(...));
}

自定义网络数据包的更多信息请参阅网络通信文档。

数据附件

实体通过补丁继承了 AttachmentHolder,因此支持使用数据附件保存数据。它主要用于为并非由自己定义的实体添加自定义数据,例如 Minecraft 或其他 Mod 添加的实体。更多信息请参阅链接文档。

自定义网络消息

同步时,也可以按需使用自定义数据包发送附加信息。更多信息请参阅网络通信文档。


来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。