脚本核心功能
WARNING
脚本API目前处于积极开发阶段,经常会有破坏性变更。本文档基于Minecraft 1.21.20版本格式编写
在脚本API中,大多数核心功能都通过@minecraft/server模块实现,该模块包含大量与Minecraft世界交互的方法,涵盖实体、方块、维度等内容。本文将对部分核心API机制进行基础介绍。更详细信息请参阅微软官方文档。
初始设置
需要在manifest.json中添加脚本模块作为依赖项。
{
"dependencies": [
{
"module_name": "@minecraft/server",
"version": "1.13.0"
}
]
}事件系统
在脚本API中,@minecraft/server模块采用事件驱动架构,通过订阅事件监听器可以在特定事件发生时执行代码。
世界事件
世界事件API提供了多个事件监听器,当Minecraft世界中发生特定类型事件时会触发,例如chatSend(聊天发送)、entityHurt(实体受伤)、playerSpawn(玩家生成)、worldInitialize(世界初始化)等。
TIP
请查阅微软文档了解Minecraft中可用的世界事件类型:
订阅事件需要从world对象获取afterEvents属性。以下示例展示如何订阅方块破坏事件:
import { world } from "@minecraft/server";
// 订阅方块破坏事件
// 当玩家破坏方块时触发
world.afterEvents.playerBreakBlock.subscribe((event) => {
const player = event.player; // 触发事件的玩家
const block = event.block; // 被破坏的方块(注意此时该方块的typeId始终为空气)
const permutation = event.brokenBlockPermutation; // 返回方块被破坏前的状态信息
player.sendMessage(
`你破坏了位于 ${block.x}, ${block.y}, ${block.z} 的 ${permutation.type.id}`
); // 向玩家发送消息
});系统事件
系统事件在Minecraft附加包系统范围内发生特定类型事件时触发。
从system对象获取beforeEvents属性。以下示例展示如何订阅watchdogTerminate事件,允许API在游戏超过性能边界时取消性能监视器关闭世界的操作(具体行为取决于脚本环境配置):
import { system } from "@minecraft/server";
// 订阅watchdogTerminate事件
system.beforeEvents.watchdogTerminate.subscribe((event) => {
event.cancel = true; // 取消世界关闭操作(这将终止脚本引擎)
console.warn("已取消类型为 " + event.terminateReason + " 的严重异常"); // 如果事件触发则在控制台打印警告
});脚本事件
脚本事件(ScriptEvents,注意不要与世界事件或系统事件混淆)允许我们通过注册scriptEventReceive事件处理器来响应输入的/scriptevent命令。当玩家、NPC或方块调用/scriptevent命令时会触发该事件。更多信息请参阅脚本事件文档。
/scriptevent <消息ID: 字符串> <消息内容: 字符串>- 命令中的
messageId可通过ScriptEventCommandMessageEvent.id获取 - 命令中的
message可通过ScriptEventCommandMessageEvent.message获取
示例:
输入命令:
/scriptevent wiki:test Hello World事件监听器返回内容:
import { system } from "@minecraft/server";
system.afterEvents.scriptEventReceive.subscribe((event) => {
const {
id, // 返回字符串 (wiki:test)
initiator, // 返回实体 (如果是NPC触发的命令则返回undefined)
message, // 返回字符串 (Hello World)
sourceBlock, // 返回方块 (如果是方块触发的命令则返回undefined)
sourceEntity, // 返回实体 (如果是实体触发的命令则返回undefined)
sourceType, // 返回MessageSourceType (可能值为'Block'、'Entity'、'NPCDialogue'或'Server')
} = event;
});任务调度
我们可能需要在未来某个特定时间执行函数,这称为"调度调用"。
在脚本API中,原生JavaScript方法如setTimeout和setInterval不可用。Minecraft实现了基于游戏刻(tick)而非真实时间的调度方法。
这些方法可通过导入的system对象访问:
import { system } from "@minecraft/server";提供以下两种主要方法:
调度定时器system.run(callback) - 在下一个可用时间点执行指定函数。常用于实现延迟行为和游戏循环。在事件处理器中调用时,通常会在事件发生的同一刻末尾执行代码;在其他代码中调用时,会在下一刻执行(但根据系统负载情况,不能保证严格在同一刻或下一刻执行)。
import { system, world } from "@minecraft/server";
system.run(() => {
world.sendMessage("这条消息会在上一刻之后的一刻显示");
});system.runInterval(callback: () => void, tickInterval?: number): number - 周期性重复执行代码,从第一次间隔时间后开始,之后每隔指定间隔重复执行。
import { system, world } from "@minecraft/server";
system.runInterval(() => {
world.sendMessage("这条消息每20刻显示一次(每秒一次)");
}, 20);system.runTimeout(callback: () => void, tickDelay?: number): number - 在指定时间间隔后执行一次函数。
import { system, world } from "@minecraft/server";
system.runTimeout(() => {
world.sendMessage("这条消息将在20刻后显示一次");
}, 20);system.runJob(generator: Generator<void, void, void>): number - 将生成器函数加入队列运行直至完成。生成器每刻会获得一个时间片,运行直至yield或完成。生成器函数参考。
import { system, world, BlockPermutation } from "@minecraft/server";
function* blockPlacingGenerator(size, startX, startY, startZ) {
const overworld = world.getDimension("overworld"); // 获取主世界维度
for (let x = startX; x < startX + size; x++) {
for (let y = startY; y < startY + size; y++) {
for (let z = startZ; z < startZ + size; z++) {
const block = overworld.getBlock({ x: x, y: y, z: z }); // 获取当前循环坐标处的方块
if (block) block.setType("minecraft:cobblestone"); // 如果方块已加载,则设置为圆石
// 每放置一个方块后让出控制权
yield;
}
}
}
}
// 在主世界坐标(-2, -60, 1)处开始建造10x10x10的圆石立方体
system.runJob(blockPlacingGenerator(10, -2, -60, 1));清除定时器
system.clearRun(runId): void - 取消通过run、runTimeout或runInterval调度的函数执行。
import { system, world } from "@minecraft/server";
const callbackId = system.runInterval(() => {
world.sendMessage("每刻运行一次");
});
system.runTimeout(() => {
system.clearRun(callbackId); // 20刻后停止system.runInterval回调
world.sendMessage("已停止");
}, 20);clearJob(jobId: number): void - 取消通过runJob调度的任务执行。
import { system, world } from "@minecraft/server";
const callbackId = system.runInterval(() => {
world.sendMessage("每刻运行一次");
});
system.runTimeout(() => {
system.clearRun(callbackId); // 20刻后停止system.runInterval回调
world.sendMessage("已停止");
}, 20);更多系统方法信息请参阅游戏循环与定时回调文档。
数据存储与读取
通过@minecraft/server模块,开发者可以定义自己的自定义属性(称为动态属性),这些属性可以在Minecraft中使用和存储。数据会使用行为包头部UUID专门存储在世界文件夹的db目录中。

存储数据前需要先初始化属性。有多种方式可以声明动态属性,可以在实体、世界或物品上定义。虽然可以定义任意数量的数值和布尔值,但Minecraft API对每个行为包的动态属性数据存储量有限制:
- 字符串动态属性最大长度为32767个字符
- 数值动态属性最大值为64位浮点数限制范围(-1.7976931348623158e+308到-2.2250738585072014e-308,或2.2250738585072014e-308到1.7976931348623158e+308)
获取和设置动态属性
可以使用getDynamicProperty和setDynamicProperty方法获取和设置动态属性。
TIP
注意获取动态属性时不能保证该属性已有存储值。首次获取属性时方法会返回undefined。
以下是Minecraft中获取和设置动态属性的示例:
import { system, world } from "@minecraft/server";
system.runInterval(() => {
world.getPlayers().forEach((player) => {
// 为每个玩家执行代码
// 这三个属性对每个玩家都是唯一的,类似于标签/记分板数据
player.setDynamicProperty("number_value", 12); // 设置玩家的数值属性
player.setDynamicProperty("string_value", "这是一个字符串 :)"); // 字符串属性
player.setDynamicProperty("boolean_value", true); // 布尔属性
});
}, 20); // 每20游戏刻运行一次
world.afterEvents.playerBreakBlock.subscribe((data) => {
// 订阅方块破坏事件
const player = data.player; // 定义玩家变量供后续使用
const numberProperty = player.getDynamicProperty("number_value"); // 获取已存储的动态属性
player.sendMessage(`你的属性值是 ${numberProperty}!`); // 将玩家存储的值打印到聊天栏
});以下是全局级别获取和设置动态属性的示例:
import { world } from "@minecraft/server";
world.setDynamicProperty("player_score", 100); // 设置一个数值属性
const playerScore = world.getDynamicProperty("player_score"); // 获取之前设置的属性-将返回100执行命令
Entity.runCommandAsync()或Dimension.runCommandAsync()允许API从更广泛的维度上下文异步执行特定命令。注意每个刻最多只能运行128个异步命令。应尽可能避免使用runCommandAsync调用,优先使用内置API方法。
游戏会在世界的下一个刻执行队列中的命令。要使命令与脚本并行运行,需要将代码包裹在异步函数中。
import { world } from "@minecraft/server";
(async () => {
await world.getDimension("overworld").runCommandAsync("say 在维度上使用say命令");
world.sendMessage("这条消息会在runCommandAsync执行后显示");
})();返回Promise<CommandResult>。如果队列已满会同步抛出错误。
脚本中应避免使用命令
通常我们建议避免使用命令,因为通过脚本API运行命令速度较慢,随着时间推移执行更多命令会导致服务器性能下降。但以下命令功能尚未在脚本API中实现,我们别无选择只能使用runCommand或runCommandAsync。
末影箱
脚本API没有提供任何获取/设置玩家末影箱信息的方法。可以使用/replaceitem、/clear、@s[hasitem=]等命令作为替代方案。
常加载区域
脚本API无法访问、设置或移除常加载区域。
踢出玩家
脚本API无法踢出玩家。
设置方块
脚本API无法破坏方块/setblock ... destroy。虽然可以设置方块。
玩家能力
- 脚本API无法为每个玩家设置能力
- 无法读取玩家能力
execute命令
脚本API可以利用新的execute语法运行带有大量if/unless条件的命令以简化操作或提高性能。
/execute可用于触发/loot命令,因为runCommandAsync无法直接访问原版战利品表。
Minecraft函数
- 脚本API无法在不使用
/function的情况下运行Minecraft函数文件
定位
- 脚本API无法获取结构位置
- 无法获取生物群系位置
战利品
- 虽然战利品系统从一开始就有问题,但对于向玩家/世界掉落或设置物品很有用
天气
- 脚本API无法获取/设置世界天气
难度
- 脚本API无法设置世界难度
生物事件
- 脚本API无法启用/禁用生物事件
雾效
- 脚本API无法管理玩家的活动雾效设置
停止声音
- 脚本API无法停止播放声音。可以使用
World::stopMusic()或Player::stopMusic()停止音乐
对话
- 脚本API无法向玩家打开NPC对话
- 无法更改NPC显示的对话内容
BeforeEvents权限系统
TIP
开发者可能会在微软文档中发布关于此主题的文章,但目前这是社区收集的信息。
在1.20.0版本中,Minecraft脚本API为前置事件(如ChatSendBeforeEvent)中的回调引入了权限系统。
这限制了在前置事件回调中允许执行的本地函数。这些本地函数会在同一刻中修改世界状态(如使用World::setTimeOfDay()设置世界时间)。此实现的目的是避免在游戏刻中间产生级联更改。
import { world } from "@minecraft/server";
world.beforeEvents.chatSend.subscribe((event) => {
event.cancel = true;
world.setTimeOfDay(0);
});在上面的示例代码中,发送到聊天栏的消息被取消,同时设置了世界时间。world.setTimeOfDay()会抛出错误,因为本地函数没有在事件触发的同一刻更改世界状态所需的权限。
要使代码适应此新系统,必须使用以下方法在事件触发后的刻中运行这些需要权限的本地函数:
- 使用
system.run:
import { world, system } from "@minecraft/server";
world.beforeEvents.chatSend.subscribe((event) => {
event.cancel = true;
system.run(() => {
world.setTimeOfDay(0);
});
});为了适应新的权限系统,world.setTimeOfDay()函数被包裹在system.run()方法中,这会将其执行延迟一刻。确保函数不会在前置事件触发的同一刻执行。
- 使用
system.runTimeout:
import { world, system } from "@minecraft/server";
world.beforeEvents.chatSend.subscribe(async (event) => {
event.cancel = true;
system.runTimeout(() => {
world.setTimeOfDay(0);
}, 5);
});此代码功能与system.run()示例非常相似,但可以在超时中指定自定义长度,从而更精确地控制代码触发时间。
- 使用
async函数在之后的刻中执行:
import { world } from "@minecraft/server";
world.beforeEvents.chatSend.subscribe(async (event) => {
// 同步代码
event.cancel = true;
// 异步代码
await sleep(10); // 假设有一个sleep函数返回10刻后解析的promise
world.setTimeOfDay(0);
});通过使用等待超过一刻的async函数可以绕过权限系统。由于await之前的代码是同步运行的,因此可以使用event.cancel = true取消事件,然后在之后的刻中继续执行操作。注意仅使用async/await调用是不够的,因为以这种方式运行的代码在遇到阻塞调用(如示例中的sleep()函数)之前仍然是同步执行的。




