本教程由 WvT Studio @阿特我自己 制作,未经授权禁止以任何形式进行引用或转载,版权所有,侵权必究!

当前修订版本:1.3.0.beta1

前言

Minecraft Script Engine 是 Mojang 为 Minecraft 基岩版 推出的跨平台 MOD API 接口,用于自定义游戏行为

Script 并非通过修改源代码实现扩展,与 Java 版的 Forge 相比会有一定的局限性

请在阅读本文时打开 API 文档对照

如果你感兴趣,可以加入交流群:132538683

注意:脚本引擎 目前还处于测试阶段,每次版本更新都可能会有破坏性改动,如果你的脚本在后续版本工作不正常,请等待作者更新本文档,或查看英文版官方wiki

一、准备工作

Script 使用 JavaScript 语言编写,本文不会介绍 JavaScript,如果你还不了解,请点击此处学习(至少需要学完 JavaScript 基本结构)

1. 开发环境

如果你看到这里,则说明你已经掌握了 JavaScript,现在让我们准备一下开发环境

JavaScript 编辑器:任何一个可以编辑 JavaScript 的 IDE 或是纯文本编辑器都可用于 MOD 开发。Mojang 推荐使用 Visual Studio 进行开发。

Minecraft Bedrock Edition:本教程适用于 1.12 版本

  • Windows10: 在 Microsoft Store 购买正版,并通过 Xbox 会员中心加入测试组。
  • Android: 在 Google Play 购买正版并加入测试组,下载 BlockLauncher,通过 BlockLauncher 启动游戏
  • iOS: 暂不支持
  • Wii U: 暂不支持
  • Xbox One: 暂不支持
  • PlayStation 4: 暂不支持
  • Nintendo Switch: 暂不支持

2. MOD文件结构

Script 属于行为包,是行为包的组件之一,这意味着你可以和 add-ons 结合使用

以下为一个只包含 Script 的行为包文件结构

┌ behavior_packs —— 行为包的根目录,包含多个行为包
├─┬ [my_behavior_pack] —— 单个行为包的根目录,目录名随意但不要包含特殊字符,纯英文最佳
├─┴─┬ scripts —— 存放脚本文件的目录
├───┴─┬ client —— 该文件夹中的所有脚本将在游戏的客户端运行
├─────┼── myClientScript.js —— 单个脚本,文件名随意但不要包含特殊字符,纯英文最佳
├─────┼ server —— 该文件夹中的所有脚本将在游戏的服务端运行
├───┬─┴── myServerScript.js
├───┼ manifest.json —— 单个行为包的清单文件,包含名称描述标识版本号等数据
├───┴ pack_icon.png —— 单个行为包的图标
├─┬ [other_behavior_pack] —— 其他单个行为包的根目录
├─┴── ......
└──......

以下为 manifest.json 文件的简单示例

{
    "format_version": 1,
    "header": {
        "description": "My Script Test", // 描述
        "name": "My Script Test", //名称
        "uuid": "ee649bcf-256c-4113-9068-6a802b89d756", // UUID唯一标识符,不能与其他标识符相同
        "version": [0, 0, 1] //版本号
    },
    "modules": [{
        "description": "My Script Test", //描述
        "type": "client_data", //类型
        "uuid": "3168e4b8-facf-12e8-8eb2-f2801f1b9fd1", // UUID唯一标识符,不能与其他标识符相同
        "version": [0, 0, 1] //版本号
    }]
}

你可以在此处在线生成UUID

你可以点击此处下载我们已经创建好的模板,请注意替换清单文件中的UUID

3. 服务端脚本与客户端脚本

脚本引擎将脚本分为服务端客户端,这种结构可以解决联机同步问题并减轻服务器的部分压力

server 文件夹内的脚本会在服务端执行,它们通常用于生成实体、生成方块、改变实体或方块的状态等等,这些操作都需要所有玩家同步

client 文件夹内的脚本会被发送给玩家,并在玩家各自的终端上运行,它们通常用于监听该玩家的行为或是操作自定义UI

无论文件夹内的脚本文件有多少,它们都将独立运行,互不干扰

4. Hello World!

按照国际惯例,我们来编写一个 Hello World 程序了解一下脚本的基本结构

在你的客户端脚本(client.js)里写入如下代码,以下代码将在玩家进入存档后打印“Hello World’”聊天栏。

let sys = client.registerSystem(0, 0);

// 初始化时调用
sys.initialize = function () {
    // 注册事件监听器,当玩家加入世界时调用
    sys.listenForEvent("minecraft:client_entered_world", callback);
};

function callback(result) {
    // 创建事件数据模板
    let eventData = sys.createEventData("minecraft:display_chat_event");
    // 更改事件数据内容
    eventData.data.message = "Hello World!";
    // 广播在聊天栏显示消息的事件
    sys.broadcastEvent("minecraft:display_chat_event", eventData);
};

如果你的脚本出现了错误,可以对照参考我们已经制作好的 HelloWorld 程序示例,点击此处下载。

5. 打包和导入MOD

将行为包根目录内的所有文件压缩为 zip 格式(不是压缩该行为包文件夹)
将压缩包的后缀改为 .mcpack
Win10 端双击该文件会自动打开 Minecraft 并导入,Android 端需要在文件管理器手动指定打开方式为 Minecraft
在创建存档时勾选 使用实验玩法,并在 行为包 选项卡添加刚才导入的行为包

点击这里,查看如何便捷地打包导入 MOD

二、脚本引擎中的系统(System)

在脚本的最开头,我们需要为脚本注册一个系统(System),正如 JavaScript 的 document 对象用于操作 HTML 页面一样,System 对象定义了一些方法和属性用于控制游戏行为

1. 获取系统对象

脚本引擎内置了两个对象:client 和 server,可以通过简单名称访问
通过这两个对象的 registerSystem(majorVersion, minorVersion) 方法,你可以获取到其对应的 System 对象
以下为该方法的简单描述,完整描述请查看API文档:

majorVersion —— Integer类型,你的脚本所使用的Minecraft脚本引擎的主要版本

minorVersion —— Integer类型,你的脚本所使用的Minecraft脚本引擎的修订版本

如果没有特殊需求,传入 0 即可

let system = client.registerSystem(0, 0);
let system = server.registerSystem(0, 0);

2. 系统初始化函数

在获取到 System对象 后,我们需要进行一些初始化工作,比如注册事件监听器或自定义组件

当世界已经准备好,但玩家还未进入世界时(无论客户端还是服务端),引擎会执行 System 对象的 initialize() 函数,我们可以覆盖这个函数
注意:不要在此方法内生成任何实体或对世界进行其他的改动,因为此时玩家还没有进入世界

let system = client.registerSystem(0, 0);

system.initialize = function() {
    // 在此处进行初始化,注册自定义组件和事件,或是注册事件监听器
};

3. 系统更新函数

脚本引擎 每 tick(50ms)都会执行一次 System 对象的 update() 函数,这类似于 ModPE 中的 modTick() 钩子函数

该函数是被异步调用的,不会阻塞线程,但请尽量将耗时控制在 50ms 内,否则可能会导致一些意外情况

let system = client.registerSystem(0, 0);

system.update = function() {
    //在此处进行更新
};

4. 系统关闭函数

当脚本引擎关闭时会调用 System 对象的 shutdown() 方法,开发者可以在该方法内做一些结尾工作

对于客户端来说,就是该玩家退出世界的时候
对于服务端来说,就是所有玩家都退出世界的时候

let system = client.registerSystem(0, 0);
//let system = server.registerSystem(0, 0);

system.shutdown = function() {
    //在此处进行结束工作
};

三、事件(Event)

脚本引擎拥有事件机制,实体、世界、物品的一举一动都是事件,例如玩家加入世界、天气改变、实体受到攻击等等

开发者可以对这些由游戏发出的事件进行监听并作出反映
开发者也可以手动广播事件,当一个事件被广播后,脚本引擎将会按需捕获这些事件,并在合适的时候作出反应

1. 内置事件的分类

在客户端能够使用的事件称为 客户端事件,在服务端能够使用的事件称为 服务端事件
这些事件还分为 可监听事件 和 可触发事件

每一个事件都有一个使用 minecraft 命名空间的标识符,方块、实体、药水效果的标识符也是这种格式

例如,“玩家进入世界”的事件即为 客户端事件 中的 可监听事件,其标识符为 minecraft:client_entered_world
而 “在聊天栏显示消息” 的事件即为 客户端事件 中的 可触发事件,其标识符为 minecraft:display_chat_event

所有事件的类型、标识符和描述都在 API文档 里查到,希望你在阅读本文的同时打开API文档

2. 监听事件

System 对象有如下函数可用于注册事件监听器

listenForEvent(identifier,callback)

  • identifier: 事件的标识符。通常情况下,事件标识符都传入一个可监听事件或自定义事件
  • callback: 回调函数。需要传入一个回调函数。当监听到该事件时,脚本引擎会调用该回调函数并传入该事件的数据,因此回调函数需要定义一个形参用于接收数据

通常情况下,事件监听器的注册都是在 系统初始化(initialize()方法) 时完成的

注册一个事件监听器的示例代码如下

...
// 定义一个回调函数,接收一个 eventData
function callback(eventData) {
    // Something
}
...
// 注册事件监听器,将 callback() 回调函数当作参数传入
xxx.listenForEvent("事件标识符", callback);
...

当然,你也可以使用匿名函数

接下来需要读取事件数据,事件数据总是一个 EventDataObject 类型的对象

EventDataObject 是 Mojang 规定的一个内置对象类型
所有此类型的对象都有以下属性

  • __type__: 只读。代表该对象的类型,值为”event_data”
  • __identifier__: 只读。代表该事件数据属于哪个事件,如果代表一个发送聊天消息的事件,则该属性的值为”minecraft:display_chat_event”
  • data: 只读。事件数据内容

data 是该对象的关键属性,该属性是一个对象,它的内容由具体的事件决定

例如:minecraft:block_destruction_started 事件数据对象包含 playerblock_position 两个属性

以下是监听事件的完整代码示例,minecraft:block_destruction_started 是服务端事件,请在服务端脚本中编辑代码

// 获取系统对象
let sys = server.registerSystem(0, 0);
// 重写系统初始化方法
sys.initialize = function () {
    // 注册"玩家开始破坏方块"的事件监听器,将 callback() 回调函数当作参数传入
    sys.listenForEvent("minecraft:block_destruction_started", callback);
};
// 定义一个回调函数,并定义一个 eventData 形参用于接收数据
function callback(eventData) {
    // 获取 player 数据
    let player = eventData.data.player;
    // 获取 block_position 数据
    let blockPosition = eventData.data.block_position;
    // 坐标
    let x = blockPosition.x;
    let y = blockPosition.y;
    let z = blockPosition.z;
}

3. 广播事件

同样,System 对象也提供了用于广播事件的方法。如果广播的是内置事件,游戏会按需捕获并作出反应;如果广播的是自定义事件,需要开发者手动捕获。

在此之前,我们需要了解一下事件数据模板。广播事件需要传入一个 EventDataObject 对象,该对象包含了事件数据所需的所有属性及其默认值,该对象的结构与上述的 EventDataObject 对象相同。我们需要先获取指定事件的数据模板并对其进行修改,接下来再广播指定事件并传入该对象即可

System 对象提供了 createEventData(identifier) 方法,用于创建一个指定事件的事件数据,identifier 为事件标识符。该方法返回一个 EventDataObject 类型的事件数据,包含了该事件所需的所有数据及其默认值

接着修改事件数据的值

例如 minecraft:display_chat_event 事件的数据有一个 message 属性,修改 message 属性的值即可

使用 broadcastEvent(identifier, data) 广播一个事件,identifier 为事件的标识符,data 是 EventDataObject 类型的事件数据
以下是广播事件的完整代码示例,当监听到事件时,向聊天栏输出事件数据

// 获取系统对象
let sys = server.registerSystem(0, 0);
// 重写系统初始化方法
sys.initialize = function () {
    // 注册"实体使用物品"的事件监听器,将 callback() 回调函数当作参数传入
    sys.listenForEvent("minecraft:entity_use_item", callback);
};
// 定义一个回调函数,并定义一个 eventData 参数用于接收数据
function callback(eventData) {
    chat(JSON.stringify(eventData));
}
function chat(content) {
    // 获取 minecraft:display_chat_event 事件的数据模板
    let eventData = sys.createEventData("minecraft:display_chat_event");
    // 更改内容
    eventData.data.message = content;
    // 广播事件 传入事件数据
    sys.broadcastEvent("minecraft:display_chat_event", eventData);
}

4. 自定义事件

除了内置事件,我们还可以广播和监听自定义事件,在此之前需要注册自定义事件。通常情况下,自定义事件的注册都是在系统初始化方法内完成的

注册自定义事件使用 System 对象的 registerEventData(identifier, data) 函数

它要求我们提供一个自定义事件的标识符,使用命名空间格式(即 [前缀] + “:” + [后缀]),且命名空间(前缀)不能为 “minecraft”,”myscript:eventName” 是一个有效的标识符,而 “minecraft:eventName” 是无效的

data 要求我们提供一个事件数据模板,调用 createEventData() 所返回的对象就是该模板的副本,我们在此定义数据的属性及默认值

以下为注册一个自定义事件的示例

let sys = client.registerSystem(0, 0);
sys.initialize = function () {
    // 定义事件数据模板及各属性的初始值
    let template = {
        id: -1,
        x: 0.0,
        y: 0.0
    };
    // 注册"myevent:onTouchDown"事件
    sys.registerEventData("myevent:onTouchDown", template);
};

在注册事件之后,就可以使用 createEventData() 方法获取事件数据模板对象,返回结果的 data 属性即为注册时指定的 template 对象

5. 使用自定义事件在服务端和客户端之间通信

事件的广播范围是跨终端的,服务端可以接收到客户端广播的事件,客户端也可以监听到服务端广播的事件,因此可以通过自定义事件在服务端和客户端之间通信

以下是一个进阶版的 HelloWorld 示例:

玩家加入世界的事件为客户端事件,需要在客户端进行监听,当监听到该事件后广播一个自定义事件

在服务端监听该自定义事件,当监听到事件后执行显示标题的指令

client.js

let sys = client.registerSystem(0, 0);

sys.initialize = function () {
    // 注册自定义事件,数据仅包含一个player属性,初始值为null
    sys.registerEventData("my:player_joined", {player: null});
    // 监听玩家加入世界的事件
    sys.listenForEvent("minecraft:client_entered_world", function (eventData) {
        // 获取自定义事件的数据模板
        let sendData = sys.createEventData("my:player_joined");
        // 将player设为监听到的player
        sendData.data.player = eventData.data.player;
        // 广播该自定义事件
        sys.broadcastEvent("my:player_joined", sendData);
    })
};

server.js

let sys = server.registerSystem(0, 0);

sys.initialize = function () {
    // 监听自定义事件
    sys.listenForEvent("my:player_joined", function (eventData) {
        // 创建"minecraft:execute_command"事件的数据模板
        let commandEventData = sys.createEventData("minecraft:execute_command");
        // 设定指令内容
        commandEventData.data.command = "title @a title §dHello World !";
        // 广播事件
        sys.broadcastEvent("minecraft:execute_command", commandEventData);
    })
};

打包运行会看到如下画面

img

四、组件(Component)

在 ModPE 中要获取某个实体的坐标,可以调用 Entity.getPosition(entity) 方法,想要改变实体的坐标,只需要使用 Entity.setPosition(entity, x, y, z) 方法。但在脚本引擎里有点不一样。

对于脚本引擎来说,坐标(”minecraft:position”)是实体的一个组件(Component)。类似地,实体的生命值、碰撞箱体积、装备、库存等等,都是实体的组件。所有内置组件都可在 API文档 里查到。

与事件(Event)相同,每个组件都有使用 “minecraft” 命名空间的标识符。组件也区分服务端和客户端,游戏内置的大多数都是服务端组件,这很好理解,因为不可能让客户端轻易地修改游戏数据

System 对象提供了对组件的增删查改方法

1. 获取组件

system.getComponent(entity, id)

该方法获取指定实体的指定组件,entityEntityObject 类型的对象,id 是组件的标识符

调用该方法会获取到一个 ComponentObject 类型的对象

该对象的格式如下

  • __type__,String 类型,只读,对象的类型。该属性的值是”component”
  • __identifier__,String 类型,只读,对象的命名空间标识符。例如:若该对象的类型为”component”,且代表坐标组件,则该属性的值为”minecraft:position”
  • data:只读 – 组件所包含的数据

与事件相同,组件的数据存储于 ComponentObject 对象的 data 属性中,内容由不同的组件决定

例如:”minecraft:position” 组件包含了 x, y, z 三个属性

如果想要更改组件数据,可以更改属性的值,再将组件对象应用回实体

2. 应用组件

system.applyComponentChanges(entity, component)

修改属性的值并不会立即生效,你需要使用该方法将修改后的组件应用回实体

entity 是实体对象,component 是 ComponentObject 对象

实战

接下来,我们试着来做一个小mod —— 当世界里生成了一个苦力怕时,把苦力怕传送到 0, 100, 0 位置
由于需要操作组件,必须写在服务端脚本里

注册系统,获得 System 对象

let sys = server.registerSystem(0, 0);

在系统初始化时注册事件监听器

sys.initialize = function () {
    //当实体创建时触发
    sys.listenForEvent("minecraft:entity_created", (eventData) => onEntityCreated(eventData));
};

事件所返回的数据对象中包含了一个 entity 属性,该属性的值是一个 EntityObject 类型的对象,我们可以定义一个变量用于存储该对象。

let entity = eventData.data.entity;

EntityObject 对象都包含 identifier 属性,该属性的值是 实体的标识符

苦力怕的实体标识符为 “minecraft:creeper”,可以用如下代码判断加入世界的实体是否为苦力怕

if (entity.__identifier__ === "minecraft:creeper") {

}

接下来我们需要获取实体的坐标组件并对其进行修改。为确保万无一失,我们也可以使用 hasComponent() 方法判断一下实体是否拥有该组件

if (sys.hasComponent(entity, "minecraft:position")) {
    let comp = sys.getComponent(entity, "minecraft:position");
    // 修改组件对象的属性的值
    comp.data.x = 0;
    comp.data.y = 100;
    comp.data.z = 0;
}

最后一步,将修改过的组件应用回实体

sys.applyComponentChanges(entity, comp);

以下为服务端脚本完整的代码示例:

let sys = server.registerSystem(0, 0);

// 初始化时触发
sys.initialize = function () {
    // 当实体创建时触发
    sys.listenForEvent("minecraft:entity_created", (eventData) => onEntityCreated(eventData));
};

function onEntityCreated(eventData) {
    // 实体的对象
    let entity = eventData.data.entity;
    // 判断实体是否为苦力怕
    if (entity.__identifier__ === "minecraft:creeper") {
        // 检查实体是否拥有"坐标"组件
        if (sys.hasComponent(entity, "minecraft:position")) {
            // 获取该组件的对象
            let component = sys.getComponent(entity, "minecraft:position");
            // 修改组件
            component.data.x = 0;
            component.data.y = 100;
            component.data.z = 0;
            // 将组件应用回实体
            sys.applyComponentChanges(entity, component);
        }
    }
}

测试一下,你会看到,无论在什么地方生成苦力怕,都将被传送到 0, 100, 0 的位置。恭喜你,你已经了解 组件 的用法了

img

五、方块

六、实体查询器(Query)

在 ModPE 中,要想获取实体,除了通过几个钩子函数以外,只能使用 Level.getAllEntities() 获取所有实体。由于 JS 性能比较差,如果想在所有实体中进行过滤,可能会导致卡顿。

而脚本引擎提供了实体查询器(Query)用于查找世界中符合规则的实体。
你可以指定实体必须拥有某个组件,或者组件的值必须在某个范围内的规则。

1. 注册实体查询器

system.registerQuery(comp, field1, field2, field3)

注册一个实体查询器并指定过滤规则

  • comp —— 组件的标识符。只有当实体拥有该组件时才会被捕获
  • field1 —— 第一个 组件中的属性 的名称
  • field2 —— 第二个 组件中的属性 的名称
  • field3 —— 第三个 组件中的属性 的名称

注册成功后会返回该查询器对象,类型为 QueryObject。失败会返回 null

2. 使用实体查询器获取实体

在接下来获取实体时,你可以指定一个过滤规则 —— 为之前所传入的这三个属性指定最大值和最小值。

system.getEntitiesFromQuery(query, field_min, field2_min, field3_min, field1_max, field2_max, field3_max)

使用指定的实体查找器查找时期

  • query —— 实体查询器对象,QueryObject类型
  • field[n]_min —— 在注册实体查询器时给定的第n个属性的最小值
  • field[n]_max —— 在注册实体查询器时给定的第n个属性的最大值

成功后会返回一个数组,包含所有符合规则的实体,失败返回 null

很显然,该 API 就是为了检测实体坐标而设计的,如果你需要指定 3 个以上的属性,或是属性无法比较大小,你只能使用原始方法,注册一个空实体查询器获取所有实体,并手动实现逻辑,不过这总比没有强

3. 实践

现在我们来做一个占领据点的小MOD实践一下:

划定一个区域为据点,如果有实体在据点内停留10秒的时间,该据点会被占领
由于实体查询器需要获取组件,请务必写在服务端

注册一个System,并在初始化时注册实体查询器,将过滤组件指定为坐标,并将属性设置为 x, y, z

let sys = server.registerSystem(0, 0);
sys.initialize = function () {
    query = sys.registerQuery("minecraft:position", "x", "y", "z");
};

在系统更新时,从实体查询器中获得实体,指定坐标的的最大值和最小值。
这个坐标的范围即为 -5, 70, -5 到 5, 100, 5

sys.update = function () {
    let entities = sys.getEntitiesFromQuery(query, -5, 70, -5, 5, 100, 5);

判断是否得到了实体,如果有则提醒玩家并开始计时

    if (entities.length > 0) {
        sys.broadcastEvent("minecraft:execute_command", "title @p title §6据点正在被占领!");
        time++;
    }

计时到10秒时,提醒玩家据点被占领

    if (time === 10) {
        sys.broadcastEvent("minecraft:execute_command", "title @p title §4据点已被占领!");
    }

以下为完整的代码示例

let sys = server.registerSystem(0, 0);
// 游戏刻
let ticks = 0;
// 时间,秒
let time = 0;
// 实体查询器
let query;
// 是否已被占领
let captured = false;
// 初始化时触发
sys.initialize = function () {
    // 注册实体查询器
    query = sys.registerQuery("minecraft:position", "x", "y", "z");
};

sys.update = function () {
    ticks++;
    // 每20tick也就是1秒执行一次
    // 如果据点还未被占领
    if (ticks === 20 && !captured) {
        // 获取所有符合规则的实体
        let entities = sys.getEntitiesFromQuery(query, -5, 70, -5, 5, 100, 5);
        // 如果有实体
        if (entities.length > 0) {
            // 打印Title
            sys.broadcastEvent("minecraft:execute_command", "title @p title §6据点正在被占领!");
            // 计时自加
            time++;
        } else if (time > 0) {
            // 如果没有并且计时大于零则计时自减
            time--;
        }
        if (time === 10) {
            // 到时,打印Title
            sys.broadcastEvent("minecraft:execute_command", "title @p title §4据点已被占领!");
            captured = true;
        }
        // 重置游戏刻
        ticks = 0;
    }
};

打包运行,你会发现聊天栏出现许多错误报告,但不要紧,这不是你的问题,也没有任何影响,这只是个bug,目前(1.9.0.3)还未修复

当你站在据点内或者有其他实体在据点内时,游戏会显示据点正在被占领

img

当你停留10秒后,会显示据点已经被占领

img

至此,MOD开发的入门篇已经完结了,感谢您的阅读。

由于时间和个人能力不足,本文可能会有些缺陷,请你自己在实战中获得更多经验!

如果有问题,可以在本站留言或加入我的交流群:132538683。

发表评论

邮箱地址不会被公开。 必填项已用*标注