KubeJS 进阶教程:完整实战案例¶
原文:原创教程(ぴよまる)
基础篇学会了「监听事件 + 操作对象」,这篇进阶教程带你把 KubeJS 用深入:数据持久化、自定义方块实体、定时任务、自定义命令、战利品表、世界生成、网络通信……每节都是能直接抄进 kubejs/ 就能用的完整案例。
1. 事件高级技巧¶
1.1 取消事件(event.cancel())¶
可取消事件(事件列表里标 ✅ 的)调用 event.cancel() 就能阻止行为:
// server_scripts/anti_cheat.js
// 这个文件要放在 kubejs/server_scripts 文件夹里,服务器启动时会自动运行
// 禁止破坏基岩
BlockEvents.broken(event => { // 监听"方块被破坏"事件;event 是装着这次事件所有信息的对象
if (event.block.id === 'minecraft:bedrock') { // 判断:如果被破坏的方块是基岩(bedrock)
event.cancel() // 破坏被取消(阻止这次破坏发生,基岩就挖不掉了)
event.player.tell('你不能破坏基岩!') // tell() 是给玩家发一条聊天框消息
}
})
// 禁止苦力怕爆炸
EntityEvents.death(event => { // 监听"实体死亡"事件
if (event.entity.type === 'minecraft:creeper') { // 判断:如果死掉的实体是苦力怕
event.cancel() // 苦力怕"死不掉"(先于爆炸)——取消死亡事件,它就不会爆炸了
}
})
1.2 带条件的监听¶
事件回调里先判断,再决定做什么:
// server_scripts/player_rules.js
PlayerEvents.chat(event => { // 监听"玩家在聊天框发消息"事件
let msg = event.message.toLowerCase() // 把玩家输入的消息转成小写,存进变量 msg(这样不管大写小写都能识别)
// 屏蔽脏话(示例词)
let badWords = ['fuck', 'shit', '傻逼'] // 定义一个"脏话清单",数组里放几个示例词
if (badWords.some(word => msg.includes(word))) { // some() 检查清单里有没有任何一个词出现在消息里,有就返回 true
event.cancel() // 有脏话:取消这条消息,不让它发出去
event.player.tell('说话文明一点!') // 私聊提醒玩家
return // 提前结束这个函数,后面的代码不再执行
}
// 自定义聊天前缀
if (event.player.username === 'pyz') { // 判断:发消息的人是不是叫 pyz
event.message = '§c[服主]§r ' + event.message // 是的话给消息前面加"服主"称号(§c 是红色颜色代码,§r 恢复默认色)
}
})
1.3 同一事件多个监听器¶
同一个事件可以写多个 addEventListener,都会执行:
// server_scripts/welcome.js
PlayerEvents.loggedIn(event => { // 监听"玩家登录进服"事件
event.player.tell('§a欢迎回来,' + event.player.username + '!') // 给登录的玩家发欢迎消息(+ 号是把文字和名字拼起来)
})
PlayerEvents.loggedIn(event => { // 同一个事件可以再监听一次,两段代码都会执行
if (event.player.username === 'pyz') { // 判断:登录的人是不是 pyz
event.player.tell('§d主人好~') // 是的话额外说一句"主人好"(§d 是粉色)
}
})
2. 玩家数据持久化(persistentData)¶
persistentData 是挂在实体/方块/玩家上的持久 NBT 数据,服务器重启也不会丢。最常用的就是它:
// server_scripts/kill_counter.js
// 统计每个玩家的击杀数
EntityEvents.death(event => { // 监听"实体死亡"事件
let player = event.source.player // event.source 是"死亡原因",从中拿到凶手玩家;如果是摔死等非玩家原因,这里就是 null
if (!player) return // 判断:如果没有凶手玩家(不是玩家杀的),就提前结束,不统计
let data = player.persistentData // persistentData 是玩家的永久存档数据,服务器重启也不会丢
data.kills = (data.kills || 0) + 1 // 击杀数 +1:没记录过就当 0,再加 1,存回去
})
// 用命令查看:/reload 后击杀另一个玩家,再执行任意命令看日志
PlayerEvents.chat(event => { // 监听"玩家发消息"事件
if (event.message === '!kills') { // 判断:玩家输入的是不是 !kills
let kills = event.player.persistentData.kills || 0 // 从存档里取出击杀数,没记录过就当 0
event.player.tell('你的击杀数:' + kills) // 把击杀数告诉玩家
}
})
💡
persistentData可以存数字、字符串、布尔、数组、对象,随意嵌套:data.stats = { kills: 1, deaths: 2 }。
3. 自定义方块实体(BlockEntity)¶
方块实体 = 带「数据/逻辑」的方块(箱子、熔炉这类)。KubeJS 可以注册带 tick 逻辑的方块实体:
// startup_scripts/machine.js
// startup 脚本在游戏启动时运行一次,负责"注册"新东西
StartupEvents.registry('block', event => { // 开始注册方块;event 提供注册方法
event.create('example_machine') // 创建一个叫 example_machine 的方块(完整 ID 是 kubejs:example_machine)
.displayName('示例机器') // 设置方块在游戏里显示的名字
.material('metal') // 设置材质为金属(影响声音、粒子效果等)
.hardness(3.0) // 设置挖掘硬度 3.0(数字越大越难挖)
.blockEntity(entity => { // 给方块绑定"方块实体"(让方块能存数据);entity 是配置对象
// 方块实体类型注册(KubeJS 自动处理)
})
})
// server_scripts/machine_tick.js
// 方块实体每 tick 逻辑:附近的玩家回血
BlockEvents.rightClicked(event => { // 监听"右键点击方块"事件
let block = event.block // 取出被点击的方块
if (block.id === 'kubejs:example_machine') { // 判断:点的是不是我们做的示例机器
let data = block.persistentData // 拿到这个方块自己的永久存档数据
data.clicks = (data.clicks || 0) + 1 // 点击次数 +1(没记录过就当 0)
event.player.tell('你点击了机器 ' + data.clicks + ' 次!') // 告诉玩家这是第几次点击
event.cancel() // 阻止默认行为(如打开合成界面)——不让方块执行它原本的默认动作
}
})
⚠️ 方块实体的复杂 tick 逻辑(如容器 GUI)需要结合 Rhino 与 Java 类,KubeJS 原生支持有限,进阶玩家可配合 BEJS(Block Entity JS 插件)实现完整机器。
方块检测器(Detector)¶
KubeJS 自带检测器方块(kubejs:detector),配合事件做红石逻辑:
// server_scripts/detector.js
BlockEvents.detectorPowered(event => { // 监听"检测器方块被激活(通电)"事件
event.block.popItem('minecraft:diamond') // 检测器被激活时掉钻石(popItem 是在方块位置丢出一个物品)
event.server.runCommandSilent('say 检测器激活!') // 悄悄执行命令:全服广播一句话(Silent 表示不输出命令回显)
})
BlockEvents.detectorUnpowered(event => { // 监听"检测器断电"事件
console.log('检测器断电') // 在服务器日志里打印一行字,方便调试
})
4. 完整案例 A:会发光的「幸运方块」¶
一个完整的自定义方块案例(启动注册 + 服务端逻辑):
// startup_scripts/lucky_block.js
StartupEvents.registry('block', event => { // 启动时注册方块
event.create('lucky_block') // 创建幸运方块
.displayName('幸运方块') // 显示名
.material('metal') // 金属材质
.hardness(2.0) // 硬度 2.0,不算难挖
.lightLevel(0.5) // 半亮度(0~1,1 是最亮)
.requiresTool(true) // 需要工具挖掘(空手挖不动)
.tagBlock('mineable/pickaxe') // 镐子可挖(打上"可以用镐挖"的标签)
})
// server_scripts/lucky_block.js
BlockEvents.broken(event => { // 监听"方块被破坏"事件
if (event.block.id !== 'kubejs:lucky_block') return // 判断:如果破坏的不是幸运方块,就提前结束这个函数
let player = event.player // 拿到破坏方块的玩家
let roll = Math.random() // 0 ~ 1 随机数(生成一个 0 到 1 之间的小数,用来抽奖)
if (roll < 0.3) { // 判断:随机数小于 0.3,也就是 30% 概率
player.tell('§a幸运!获得钻石!') // 告诉玩家中奖了
player.give('3x minecraft:diamond') // 给玩家发物品,'3x minecraft:diamond' 表示 3 个钻石('数量x 物品ID' 格式)
} else if (roll < 0.6) { // 否则再判断:小于 0.6,也就是落在 30%~60% 这一档(又是 30% 概率)
player.tell('§e还行,获得铁锭。')
player.give('5x minecraft:iron_ingot') // 发 5 个铁锭
} else if (roll < 0.9) { // 再否则:小于 0.9,即落在 60%~90% 这一档(30% 概率)
player.tell('§c倒霉,召唤苦力怕!')
player.server.runCommandSilent('summon minecraft:creeper ' + player.x + ' ' + player.y + ' ' + player.z) // 在玩家所在坐标召唤一只苦力怕(summon 是游戏命令,player.x/y/z 是玩家坐标)
} else { // 剩下的情况:随机数 ≥ 0.9,也就是最后 10% 概率——超级大奖
player.tell('§d超级大奖!100 经验!')
player.give('10x minecraft:emerald') // 发 10 个绿宝石
player.server.runCommandSilent('experience add ' + player.username + ' 100') // 用命令给玩家加 100 点经验
}
})
💡 常用方法速记:
player.give('物品id 数量')、player.tell('消息')、server.runCommandSilent('命令')、event.block.popItem(...)。
5. 定时任务(ScheduledEvent)¶
用 server.scheduleInTicks(ticks, callback) 做延时/周期逻辑(20 ticks = 1 秒):
// server_scripts/auto_tasks.js
// 玩家进服 5 秒后发提示
PlayerEvents.loggedIn(event => { // 监听"玩家登录"事件
let player = event.player // 拿到登录的玩家
player.server.scheduleInTicks(100, () => { // 100 ticks = 5 秒(20 tick 等于 1 秒);把里面的代码推迟到 5 秒后执行
if (player.health > 0) { // 判断:玩家还活着(血量大于 0)
player.tell('欢迎!记住规则:禁止作弊,友好交流。') // 5 秒后才给玩家发这条提示
}
})
})
// 周期性广播(每 10 分钟)
ServerEvents.loaded(event => { // 监听"服务器加载完成"事件(开服时触发一次)
event.server.scheduleInTicks(12000, () => { // 12000 tick = 10 分钟,先等 10 分钟
event.server.runCommandSilent('say 服务器运行中,玩得开心!') // 时间一到,全服广播一句话
// 再次调度,形成循环
event.server.scheduleInTicks(12000, arguments.callee) // arguments.callee 指"当前这个函数自己"——每 10 分钟再安排一次自己,形成无限循环
})
})
⚠️ 循环调度注意:
arguments.callee指当前函数(递归调度自己)。更稳妥的做法是写具名函数:
function broadcastLoop(server) { // 定义一个"具名"函数:循环广播;参数 server 是服务器对象
server.runCommandSilent('say 服务器运行中!') // 全服广播一句话
server.scheduleInTicks(12000, () => broadcastLoop(server)) // 10 分钟后再次调用自己,形成循环(比 arguments.callee 更清晰好懂)
}
ServerEvents.loaded(event => { // 监听"服务器加载完成"事件
broadcastLoop(event.server) // 开服时调用一次,启动循环广播
})
6. 自定义命令¶
用 ServerEvents.commandRegistry 注册自己的命令(需要权限等级 2 = OP):
// server_scripts/my_commands.js
ServerEvents.commandRegistry(event => { // 监听"注册命令"事件;event 提供 register 方法
// 注册 /hello
event.register('hello', command => { // 注册一条叫 hello 的命令(游戏里输入 /hello 使用)
command.permissionLevel(2) // 设置权限等级 2 = OP(管理员)才能用
command.executes(ctx => { // 设置命令真正执行时要运行的代码;ctx 是"命令执行上下文"
let player = ctx.source.player // 从上下文里取出执行命令的玩家
player.tell('你好,' + player.username + '!这是 KubeJS 命令。') // 给玩家发一条问候消息
return 1 // 返回 1 表示命令执行成功(游戏要求返回一个数字)
})
})
// 注册 /giveall <物品> —— 全服发物品
event.register('giveall', command => { // 注册第二条命令 giveall
command.permissionLevel(2) // 同样需要 OP 权限
command.argument('item', 'string', argument => { // 给命令加一个参数 item,类型是字符串(玩家输入 /giveall 后面跟的物品 ID)
argument.executes(ctx => { // 参数填写完整后执行
let item = ctx.arguments.item // 从上下文里取出玩家输入的参数值(物品 ID)
ctx.source.server.players.forEach(p => { // 遍历服务器上的每一个玩家(forEach 就是"每个都做一遍")
p.give(item) // 给当前这个玩家发物品
})
ctx.source.server.runCommandSilent('say 全服发放:' + item) // 全服广播发放了什么东西
return 1 // 命令执行成功
})
})
})
})
⚠️ 命令参数类型:
'string'、'integer'、'float'、'player'、'blockpos'等,参数值从ctx.arguments.参数名取。
7. 战利品表进阶¶
7.1 修改方块掉落¶
// server_scripts/loot.js
ServerEvents.blockLootTables(event => { // 监听"方块战利品表"事件,用来修改方块掉落
// 挖石头额外掉钻石(10%)
event.modifyBlock('minecraft:stone', table => { // 修改石头的掉落表;table 是掉落表对象
table.addPool(pool => { // 添加一个掉落池(相当于一次"抽奖机会")
pool.addItem('minecraft:diamond', 1, 10) // 1 个,权重 10(权重越大越容易掉;石头本身还有其他掉落,所以大约 10%)
})
})
// 自定义方块掉落自己(幸运方块)
event.modifyBlock('kubejs:lucky_block', table => { // 修改幸运方块的掉落表
table.addPool(pool => {
pool.addItem('kubejs:lucky_block') // 掉落自己,这样挖掉幸运方块还能捡回来
})
})
})
7.2 实体掉落¶
ServerEvents.entityLootTables(event => { // 监听"实体战利品表"事件
// 击杀僵尸必掉铁锭
event.modifyEntity('minecraft:zombie', table => { // 修改僵尸的掉落表
table.addPool(pool => {
pool.addItem('minecraft:iron_ingot', 1, 1) // 必掉 1 个铁锭(池里只有这一项,权重多少都会掉)
})
})
})
7.3 箱子战利品(地牢/要塞)¶
ServerEvents.chestLootTables(event => { // 监听"箱子战利品表"事件,修改地牢等箱子里的东西
event.modify('minecraft:chests/simple_dungeon', table => { // 修改"简易地牢"箱子的战利品表
table.addPool(pool => {
pool.addItem('minecraft:enchanted_golden_apple', 1, 2) // 附魔金苹果 1 个,权重 2
pool.addItem('minecraft:diamond_sword', 1, 5) // 钻石剑 1 把,权重 5(比金苹果更容易出)
})
})
})
8. 世界生成进阶¶
8.1 添加矿石(addOre 快捷方法)¶
// startup_scripts/worldgen.js
WorldgenEvents.add(event => { // 监听"世界生成-添加"事件,往世界里加新东西
event.addOre(ore => { // 添加一种矿石生成规则;ore 是配置对象
ore.id = 'kubejs:ruby_ore' // 生成的矿石方块(要生成的是红宝石矿石)
ore.addTarget('#minecraft:stone', 'kubejs:ruby_ore') // 替换什么:把普通石头(# 开头表示标签,代表所有石头)替换成红宝石矿石
ore.count(6) // 每区块最多 6 团(一个区块里最多生成 6 堆)
ore.squared() // 随机散布(让生成位置在水平方向随机分布)
ore.heightRange(0, 40) // Y 0~40(只在高度 0 到 40 之间生成)
ore.biomeFilter(biome => { // 设置生成的群系(生物群系)条件
biome.include('minecraft:plains') // 只在平原生成
})
})
})
8.2 移除默认地物¶
WorldgenEvents.remove(event => { // 监听"世界生成-移除"事件,用来删掉默认生成的东西
// 移除所有钻石矿石生成
event.removeOres(ore => { // 按矿石生成规则移除
ore.addTarget('#minecraft:stone', 'minecraft:diamond_ore') // 匹配"把石头替换成钻石矿石"这条规则,把它删掉
})
})
9. 网络通信(客户端 ↔ 服务端)¶
KubeJS 支持自定义数据包在两端传数据:
// client_scripts/network.js
// 客户端收到服务端消息
NetworkEvents.fromServer(event => { // 监听"从服务端收到数据"事件(写在这份客户端脚本里)
console.log('收到服务端数据:', event.data) // 把收到的数据打印到客户端日志
if (event.data.action === 'notify') { // 判断:数据里带的 action 是不是 notify(通知)
// 弹提示(需要配合其他 UI 方式,这里只是示例)
console.log('通知:' + event.data.text) // 打印通知内容
}
})
// server_scripts/network.js
// 服务端收到客户端消息
NetworkEvents.fromClient(event => { // 监听"从客户端收到数据"事件(写在这份服务端脚本里)
let player = event.player // 拿到发来数据的玩家
console.log(player.username + ' 发送了数据:', event.data) // 在服务器日志里打印谁发了什么
player.tell('已收到你的数据!') // 回一条消息告诉玩家收到了
})
⚠️ 发送数据端需要频道注册,不同版本 API 有差异(
player.sendData(...)或Network.send(...)),以你所用 KubeJS 版本官方文档为准。
10. 完整案例 B:多功能「传送法杖」¶
综合运用:右键传送 + 冷却 + 数据持久化:
// startup_scripts/wand.js
StartupEvents.registry('item', event => { // 启动时注册物品
event.create('teleport_wand') // 创建传送法杖
.displayName('传送法杖') // 显示名
.maxStackSize(1) // 一组最多 1 个(法杖不能堆叠)
.tooltip('右键:向前传送 10 格(冷却 5 秒)') // 鼠标放上去时显示的说明文字
})
// server_scripts/wand.js
const COOLDOWN_TICKS = 100 // 5 秒(定义常量:冷却时间 100 tick;20 tick 是 1 秒,所以是 5 秒)
ItemEvents.rightClicked(event => { // 监听"右键使用物品"事件
if (event.item.id !== 'kubejs:teleport_wand') return // 判断:用的不是传送法杖就直接结束函数
let player = event.player // 拿到使用物品的玩家
let data = player.persistentData // 拿到玩家的永久存档数据
// 冷却检查
let now = player.server.tick // 服务器当前的总 tick 数(相当于游戏世界的时间戳)
if (data.lastWandUse && now - data.lastWandUse < COOLDOWN_TICKS) { // 判断:上次使用过,而且距离现在还没到 100 tick(冷却没结束)
let remain = Math.ceil((COOLDOWN_TICKS - (now - data.lastWandUse)) / 20) // 算还剩几秒:剩余 tick 除以 20,再向上取整
player.tell('§c冷却中,还有 ' + remain + ' 秒!') // 告诉玩家还要等几秒
return // 提前结束,不执行传送
}
// 计算传送目标:玩家面朝方向 +10 格
let dx = player.lookDirection.x * 10 // 玩家面朝方向的 x 分量乘 10,得到 x 方向要移动的距离
let dz = player.lookDirection.z * 10 // 同理算出 z 方向要移动的距离(y 是上下,先不管)
let target = { x: player.x + dx, y: player.y, z: player.z + dz } // 目标点 = 当前位置 + 面朝方向 × 10
// 传送到目标(保持 y 不变,简化处理)
player.server.runCommandSilent( // 执行一条游戏命令(反引号是模板字符串,里面 ${} 可以塞变量)
`tp ${player.username} ${target.x.toFixed(1)} ${target.y.toFixed(1)} ${target.z.toFixed(1)}` // tp 是传送命令;toFixed(1) 把坐标保留 1 位小数,命令更干净
)
data.lastWandUse = now // 记录使用时间(把当前 tick 存进存档,作为下次冷却判断的依据)
player.tell('§a传送完成!')
})
11. 全局脚本工具(server_scripts/utils.js)¶
把常用逻辑写成全局函数,所有脚本都能调用:
// server_scripts/utils.js —— 全局工具函数
function giveItem(player, itemId, count = 1) { // 定义全局函数:给玩家发物品;count = 1 表示不传数量时默认发 1 个
player.give((count > 1 ? count + 'x ' : '') + itemId) // 数量大于 1 时拼成 "3x 物品ID" 的格式,否则直接发 1 个
}
function randomFrom(array) { // 定义全局函数:从数组里随机抽一个元素
return array[Math.floor(Math.random() * array.length)] // Math.random() 生成 0~1 随机小数,乘数组长度再向下取整,得到随机下标
}
function isNight(level) { // 定义全局函数:判断当前是不是晚上
return level.dayTime % 24000 >= 13000 // 游戏一天是 24000 tick,13000 之后是夜晚;% 是取余数,把整天的部分去掉只留今天的时间
}
// 其他脚本直接调用:
// giveItem(player, 'minecraft:apple', 3)
// let gift = randomFrom(['minecraft:apple', 'minecraft:bread'])
12. 性能与调试技巧¶
12.1 别在 tick 事件里做重活¶
每 tick 触发的事件(ServerEvents.tick、EntityEvents 高频事件)里不要频繁创建对象、不要循环大量实体:
// ❌ 不好:每 tick 遍历所有实体
ServerEvents.tick(event => { // 监听"每 tick"事件(每秒触发 20 次,非常频繁)
event.server.allLevels.forEach(level => { // 遍历所有维度(主世界、地狱、末地等)
level.getEntities().forEach(entity => { /* 卡! */ }) // 再遍历每个维度里的所有实体——每 tick 全服扫一遍,服务器会卡死
})
})
// ✅ 推荐:低频检查(每 40 tick 一次)
ServerEvents.tick(event => { // 同样监听每 tick 事件
if (event.server.tick % 40 === 0) { // 判断:当前 tick 能不能被 40 整除——能的话就每 40 tick(2 秒)才执行一次
// 低频逻辑放这里
}
})
12.2 调试三板斧¶
console.log('变量值:', myVar) // 看日志(把变量值打印到日志文件,适合查问题)
event.player.tell('调试信息:' + value) // 游戏内看(直接在游戏聊天框显示)
event.server.runCommandSilent('say ...') // 全服广播调试(让所有玩家看到调试信息)
日志位置:游戏目录/logs/kubejs/server.txt(服务端脚本)和 client.txt(客户端脚本)。
12.3 常见卡服/报错原因¶
| 症状 | 原因 | 解决 |
|---|---|---|
| 脚本不生效 | 放错文件夹 | 对照事件类型选 startup/server/client |
报错 Cannot read properties of undefined |
对象不存在 | 先 console.log(event) 看结构 |
| 服务器卡顿 | tick 里重活 | 用 % N === 0 降频 |
| 物品没生成 | ID 拼写错 | 用 /kubejs hand 或 JEI 查真实 ID |
13. 完整案例 C:简易「签到系统」¶
把本篇所有知识串起来——玩家每天签到领奖:
// server_scripts/daily_signin.js
const REWARDS = [ // 定义奖励清单(一个数组,里面装 4 种奖励)
'5x minecraft:iron_ingot', // 5 个铁锭
'3x minecraft:gold_ingot', // 3 个金锭
'1x minecraft:diamond', // 1 个钻石
'2x minecraft:emerald' // 2 个绿宝石
]
PlayerEvents.chat(event => { // 监听"玩家发消息"事件
if (event.message !== '!签到') return // 判断:玩家没输入"!签到"就提前结束
let player = event.player // 拿到签到的玩家
let data = player.persistentData // 拿到永久存档数据
// 判断今天是否签过:记录上次签到日期(游戏天数)
let today = player.server.overworld.dayTime / 24000 // 游戏天数(把总 tick 除以 24000,从第几天开始;可能带小数)
if (data.lastSignDay === Math.floor(today)) { // 判断:上次签到日期(取整后)是不是今天——是就说明今天已经签过了
player.tell('§c你今天已经签过到啦,明天再来!')
return // 提前结束
}
data.lastSignDay = Math.floor(today) // 把今天的日期存进存档(表示今天签过了)
data.signCount = (data.signCount || 0) + 1 // 签到总次数 +1(第一次就按 0 算)
// 随机奖励
let reward = REWARDS[Math.floor(Math.random() * REWARDS.length)] // 随机抽一个奖励:随机数乘数组长度再取整,得到数组下标
player.give(reward) // 把奖励发给玩家
player.tell('§a签到成功!这是你第 ' + data.signCount + ' 次签到,获得:' + reward) // 告诉玩家结果
})
14. 自定义物品进阶(食物 / 工具 / 剑 / 盔甲)¶
14.1 自定义食物¶
// startup_scripts/items.js
StartupEvents.registry('item', event => { // 启动时注册物品
event.create('magic_apple') // 创建魔法苹果
.displayName('魔法苹果') // 显示名
.food(food => { // 配置它是食物;food 是配置对象
food.hunger(6) // 恢复 6 格饥饿值(一格是半个鸡腿)
food.saturation(8) // 饱和度(吃饱后的"隐藏饱腹",越高越耐饿)
food.effect('minecraft:regeneration', 200, 1, 1) // 回血效果 10 秒(200 tick = 10 秒;1 是效果等级,1 是概率 100%)
food.effect('minecraft:absorption', 1200, 2, 1) // 伤害吸收 60 秒(1200 tick = 60 秒;等级 2 多 2 颗金心)
food.alwaysEdible() // 饥饿值满也能吃(普通食物吃饱了就吃不了)
})
})
14.2 自定义工具(镐子)¶
StartupEvents.registry('item', event => { // 启动时注册物品
event.create('ruby_pickaxe', 'pickaxe') // 创建红宝石镐(第二个参数 'pickaxe' 指定物品类型是镐子)
.displayName('红宝石镐')
.tier(tier => { // 配置工具的等级属性;tier 是配置对象
tier.durability(1500) // 耐久(能挖 1500 次)
tier.speed(8) // 挖掘速度(数字越大挖得越快)
tier.attackDamageBonus(4) // 攻击伤害加成(用它打人额外 +4 伤害)
tier.level(3) // 挖掘等级(3 = 钻石级,能挖黑曜石、钻石矿)
tier.enchantmentValue(15) // 附魔能力(越高越容易附出好属性)
})
.tag('minecraft:mineable/pickaxe') // 打标签:属于"可以用镐挖"的工具
.tag('minecraft:tools') // 打标签:属于工具
})
14.3 自定义剑¶
StartupEvents.registry('item', event => { // 启动时注册物品
event.create('thunder_blade', 'sword') // 创建雷霆之刃(第二个参数 'sword' 指定类型是剑)
.displayName('雷霆之刃')
.tier(tier => { // 配置剑的等级属性
tier.durability(1000) // 耐久 1000
tier.speed(2) // 挖掘速度 2(剑本来就不擅长挖东西)
tier.attackDamageBonus(7) // 攻击伤害 +7
tier.level(3) // 等级 3(钻石级)
})
.tooltip('§e挥舞时释放闪电!') // 物品说明文字
})
// 剑的效果:攻击时放电(server_scripts)
EntityEvents.hurt(event => { // 监听"实体受伤"事件
let source = event.source // 拿到伤害来源
if (source.player && source.player.mainHandItem.id === 'kubejs:thunder_blade') { // 判断:是玩家打的,而且玩家手上拿的是雷霆之刃
let pos = event.entity.blockPosition() // 拿到被攻击实体的位置(blockPosition 返回坐标对象)
source.player.server.runCommandSilent( // 执行命令:summon 是召唤命令
`summon minecraft:lightning_bolt ${pos.x} ${pos.y} ${pos.z}` // 在被攻击实体的位置召唤一道闪电
)
}
})
14.4 自定义盔甲¶
StartupEvents.registry('item', event => { // 启动时注册物品
// 先注册盔甲等级
ItemEvents.armorTierRegistry(event => { // 监听"注册盔甲等级"事件(盔甲要先有等级才能做装备)
event.register('ruby', tier => { // 注册一个叫 ruby 的盔甲等级
tier.durabilityMultiplier(33) // 耐久倍率(33 倍基础耐久,数值越大越耐穿)
tier.reduction(3, 6, 8, 3) // 护甲值:靴/腿/胸/头(顺序是 靴子、护腿、胸甲、头盔的减伤值)
tier.enchantmentValue(15) // 附魔能力
tier.toughness(2) // 盔甲韧性(减少高伤害武器的额外伤害)
})
})
// 注册四件套
event.create('ruby_helmet', 'helmet').displayName('红宝石头盔').tier('ruby') // 头盔,使用 ruby 等级
event.create('ruby_chestplate', 'chestplate').displayName('红宝石胸甲').tier('ruby') // 胸甲
event.create('ruby_leggings', 'leggings').displayName('红宝石护腿').tier('ruby') // 护腿
event.create('ruby_boots', 'boots').displayName('红宝石靴子').tier('ruby') // 靴子
})
15. 自定义方块进阶(更多类型)¶
KubeJS 可以注册多种类型的方块,不只是普通方块:
// startup_scripts/blocks.js
StartupEvents.registry('block', event => { // 启动时注册方块
// 按钮
event.create('ruby_button', 'button') // 创建红宝石按钮(第二个参数 'button' 指定方块类型是按钮)
.displayName('红宝石按钮')
.material('metal') // 金属材质
// 压力板
event.create('ruby_pressure_plate', 'pressure_plate') // 创建压力板(踩上去触发)
.displayName('红宝石压力板')
.material('metal')
// 楼梯
event.create('ruby_stairs', 'stairs') // 创建楼梯
.displayName('红宝石楼梯')
.material('metal')
// 栅栏
event.create('ruby_fence', 'fence') // 创建栅栏
.displayName('红宝石栅栏')
.material('metal')
// 楼梯台阶
event.create('ruby_slab', 'slab') // 创建台阶
.displayName('红宝石台阶')
.material('metal')
// 墙壁
event.create('ruby_wall', 'wall') // 创建墙
.displayName('红宝石墙')
.material('metal')
// 门
event.create('ruby_door', 'cardinal') // 创建门(门要用 cardinal 类型,支持四个朝向)
.displayName('红宝石门')
.material('metal')
.rightClickOpen() // 右键开关(右键开门关门)
})
💡 方块类型:
'button'、'pressure_plate'、'stairs'、'slab'、'fence'、'wall'、'door'(cardinal)、'trapdoor'、'ladder'、'torch'等。
16. 配方高级操作¶
16.1 带 NBT 的合成(精确匹配)¶
ServerEvents.recipes(event => { // 监听"配方"事件,用来增删改合成配方
// 移除指定 NBT 的物品(比如附魔书)
event.remove({ output: { item: 'minecraft:enchanted_book' } }) // 删除所有"合成结果是附魔书"的配方(output 指合成产物)
// 带 NBT 输出:合成出带附魔的剑
event.shaped('minecraft:diamond_sword', [' A ', ' A ', ' B '], { // 合成表:3×3 格子按字母排列,A 和 B 对应下面的材料
A: 'minecraft:diamond', // A = 钻石
B: 'minecraft:stick' // B = 木棍(这样摆出来合成一把钻石剑)
}).id('kubejs:enchanted_sword') // 给这个配方起个 ID,方便以后精准删除或覆盖
})
16.2 条件配方(只在某些情况下生效)¶
ServerEvents.recipes(event => { // 监听"配方"事件
// 给配方加 ID,方便之后精准移除
event.shaped('minecraft:golden_apple', ['AAA', 'ABA', 'AAA'], { // 合成表:周围一圈 A,中间是 B
A: 'minecraft:gold_ingot', // A = 金锭
B: 'minecraft:apple' // B = 苹果(结果:金苹果)
}).id('kubejs:special_gapple') // 配方 ID 命名为 special_gapple
// 后续按 ID 移除
event.remove({ id: 'kubejs:special_gapple' }) // 用 ID 精准删除刚才那个配方(示例:先建后删)
})
16.3 原料替代(用标签批量)¶
ServerEvents.recipes(event => { // 监听"配方"事件
// #forge:stone 代表所有石头类
event.shaped('minecraft:stone_bricks', ['SS', 'SS'], { // 合成表:2×2 全是 S
S: '#forge:stone' // S = 任意石头(# 开头是标签,一种标签代表一类物品)
})
// 任意木头都能合成
event.shapeless('minecraft:stick', ['#minecraft:planks', '#minecraft:planks']) // 无序合成:任意两个木板合成木棍(shapeless 不要求摆放位置)
})
16.4 熔炉配方与更多¶
ServerEvents.recipes(event => { // 监听"配方"事件
// 烧炼配方:红宝石矿石 → 红宝石(经验 1,时长 200 tick)
event.smelting('kubejs:ruby', 'kubejs:ruby_ore').xp(1.0).cookingTime(200) // 熔炉烧红宝石矿石得到红宝石;xp 是给的经验,cookingTime 200 tick = 10 秒
// 切石配方
event.stonecutting('minecraft:stone_slab', 'minecraft:stone') // 切石机:石头切成石台阶
// 锻造配方(下界合金升级样式)
event.smithing('minecraft:netherite_ingot', 'minecraft:diamond', 'minecraft:netherite_upgrade_smithing_template') // 锻造台:钻石 + 升级模板 = 下界合金锭
})
17. 标签(Tag)进阶用法¶
17.1 操作各种注册表标签¶
ServerEvents.tags(event => { // 监听"标签"事件,用来给物品/方块等打标签
// 物品标签
event.add('forge:ingots/ruby', 'kubejs:ruby') // 给红宝石打上"红宝石锭"标签(forge:ingots/ruby)
event.add('forge:ores/ruby', 'kubejs:ruby_ore') // 给红宝石矿石打上"红宝石矿"标签
// 方块标签:让红宝石矿石能被镐子挖
event.add('minecraft:mineable/pickaxe', 'kubejs:ruby_ore') // 标记:红宝石矿石可以用镐挖
event.add('minecraft:needs_iron_tool', 'kubejs:ruby_ore') // 标记:需要铁镐及以上才能挖
// 实体标签:僵尸和骷髅算敌对
event.add('minecraft:hostile', 'minecraft:zombie') // 把僵尸归入"敌对生物"标签
event.add('minecraft:hostile', 'minecraft:skeleton') // 把骷髅也归入敌对
// 流体标签
event.add('minecraft:water', 'minecraft:water') // 给水打上"水"的标签(示例:把水归入水标签)
})
17.2 批量添加¶
ServerEvents.tags('item', event => { // 监听"标签"事件,并指定只处理物品类型(item)的标签
// 把一串物品都加到同一个标签
let rubies = ['kubejs:ruby', 'kubejs:ruby_block', 'kubejs:ruby_ore'] // 定义一个数组,装 3 个红宝石相关物品的 ID
rubies.forEach(id => event.add('forge:gems/ruby', id)) // 遍历数组,把每个物品都加到"宝石-红宝石"标签里
})
17.3 用标签做配方原料¶
ServerEvents.recipes(event => { // 监听"配方"事件
// 任何 #forge:gems/ruby 都能参与合成
event.shaped('kubejs:ruby_block', ['RRR', 'RRR', 'RRR'], { // 合成表:3×3 全是 R
R: '#forge:gems/ruby' // R = 任意带"红宝石宝石"标签的物品(9 个红宝石合成一个红宝石块)
})
})
18. 实体玩法¶
18.1 召唤实体¶
// server_scripts/spawn.js
PlayerEvents.chat(event => { // 监听"玩家发消息"事件
if (event.message !== '!招宠物') return // 判断:输入的不是"!招宠物"就结束
let p = event.player // 拿到玩家(起个短变量名 p 方便写)
p.server.runCommandSilent(`summon minecraft:wolf ${p.x} ${p.y} ${p.z}`) // 在玩家当前位置召唤一只狼(summon 是召唤命令)
p.tell('召唤了一只狼!') // 提示玩家
})
18.2 实体死亡掉落(进阶)¶
ServerEvents.entityLootTables(event => { // 监听"实体战利品表"事件
// 苦力怕额外掉火药 + 随机红宝石
event.modifyEntity('minecraft:creeper', table => { // 修改苦力怕的掉落表
table.addPool(pool => {
pool.addItem('minecraft:gunpowder', 2, 3) // 2 个,权重 3(火药,权重高容易掉)
pool.addItem('kubejs:ruby', 1, 1) // 1 个,权重 1(红宝石,比火药难掉)
pool.randomChance(0.5) // 50% 概率整个池生效(这个池有 50% 几率触发,触发后按权重掉落)
})
})
})
18.3 修改实体属性(自定义怪物)¶
// 让所有僵尸速度更快、血更厚
EntityEvents.spawned(event => { // 监听"实体生成"事件
let entity = event.entity // 拿到刚生成的实体
if (entity.type === 'minecraft:zombie') { // 判断:是不是僵尸
entity.setCustomName('强力僵尸') // 给僵尸改名字(头顶显示"强力僵尸")
entity.setGlowing(true) // 让它发光(方便看到)
entity.server.runCommandSilent( // 执行命令修改属性:attribute 是属性命令,把移动速度基值设为 0.35(更快)
`attribute ${entity.uuid} minecraft:movement_speed base set 0.35` // uuid 是实体的唯一编号,用来指定是哪只僵尸
)
}
})
18.4 防止特定实体生成¶
EntityEvents.checkSpawn(event => { // 监听"实体即将生成"事件(生成前一刻)
// 禁止幻翼生成
if (event.entity.type === 'minecraft:phantom') { // 判断:要生成的实体是不是幻翼
event.cancel() // 取消生成,幻翼就刷不出来了
}
})
19. 粒子与音效¶
19.1 播放粒子¶
// server_scripts/fx.js
BlockEvents.rightClicked(event => { // 监听"右键点击方块"事件
if (event.block.id !== 'minecraft:beacon') return // 判断:点的不是信标就结束
let p = event.player // 拿到玩家
let pos = event.block.getPos() // 拿到信标方块的坐标
// 生成火焰粒子(数量 50,扩散 0.5)
event.server.runCommandSilent( // 执行 particle 命令(生成粒子特效)
`particle minecraft:flame ${pos.x + 0.5} ${pos.y + 1} ${pos.z + 0.5} 0.5 0.5 0.5 0.1 50` // 在信标上方 1 格生成火焰粒子;0.5 是扩散范围,0.1 是速度,50 是数量
)
// 生成爱心粒子
event.server.runCommandSilent(
`particle minecraft:heart ${pos.x + 0.5} ${pos.y + 1.5} ${pos.z + 0.5} 0 0 0 0 10` // 在更高一点的位置生成 10 个爱心粒子,0 表示不扩散
)
})
19.2 播放音效¶
PlayerEvents.loggedIn(event => { // 监听"玩家登录"事件
let p = event.player // 拿到玩家
// 播放音效(音效ID,音量,音调)
p.server.runCommandSilent(`playsound minecraft:block.note_block.harp master ${p.username} ~ ~ ~ 1 1`) // 给玩家播放竖琴音效(playsound 命令;master 是音量分类,~ 表示当前位置,最后两个 1 是音量和音调)
})
// 自定义物品使用时播放
ItemEvents.rightClicked(event => { // 监听"右键使用物品"事件
if (event.item.id !== 'kubejs:magic_apple') return // 判断:用的不是魔法苹果就结束
let p = event.player // 拿到玩家
p.server.runCommandSilent(`playsound minecraft:entity.player.levelup master ${p.username} ~ ~ ~ 1 1`) // 给玩家播放升级音效
})
20. 玩家操作大全¶
// server_scripts/player_ops.js
PlayerEvents.chat(event => { // 监听"玩家发消息"事件
let p = event.player // 拿到玩家
let msg = event.message // 拿到玩家输入的消息
if (msg === '!满血') p.setHealth(p.maxHealth) // 输入"!满血":把血量设置成最大血量(maxHealth 是最大生命值)
if (msg === '!吃饱') { // 输入"!吃饱"
p.setFoodLevel(20) // 饥饿值设为 20(满格)
p.setSaturation(20) // 饱和度设为 20(更耐饿)
}
if (msg === '!经验') { // 输入"!经验"
p.server.runCommandSilent(`experience add ${p.username} 100`) // 用命令给玩家加 100 经验
}
if (msg === '!回血道具') p.give('1x minecraft:golden_apple') // 输入"!回血道具":发一个金苹果
if (msg === '!清理背包') p.server.runCommandSilent(`clear ${p.username}`) // 输入"!清理背包":用 clear 命令清空该玩家背包
if (msg === '!飞') p.server.runCommandSilent(`effect give ${p.username} minecraft:levitation 10 1`) // 输入"!飞":给漂浮效果 10 秒(levitation 会让玩家飘起来)
})
💡 常用:
p.setHealth()、p.setFoodLevel()、p.give()、p.tell()、p.x/p.y/p.z、p.username、p.mainHandItem。
21. 完整案例 D:简易商店系统¶
用聊天命令 + persistentData 做玩家间交易:
// server_scripts/shop.js
const SHOP = { // 定义商店数据:一个对象,键是玩家输入的命令,值是商品信息
'!买钻石': { item: 'minecraft:diamond', count: 1, price: 10 }, // 花 10 货币买 1 个钻石
'!买红宝石': { item: 'kubejs:ruby', count: 1, price: 5 }, // 花 5 货币买 1 个红宝石
'!买金苹果': { item: 'minecraft:golden_apple', count: 1, price: 20 } // 花 20 货币买 1 个金苹果
}
PlayerEvents.chat(event => { // 监听"玩家发消息"事件
let p = event.player // 拿到玩家
let product = SHOP[event.message] // 用玩家输入的消息去商店表里查商品;查不到就是 undefined
if (!product) return // 判断:输入的不是商店命令,直接结束
let balance = p.persistentData.money || 0 // 从存档里拿余额(货币),没记录过就当 0
if (balance < product.price) { // 判断:余额不够付这个商品的价格
p.tell(`§c余额不足!需要 ${product.price},你有 ${balance}`) // 提示余额不足,显示价格和当前余额
return // 结束购买
}
p.persistentData.money = balance - product.price // 扣钱:余额减去价格,存回存档
p.give(`${product.count}x ${product.item}`) // 把商品发给玩家(拼成"数量x 物品ID"的格式)
p.tell(`§a购买成功!花费 ${product.price},余额 ${p.persistentData.money}`) // 提示购买成功
})
// 发钱命令(OP 用):/reload 后控制台或游戏内
ServerEvents.commandRegistry(event => { // 注册自定义命令
event.register('money', cmd => { // 注册 /money 命令
cmd.permissionLevel(2) // 需要 OP 权限
cmd.argument('player', 'player', arg => { // 第一个参数:玩家名字(类型 player)
arg.argument('amount', 'integer', amountArg => { // 第二个参数:金额(类型 integer 整数)
amountArg.executes(ctx => { // 两个参数都填好后执行
let target = ctx.arguments.player // 取出目标玩家
let amount = ctx.arguments.amount // 取出金额
target.persistentData.money = (target.persistentData.money || 0) + amount // 给目标玩家的余额加钱(没记录过当 0)
ctx.source.server.runCommandSilent(`say ${target.username} 获得了 ${amount} 货币`) // 全服广播谁获得了多少钱
return 1 // 命令执行成功
})
})
})
})
})
22. 完整案例 E:玩家传送点(/sethome /home)¶
// server_scripts/homes.js
PlayerEvents.chat(event => { // 监听"玩家发消息"事件
let p = event.player // 拿到玩家
let msg = event.message // 拿到消息
// 设置家
if (msg === '!sethome') { // 判断:输入的是"!sethome"
p.persistentData.home = { x: p.x, y: p.y, z: p.z, dim: p.level.dimension } // 把当前坐标和所在维度存进存档(对象:x/y/z 坐标 + 维度)
p.tell(`§a已设置家:(${p.x.toFixed(0)}, ${p.y.toFixed(0)}, ${p.z.toFixed(0)})`) // 提示设置成功;toFixed(0) 是取整显示坐标
}
// 回家
if (msg === '!home') { // 判断:输入的是"!home"
let home = p.persistentData.home // 从存档里取出家的坐标
if (!home) { // 判断:还没设置过家
p.tell('§c你还没设置家!用 !sethome')
return // 结束
}
p.server.runCommandSilent(`tp ${p.username} ${home.x} ${home.y} ${home.z}`) // 用 tp 命令把玩家传送到家的坐标
p.tell('§a已回家!')
}
})
23. 完整案例 F:每日任务系统¶
// server_scripts/daily_quests.js
const QUESTS = { // 定义任务表:键是任务名,值是任务的"完成判断"和"奖励"
'挖 10 个石头': {
check: (p, data) => (data.stoneMined || 0) >= 10, // 判断函数:挖石头数量(没记录当 0)大于等于 10 就算完成
reward: '5x minecraft:iron_ingot' // 完成奖励:5 个铁锭
},
'击杀 5 只僵尸': {
check: (p, data) => (data.zombieKills || 0) >= 5, // 判断函数:击杀僵尸数量大于等于 5 就算完成
reward: '3x minecraft:gold_ingot' // 完成奖励:3 个金锭
}
}
// 记录进度
BlockEvents.broken(event => { // 监听"方块被破坏"事件
if (event.block.id === 'minecraft:stone') { // 判断:挖的是石头
let data = event.player.persistentData // 拿到玩家存档
data.stoneMined = (data.stoneMined || 0) + 1 // 挖石头计数 +1
}
})
EntityEvents.death(event => { // 监听"实体死亡"事件
let p = event.source.player // 拿到凶手玩家
if (p && event.entity.type === 'minecraft:zombie') { // 判断:是玩家杀的,而且杀的是僵尸
let data = p.persistentData // 拿到存档
data.zombieKills = (data.zombieKills || 0) + 1 // 击杀僵尸计数 +1
}
})
// 查看/领取任务
PlayerEvents.chat(event => { // 监听"玩家发消息"事件
let p = event.player // 拿到玩家
if (event.message !== '!任务') return // 判断:输入的不是"!任务"就结束
let data = p.persistentData // 拿到存档
// 每天重置(简单版:记录游戏天数)
let today = Math.floor(p.server.overworld.dayTime / 24000) // 计算今天是第几天(总 tick ÷ 24000 再取整)
if (data.questDay !== today) { // 判断:记录的任务日期不是今天——说明是新的一天,要重置
data.questDay = today // 更新任务日期
data.stoneMined = 0 // 清零挖石头进度
data.zombieKills = 0 // 清零击杀进度
data.questClaimed = {} // 清空"已领取"记录
}
Object.keys(QUESTS).forEach(name => { // Object.keys 取出所有任务名,遍历每个任务
let q = QUESTS[name] // 拿到当前这个任务
let done = q.check(p, data) // 调用任务的判断函数,看完成没有
let claimed = data.questClaimed && data.questClaimed[name] // 看这个任务今天领过奖没有
p.tell(`${done ? '§a✓' : '§7○'} ${name} ${claimed ? '§e[已领取]' : ''}`) // 显示任务状态:✓ 已完成 / ○ 未完成;领过就标"[已领取]"
})
})
// 领取奖励:!领任务1 / !领任务2
PlayerEvents.chat(event => { // 再写一个消息监听,专门处理领奖
let p = event.player // 拿到玩家
let idx = ['!领任务1', '!领任务2'].indexOf(event.message) // 在数组里找玩家输入的命令:找到返回下标 0 或 1,找不到返回 -1
if (idx < 0) return // 判断:输入的不是领奖命令就结束
let name = Object.keys(QUESTS)[idx] // 用下标取出对应的任务名(第 1 个任务或第 2 个)
let q = QUESTS[name] // 拿到任务
let data = p.persistentData // 拿到存档
let today = Math.floor(p.server.overworld.dayTime / 24000) // 计算今天是第几天
if (data.questDay !== today) { // 判断:任务日期不是今天(说明任务还没刷新)
p.tell('§c任务还没刷新!')
return
}
if (!q.check(p, data)) { // 判断:任务还没完成
p.tell('§c任务还没完成!')
return
}
if (data.questClaimed && data.questClaimed[name]) { // 判断:今天已经领过这个任务的奖
p.tell('§c已经领过啦!')
return
}
data.questClaimed = data.questClaimed || {} // 确保"已领取记录"存在(没有就新建一个空对象)
data.questClaimed[name] = true // 标记这个任务今天领过了
p.give(q.reward) // 把奖励发给玩家
p.tell(`§a领取奖励:${q.reward}`) // 提示领取成功
})
结语¶
KubeJS 的进阶玩法 = 事件 × 数据 × 工具方法的组合:
- 事件决定「什么时候触发」(查事件列表篇)
- persistentData 存「跨重启的数据」
- server 工具方法(give/tell/runCommandSilent/scheduleInTicks)做「具体动作」
- 复杂功能(容器 GUI、自定义实体 AI)可以配合 BEJS / ProbeJS 等插件扩展
多抄多改多调试,遇到问题先 console.log 看数据,再查事件列表——你就能写出自己的玩法系统了!