本来打算直接介绍Block Entities,但看了下源代码,还是先需要介绍数据存储 :)

NBT

我的世界中存在一个NBT系统,通过标签(Tag)来存储和读取数据,一个标签由标签名和数值两部分组成,格式为name:data,中间不能有空格,中文或是特殊符号,name对大小写敏感。一串标签(name:data)拼接在一起并用{}包括称之为NBT,比如{damage:3,speed:2},有点像json格式。

数据类型

这一部分主要列出了Tag类型,先介绍简单的Tag,以及复合Tag

  • 整型Tag:

    • ByteTag:字节Tag,用于存储一字节(8bit)

    • ShortTag:Short类型Tag,用于存储两字节(16bit)

    • IntTag:Int类型Tag,用于存储4字节(32bit)

    • LongTag:Long类型Tag,用于存储8字节(64bit)

  • 浮点数Tag:

    • FloatTag:Float类型Tag,用于存储浮点数,4字节(32bit)
    • DoubleTag:Double类型Tag,用于存储浮点数,8字节(64bit)
  • StringTag:用于保存字符串

  • 集合类Tag:

    • ListTag:用于存储一系列Tag(相同类型),如果为CompoundTag,则每个复合类型的子标签名要一致
    • ByteArrayTag:用于存储多个Byte Tag
    • IntArrayTag:用于存储多个Int Tag
    • LongArrayTag:用于存储多个Long Tag
  • CompoundTag:包含一段独立的NBT标签,存储多个不同Tag

Tag的格式等详见这里使用的比较多的应该是CompoundTag

Capability

首先来看源码

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
public class Capability<T>
{
public String getName() { return name; }

public @Nonnull <R> LazyOptional<R> orEmpty(Capability<R> toCheck, LazyOptional<T> inst)
{
return this == toCheck ? inst.cast() : LazyOptional.empty();
}

public boolean isRegistered()
{
return this.listeners == null;
}

public synchronized Capability<T> addListener(Consumer<Capability<T>> listener)
{
if (this.isRegistered())
listener.accept(this);
else
this.listeners.add(listener);
return this;
}

private final String name;
List<Consumer<Capability<T>>> listeners = new ArrayList<>();

Capability(String name)
{
this.name = name;
}

void onRegister()
{
var listeners = this.listeners;
this.listeners = null;
listeners.forEach(l -> l.accept(this));
}
}

可以看到里面有一个叫做Consumer,这个是用来传输函数的,调用accept即可调用传进的函数,有一个参数,不需要返回值,当调用了onRegister后Capability将会调用这个函数

在这里我将不会先介绍如何使用他定义好的Capability,而是先介绍如何创建自己的Capability,并将他注册到游戏内不知道如何创建自己的而去直接用他的可能会比较困难,在这里我们会将这个Capability赋予玩家,并利用之前的写好的红石锭激活这个Capability(注意是利用Capability里面的自定义布尔激活),并且赋予玩家一个加速效果。新建一个接口,名为IPlayer放在examplemod/player下,在相同路径player下创建一个类叫做SuperPlayer,并implement IPlayer

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
public interface IPlayer {

void activate();

Boolean isActivated();

void loadData(CompoundTag nbt);

CompoundTag saveData();

}

public class SuperPlayer implements IPlayer{

public static final String IS_ACTIVATE_STRING = "isActivate";
public static final Integer UPDATE_TICK = 80;
public static final ResourceLocation SUPER_PLAYER_KEY = new ResourceLocation(Reference.MOD_ID,"super_player");
private int tickCount = 0;
private boolean isActivated = false;

public static Capability<IPlayer> CAPABILITY = CapabilityManager.get(new CapabilityToken<>(){});

public SuperPlayer(){
MinecraftForge.EVENT_BUS.register(this);
}

public static ICapabilityProvider createNewCapability() {
return new ICapabilitySerializable<CompoundTag>() {

@NotNull
@Override
public <T> LazyOptional<T> getCapability(@NotNull Capability<T> cap, @org.jetbrains.annotations.Nullable Direction side) {
return CAPABILITY.orEmpty(cap, opt);
}

final IPlayer inst = new SuperPlayer();
final LazyOptional<IPlayer> opt = LazyOptional.of(() -> inst);

@Override
public void deserializeNBT(CompoundTag nbt) {
((SuperPlayer)inst).loadData(nbt);
}

@Override
public CompoundTag serializeNBT() {
return ((SuperPlayer)inst).saveData();
}
};
}

@Override
public void activate(){
this.isActivated = true;
}

@Override
public Boolean isActivated(){
return isActivated;
}

@Override
public void loadData(CompoundTag nbt) {
this.isActivated = nbt.getBoolean(IS_ACTIVATE_STRING);
}

@SubscribeEvent
public void onTickUpdate(TickEvent.PlayerTickEvent event) {
if(event.side== LogicalSide.SERVER&&tickCount++>UPDATE_TICK) {
tickCount = 0;
Player player = event.player;
LazyOptional<IPlayer> capability;
if (player == null) {
return;
}
capability = player.getCapability(CAPABILITY, null);
capability.ifPresent((cap) -> {
if (cap.isActivated()) {
//necessary
player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SPEED,
200, 3));
}
});
}
}

@Override
public CompoundTag saveData() {
CompoundTag tag = new CompoundTag();
tag.putBoolean(IS_ACTIVATE_STRING,isActivated);
return tag;
}

}

添加AttachHandler到handler下,修改RedstoneIngot,添加new AttachHandler();到ExampleMod下的init()函数中

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
public class AttachHandler {

public AttachHandler(){
MinecraftForge.EVENT_BUS.register(this);
}

@SubscribeEvent
public void onAttachCapability(AttachCapabilitiesEvent<Entity> event) {
if (event.getObject() instanceof Player) {
try {
event.addCapability(SuperPlayer.SUPER_PLAYER_KEY, SuperPlayer.createNewCapability());
} catch (Exception e) {
e.printStackTrace();
}
}
}

}

public class RedstoneIngot extends ItemBase{

public RedstoneIngot() {
super("redstone_ingot", new Item.Properties().tab(ExampleMod.creativeTab));
}

@Override
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
LazyOptional<IPlayer> capability = player.getCapability(SuperPlayer.CAPABILITY);
capability.ifPresent((cap) -> {
if (!level.isClientSide&&!cap.isActivated()) {
//necessary
cap.activate();
player.getMainHandItem().setCount(player.getMainHandItem().getCount()-1);
}
});
return InteractionResultHolder.consume(player.getMainHandItem());
}
}

这样一来,进入游戏后右击红石锭,就会发现获得了一个速度效果,kill后会发现效果消失了,需要重新建立,这就是因为我们没有订阅 PlayerEvent.Clone 事件来达成kill后依旧有原来的效果,我们先分析上面修改的几个类

  • IPlayer:用于定义一个玩家的接口,用于激活和获得激活状态
  • RedstoneIngot:用于右击后激活超级玩家
  • AttachHandler:添加Capability到玩家内,就像注册物品,方块一样,Capability也需要注册到各种实体内,就比如玩家

SuperPlayer

具体实现的(超级)玩家,其中的几个变量具体为:

  • IS_ACTIVATE_STRING:是否激活的名字,因为保存的Tag需要一个名字
  • UPDATE_TICK:更新时间,80一般代表80/20=4s
  • SUPER_PLAYER_KEY:用于添加Capability时对应的key,如果不知道key和value是什么,请参考java的Map
  • tickCount:用于计tick数
  • isActivated:用于判断玩家是否激活
  • CAPABILITY:用于生成一个Capability,生成Capability只需要使用CapabilityManager.get(new CapabilityToken<>(){});即可,注意Capability<?>中的?为你的接口/类,CapabilityManager.get方法貌似是获取了你的接口或类的相对路径,然后根据这个路径来判断是否是重复

里面的函数具体解释:(不会解释比较短的函数)

  • createNewCapability:用于创建一个新的Capability,里面创建的具体函数为
    1. 判断是否是自己
    2. 从NBT中读取数据,转化为自己的属性(即加载数据)
    3. 从自身的数据转化为NBT(即保存数据)
  • onTickUpdate:用于每个玩家Tick的更新,如果激活则会给予玩家速度效果

使用forge功能以及添加

Forge提供了三个接口,用于规范,具体使用方法即为新建Capability时,讲IPlayer替换为下列接口名

  • IItemHandler:用于处理库存的接口,可以应用于BlockEntities、Entities或ItemStacks
  • IFluidHandler:用于处理流体库存的接口,可以应用于BlockEntities、Entities或ItemStacks
  • IEnergyStorage:用于处理能量容器的接口,可以应用于BlockEntities、Entities或ItemStacks

名词举例:

  • BlockEntities(箱子、机器)
  • Entities(玩家、生物)
  • ItemStacks(便携式背包)

Capability可以添加到EntityBlockEntityItemStackLevelLevelChunk中,具体为订阅以下事件:

  • AttachCapabilitiesEvent<Entity>:对实体触发。
  • AttachCapabilitiesEvent<BlockEntity>:对方块实体触发。
  • AttachCapabilitiesEvent<ItemStack>:对物品堆叠触发。
  • AttachCapabilitiesEvent<Level>:针对关卡触发。
  • AttachCapabilitiesEvent<LevelChunk>:对关卡块触发。

持久化与同步

对于持久化(保存到磁盘中)Capability,请参见这里

对于数据的同步,主要是需要Server同步给Client,在forge文档内写出了一些场景需要同步(网络会单独出一章)

  1. 当实体在世界中生成或放置块时
  2. 当存储的数据发生变化时
  3. 当新客户端开始查看实体或块时

默认状态下,玩家死亡时不会同步Capability,如果要同步需要订阅PlayerEvent.Clone事件,参考,这里我给出了一个例子,将下面代码添加到AttachHandler中运行游戏即可发现死亡后依旧有能力了

1
2
3
4
5
6
7
8
9
10
@SubscribeEvent
public void onPlayerClone(PlayerEvent.Clone event) {
if (!event.getPlayer().getCommandSenderWorld().isClientSide&&event.isWasDeath()) {
SuperPlayer player = (SuperPlayer) event.getPlayer().getCapability(SuperPlayer.CAPABILITY).orElseThrow(() -> new IllegalStateException("Cannot get Super player capability from player " + event.getPlayer()));
event.getOriginal().reviveCaps();
SuperPlayer oldPlayer = (SuperPlayer) event.getOriginal().getCapability(SuperPlayer.CAPABILITY).orElseThrow(() -> new IllegalStateException("Cannot get Super player capability from player " + event.getOriginal()));
event.getOriginal().invalidateCaps();
player.loadData(oldPlayer.saveData());
}
}

在这一版本中,重生还需要有能力需要调用player中的reviveCaps,得到原来的后在调用invalidateCaps,之前弄的时候貌似不用导致现在出错..折腾了我蛮久 :(

保存世界数据

如果你需要保存数据到世界中,则需要 implement SavedData,在这里需要创建一个类名为WorldSaveData,并保存在examplemod/world下,需要修改RedstoneIngot用于修改物品时设置数据为脏数据,这样之后就会自动保存。首先是WorldSaveData

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
45
46
47
48
49
50
51
52
53
54
55
56
57
public class WorldSaveData extends SavedData {
private Map<UUID, IPlayer> map = new HashMap<>();
public static final String DATA_KEY = "example_world_data";
private final static String ID = "example-data";

@Nonnull
private WorldSaveData getData(final MinecraftServer server) {

return server.getLevel(Level.OVERWORLD).getDataStorage().computeIfAbsent(this::load, this::create, ID);
}

public WorldSaveData create(){
return this;
}

@Nonnull
public static Optional<WorldSaveData> getData(LevelAccessor world) {
if (world instanceof ServerLevel) {
WorldSaveData worldSaveData = new WorldSaveData();
return Optional.of(worldSaveData.getData(((ServerLevel) world).getServer()));
}
return Optional.empty();
}

public void modifyPlayer(Player player){
LazyOptional<IPlayer> capability = player.getCapability(SuperPlayer.CAPABILITY);
capability.ifPresent((cap) -> {
map.put(player.getUUID(),cap);
});
}

public WorldSaveData load(CompoundTag nbt) {
WorldSaveData worldSaveData = new WorldSaveData().create();
Tag tag1 = nbt.get(DATA_KEY);
if(tag1 instanceof ListTag){
for (Tag tag : (ListTag) tag1) {
CompoundTag compoundTag = (CompoundTag) tag;
UUID name = compoundTag.getUUID("uuid");
SuperPlayer superPlayer = new SuperPlayer();
superPlayer.loadData(compoundTag);
worldSaveData.map.put(name, superPlayer);
}
}
return worldSaveData;
}
@Override
public CompoundTag save(CompoundTag p_77763_) {
ListTag listTag = new ListTag();
for(Map.Entry<UUID,IPlayer> entry : map.entrySet()){
CompoundTag compoundTag = entry.getValue().saveData();
compoundTag.putUUID("uuid",entry.getKey());
listTag.add(compoundTag);
}
p_77763_.put(DATA_KEY,listTag);
return p_77763_;
}
}

以及需要将RedstoneIngot中的use进行修改,激活后需要保存玩家数据(默认貌似不会保存,也应不需要保存)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
LazyOptional<IPlayer> capability = player.getCapability(SuperPlayer.CAPABILITY);
capability.ifPresent((cap) -> {
if (!level.isClientSide&&!cap.isActivated()) {
//necessary
cap.activate();
player.getMainHandItem().setCount(player.getMainHandItem().getCount()-1);
Optional<WorldSaveData> data = WorldSaveData.getData(level);
data.ifPresent((worldSaveData)->{
worldSaveData.setDirty();
worldSaveData.modifyPlayer(player);
});
}
});
return InteractionResultHolder.consume(player.getMainHandItem());
}

如此就能达到保存游戏数据的目的,现在进行分析,主要是针对WorldSaveData,RedstoneIngot中只调整了玩家以及设置了数据块为脏,从上到下为代码中变量和方法出现的顺序,方法带有()

  • map:用于存储玩家信息,一个玩家有一个唯一的UUID,利用他来存储,value为玩家信息
  • DATA_KEY:用于声明自己存储的ListTag的名字
  • ID:用于声明自己存储的文件名
  • getData():用于获得一个存储实例,这边暂时没有分析,只有forge文档的说明
  • create():用于传递给getData()的方法,新建了一个自己
  • static getData():用于给全局获取一个存储实例,这样自己可以存储一些信息
  • modifyPlayer():修改玩家信息,用于全局获取存储实例后调用
  • load():从CompoundTag中获取自己存储的信息
  • save():往CompoundTag中写入自己要存储的数据