教程:编写脚本API/动态属性

来自Minecraft基岩版开发Wiki
动态属性
系列教程
所属系列
难度
初级
实践设备
WindowsAndroid
所需软件

引言[编辑]

动态属性(DynamicProperties) 是脚本系统存储自定义数据的一种格式。目前动态属性可以存储在实体、物品和世界上。相较于数据驱动中属性(或状态)的“静态性”,动态属性可以实时增删,且动态属性的类型不固定,脚本系统以此可以实现数据的动态存储。

动态属性以NBT的形式存储,其数据内容位于DynamicProperties复合标签下。每个行为包的动态属性存储在以该行为包UUID命名的复合标签内。

动态属性NBT结构:
  •  DynamicProperties
    •  <UUID>
      •  <dynamic_property_id1>
      •  <dynamic_property_id2>
        •  X
        •  Y
        •  Z
      •  ……

获取和设置[编辑]

旧版动属相对复杂,在新版,提供了以下5个方法来对动态属性进行操作:

  • clearDynamicProperties:清除该对象上的所有动态属性
  • getDynamicProperty:获取该对象上指定动态属性的值
  • getDynamicPropertyIds:获取该对象上所有动态属性的ID
  • getDynamicPropertyTotalByteCount:获取该对象上存储的所有动态属性的总字节数
  • setDynamicProperty:在该对象上设置一个动态属性

这些方法可从World、​EntityItemStack三个类中调用。

  注意:ItemStack类上此ItemStack必须不可堆叠,这要求在本质上不能堆叠,同时利用nbt修改的堆叠也无效。

动态属性可以存储布尔型、数值型、字符串型和三维向量型的数据,单个动态属性的数据大小限制在32KB以内。大量的动态属性数据可能会导致某些设备上加载缓慢。

设置[编辑]

对任意可使用动属的对象,使用setDynamicProperty可设置其动属。

import {
    ItemStack
} from "@minecraft/server";

// setDynamicProperty(identifier: string, value?: boolean | number | string | Vector3): void
const exampleItem = new ItemStack("minecraft:diamond_axe");

exampleItem.setDynamicProperty("example", "Test");
exampleItem.setDynamicProperty("示例", true);
// identifier 在此不是命名空间,可为任意字符串,但推荐使用命名空间

setDynamicProperty的数据可为一个字符串,大小限制为32726个字符。

JSON是一种好的方法,JSON.stringify()可以把JSON转为字符串,JSON.parse()又可以把字符串转回JSON。所以可以像下面这么做:

import {
    world
} from "@minecraft/server";

function setData(id, json, Class = world) {
    Class.setDynamicProperty(id, JSON.stringify(json));
}

除此外我们也可以清除动属。

import {
    ItemStack
} from "@minecraft/server";

// clearDynamicProperties(): void
const exampleItem = new ItemStack("minecraft:diamond_axe");
exampleItem.setDynamicProperty("example", "Test");
exampleItem.clearDynamicProperties();

clearDynamicProperties()可以清除对象上的所有动属。

获取[编辑]

对任意使用了动属的对象,使用以下方法可获取其动属。

getDynamicProperty()可获取目标动属,示例如下:

import {
    ItemStack
} from "@minecraft/server";

// getDynamicProperty(identifier: string): boolean | number | string | Vector3 | undefined
// ...
// 省略定义和设置动属的过程。
let result = exampleItem.getDynamicProperty("example");
console.warn(result);

getDynamicPropertyIds()可获取对象上使用的可用动态属性标识符集,示例如下:

import {
    ItemStack
} from "@minecraft/server";

// getDynamicPropertyIds(): string[]
// ...
// 省略定义和设置动属的过程。

let result = exampleItem.getDynamicPropertyIds();
console.warn(result);

getDynamicPropertyTotalByteCount()可获得对象存储的所有动态属性的总大小(以字节为单位)。这包括键和值的大小。这对于诊断性能警告标志非常有用 - 例如,如果一个实体具有许多兆字节的关联动态属性,则在各种设备上加载它可能会很慢,示例如下:

import {
    ItemStack
} from "@minecraft/server";

// getDynamicPropertyTotalByteCount(): number
// ...
// 省略定义和设置动属的过程。
let result = exampleItem.getDynamicPropertyTotalByteCount();
console.warn(result);

再结合上面的setData,我们可以使用它:

import {
    world
} from "@minecraft/server";

function getData(id, Class = world) {
    return JSON.parse(Class.getDynamicProperty(id));
}

动态属性管理[编辑]

对于一些大型的项目,我们可能要进行比较复杂的动态属性管理。下面有些思路供您参考:

分段[编辑]

32726个字符对于小项目已经足够使用了,但对于大型项目来说远远不够,可以尝试将长的字符串分割成多部分以供储存:

// 限制最大分割长度
const BLOCK_LENGTH = 1000;

function readDataOnDynamicProperty(target) {
    // 读取分割块数
    const quantity = target.getDynamicProperty("data_space_quantity");
    // 没有正常读取成功直接返回
    if (typeof quantity !== "number") return {};
    if (quantity === 0) return {};
    // 准备储存量
    let value = "";
    // 拼接
    for (let i = 1; i <= quantity; i++) {
        const currentValue = target.getDynamicProperty(`data_space_${i}`);
        if (typeof currentValue !== "string" || currentValue === "") return {};
        value += currentValue;
    }
    try {
        // 转回对象
        return JSON.parse(value);
    } catch {
        return {};
    }
}

function writeDataOnDynamicProperty(target, source_data) {
    // 转为字符串
    const data = JSON.stringify(source_data);
    let dataBlocks = [];
    // 分割
    for (let i = 0; i < data.length; i += BLOCK_LENGTH) {
        dataBlocks.push(data.slice(i, i + BLOCK_LENGTH));
    }
    // 设置数量
    const quantity = dataBlocks.length;
    target.setDynamicProperty("data_space_quantity", quantity);
    // 保存
    for (let i = 1; i <= quantity; i++) {
        target.setDynamicProperty(`data_space_${i}`, dataBlocks[i - 1]);
    }
}

这里我们默认将所有的数据存储在同一个对象中,这样可以更大程度的利用空间。

压缩[编辑]

通过各种形式,我们可以压缩字符串来获得更多的储存空间,js有一个bota()方法可以将字符串base64化,但我们还有另外的更好方法:

LZString是一个以MIT协议开源(该协议限制很小,在我们的项目中可以很好地使用)的字符串压缩项目(JS),它本来旨在满足在localStorage中存储大量数据的需求,但其原理就是使用压缩的方式。其压缩度很高,可以被我们使用:

npm i lz-string

如果您有NodeJs的开发环境的话,请使用上面的代码来安装它。否则,请在这个地方来复制它的单文件版本。

存储与读取[编辑]

上面的方法足以满足大部分需求,但压缩和保存都是消耗大量时间的操作,我们需要一个良好的方式来完成存储和读取:

下面只介绍思路,文件保留在实例中:

  1. 使用对象(DataObject)来控制系统。
  2. 使用方法来读取对象的引用,这样可以通过直接的赋值操作,而无需额外设置。
  3. 监听worldInitializeworldLoad以保证在世界加载完成的时候去读取动态属性。
  4. 同时注册一个循环调用器(runInterval),来保证自动储存,防止数据丢失。
  5. 监听shutdown来保证在退出游戏之前保存数据。
  6. 监听playerSpawn用来在玩家初次生成时读取动态属性,推荐在世界加载的时候也读取所有玩家的动态属性(防止/reload命令引起错误)

引用[编辑]

  1. 官方文档: ItemStack Class