原创教程(ぴよまる)· KubeJS 6.x / Minecraft 1.20.1 Forge · 方案验证于 2026-08-23
自定义 Screen:Java.extend 自由绘制 UI¶
纯 KubeJS 脚本(不写 Java 文件)实现「像素级自由绘制 + 全交互」的终极方案:在 client_scripts 里用 Rhino 的 Java.extend() 直接继承原版 Screen 类,自己控制每一帧画什么、每一个鼠标键盘事件做什么。
⚠️ 定位:这是高手向方案。官方纯脚本方案里:Painter API(25 篇)能自由画但不能点,Chest GUI(27 篇)能点但是格子式。本方案两者都要——代价是代码量、坐标系全手算、强绑 1.20.1。小界面用着爽,大系统建议 Chest GUI 或写模组。
目录¶
- 原理:为什么脚本能继承 Screen
- 准备工作
- 最小骨架:打开一个自定义界面
- 绘制层详解:GuiGraphics 全 API
- 交互层详解:事件方法签名表
- 完整事例:设置面板(按钮+滑块+输入框+物品图标)
- 常见坑
- 方案取舍
1. 原理:为什么脚本能继承 Screen¶
KubeJS 的脚本引擎是 Rhino(Mozilla 的 JS 引擎),它自带 Java.extend()——可以在 JS 里创建 Java 类的匿名子类:
net.minecraft.client.gui.screens.Screen 是原版所有界面(箱子、物品栏、设置页)的基类。我们用 Java.extend(Screen, {...}) 造一个自己的 Screen 子类,覆盖它的绘制方法(render)和交互方法(mouseClicked、keyPressed…),然后用 Minecraft.getInstance().setScreen(...) 打开它。
整个过程不产生任何 .java 文件,全在 client_scripts 的 JS 里完成。
为什么这能行:KubeJS 的 class filter 默认放行 net.minecraft 包(脚本里常见的 Java.loadClass 都能用),Rhino 的 Java.extend 是标准能力。所以这不是漏洞,是 KubeJS 设计上就允许的「原生 Java 类型访问」(官方文档明确提到 Java.loadClass("package.class") 可用)。
2. 准备工作¶
- 必须是 client_scripts(客户端脚本),服务端没有 Screen;
- 脚本在客户端主线程运行(
KeyEvents、ClientEvents等客户端事件里打开); - 只适用于 1.20.1(Screen 渲染 API 每个 MC 版本都在变,1.21 以上此教程全部失效);
- 需要引入的类:
const Screen = Java.loadClass('net.minecraft.client.gui.screens.Screen')
const Component = Java.loadClass('net.minecraft.network.chat.Component')
const Minecraft = Java.loadClass('net.minecraft.client.Minecraft')
const ResourceLocation = Java.loadClass('net.minecraft.resources.ResourceLocation')
3. 最小骨架:打开一个自定义界面¶
// client_scripts/my_screen.js
const Screen = Java.loadClass('net.minecraft.client.gui.screens.Screen')
const Component = Java.loadClass('net.minecraft.network.chat.Component')
const Minecraft = Java.loadClass('net.minecraft.client.Minecraft')
// 创建 Screen 子类(一次定义,可多次 new)
const MyScreen = Java.extend(Screen, {
render: function(guiGraphics, mouseX, mouseY, partialTick) {
// 每帧绘制:这里先画个全屏黑底
guiGraphics.fill(0, 0, this.width, this.height, 0xFF000000)
// 居中画一行字(this.font 是 Screen 自带的字体对象)
guiGraphics.drawCenteredString(this.font, '我的第一个自定义界面', this.width / 2, this.height / 2, 0xFFFFFF)
},
isPauseScreen: function() {
return false // 打开时不暂停游戏
}
})
// 按 P 键打开
KeyEvents.key(event => {
if (event.key === 'key.keyboard.p') {
// Screen 构造器接收一个 Component 作为标题
Minecraft.getInstance().setScreen(new MyScreen(Component.literal('my_screen')))
}
})
进游戏按 P,屏幕变黑、中间出现一行字——你已经拥有一个完全自己控制的界面了。
关键点:
this.width/this.height:屏幕尺寸(Screen 自带字段);this.font:字体对象(Screen 自带);this.minecraft:Minecraft 实例(Screen 自带);- 绘制必须写在
render里(每帧调用);构造后的初始化写在init里(可选覆盖)。
4. 绘制层详解:GuiGraphics 全 API¶
render(guiGraphics, mouseX, mouseY, partialTick) 的第一个参数 guiGraphics 是原版绘制工具类(GuiGraphics),1.20.1 可用方法:
4.1 形状与颜色¶
// 实心矩形 fill(x1, y1, x2, y2, 颜色ARGB) —— x2/y2 是"结束坐标+1"
guiGraphics.fill(100, 80, 220, 120, 0xFF409EFF)
// 垂直渐变 fillGradient(x1, y1, x2, y2, 顶部颜色, 底部颜色)
guiGraphics.fillGradient(0, 0, this.width, this.height, 0xC0102030, 0xC0304060)
// 水平线 / 垂直线(1.20.1 可用)
guiGraphics.hLine(100, 300, 150, 0xFFFFFFFF)
guiGraphics.vLine(200, 80, 200, 0xFFFFFFFF)
🎨 颜色格式是 ARGB:
0xAARRGGBB。0xFF409EFF= 不透明蓝色;0x88FFFFFF= 半透明白。忘写0xFF前缀会得到全透明(看不见)。
4.2 文字¶
// 左对齐文字 drawString(字体, 文本, x, y, 颜色)
guiGraphics.drawString(this.font, 'Hello', 100, 60, 0xFFFFFF)
// 居中文字 drawCenteredString(字体, 文本, 中心x, 中心y, 颜色)
guiGraphics.drawCenteredString(this.font, '标题', this.width / 2, 20, 0xFFFFFF)
// 也支持 Component 对象(可带颜色/点击事件)
guiGraphics.drawString(this.font, Component.literal('彩色').withColor(0xFF55FF55), 100, 80, 0xFFFFFF)
4.3 贴图(图片)¶
// 整图 blit(图片路径, x, y, 宽, 高)
guiGraphics.blit(ResourceLocation.of('minecraft:textures/gui/container/inventory.png', 'default'), 100, 100, 176, 166)
// 带 UV 的九宫格/局部贴图
guiGraphics.blit(tex, 100, 100, 0, 0, 20, 20, 256, 256) // (x, y, u, v, w, h, 整图宽, 整图高)
4.4 物品图标¶
// 物品图标 renderItem(ItemStack, x, y) —— KubeJS 的 Item.of() 可以取 ItemStack
guiGraphics.renderItem(Item.of('minecraft:diamond').itemStack, 320, 80)
// 带耐久/数量角标
guiGraphics.renderItemDecorations(this.font, Item.of('minecraft:diamond', 5).itemStack, 320, 80)
4.5 变换(平移 / 缩放 / 旋转)¶
// 获取矩阵栈,pushPose 后变换,最后 popPose 还原
const pose = guiGraphics.pose()
pose.pushPose()
pose.translate(100, 100, 0) // 平移
pose.scale(2, 2, 1) // 放大 2 倍(之后画的内容坐标都按新坐标系)
// ... 画东西 ...
pose.popPose()
4.6 裁剪(滚动列表必备)¶
guiGraphics.enableScissor(100, 200, 300, 300) // 只允许在 (100,200)-(300,300) 内绘制
// ... 画列表内容(超出的部分被裁掉)...
guiGraphics.disableScissor()
5. 交互层详解:事件方法签名表¶
在 Java.extend(Screen, {...}) 里覆盖以下方法(方法名必须完全一致,参数个数要对):
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
init |
无 | - | 界面打开/窗口尺寸变化时初始化 |
tick |
无 | - | 每 tick(20次/秒)逻辑 |
render |
(guiGraphics, mouseX, mouseY, partialTick) |
- | 每帧绘制 |
mouseClicked |
(mouseX, mouseY, button) |
boolean | 鼠标按下;button: 0左 1右 2中 |
mouseReleased |
(mouseX, mouseY, button) |
boolean | 鼠标释放 |
mouseDragged |
(mouseX, mouseY, button, dragX, dragY) |
boolean | 按住拖动 |
mouseScrolled |
(mouseX, mouseY, delta) |
boolean | 滚轮;delta 正=上滚 |
keyPressed |
(keyCode, scanCode, modifiers) |
boolean | 键盘按下(keyCode 是 GLFW 码) |
keyReleased |
(keyCode, scanCode, modifiers) |
boolean | 键盘释放 |
charTyped |
(codePoint, modifiers) |
boolean | 文本字符输入(含中文/特殊符号) |
onClose |
无 | - | 关闭时(Esc 也会触发) |
isPauseScreen |
无 | boolean | true=打开时暂停游戏(默认 true) |
shouldCloseOnEsc |
无 | boolean | 按 Esc 是否关闭(默认 true) |
返回值规则(重要):返回 true 表示「事件我处理了」;返回 false 表示「不处理,传给下层」。交互方法记得返回 true 或 false,不要留空。
常用 GLFW 键码:256=Esc,259=Backspace,257=Enter,32=空格,65~90=A~Z,320~329=F1~F10。
keyPressed: function(keyCode, scanCode, modifiers) {
if (keyCode === 259) { // Backspace:删一个字
this.inputText = this.inputText.slice(0, -1)
return true
}
return false // 其他键不处理(比如让 Esc 正常关界面)
}
6. 完整事例:设置面板(按钮+滑块+输入框+物品图标)¶
一个能直接跑的完整界面:渐变背景、悬停高亮的按钮、可拖动的滑块、可输入的文本框、物品图标展示。
// client_scripts/setting_screen.js
const Screen = Java.loadClass('net.minecraft.client.gui.screens.Screen')
const Component = Java.loadClass('net.minecraft.network.chat.Component')
const Minecraft = Java.loadClass('net.minecraft.client.Minecraft')
const SettingScreen = Java.extend(Screen, {
// ---- 界面状态 ----
sliderValue: 50, // 滑块值 0~100
inputText: '', // 输入框内容
// ---- 绘制(每帧)----
render: function(g, mx, my, tick) {
// 背景:垂直渐变
g.fillGradient(0, 0, this.width, this.height, 0xC0102030, 0xC0304060)
// 标题
g.drawCenteredString(this.font, '设置面板', this.width / 2, 20, 0xFFFFFF)
// 按钮(鼠标悬停时变亮)
const hovering = (mx >= 100 && mx <= 220 && my >= 80 && my <= 120)
g.fill(100, 80, 220, 120, hovering ? 0xFF55AAFF : 0xFF409EFF)
g.drawCenteredString(this.font, '点我', 160, 96, 0xFFFFFF)
// 滑块轨道 + 滑块头
g.fill(100, 150, 300, 158, 0xFF888888)
const sx = 100 + (this.sliderValue / 100) * 200
g.fill(sx - 4, 144, sx + 4, 164, 0xFFFFD700)
g.drawString(this.font, '滑块: ' + this.sliderValue, 100, 170, 0xFFFFFF)
// 输入框
g.fill(100, 200, 300, 224, 0xFF222222)
g.drawString(this.font, '输入: ' + this.inputText + '_', 105, 207, 0xFFFFFF)
// 物品图标
g.renderItem(Item.of('minecraft:diamond').itemStack, 320, 80)
},
// ---- 鼠标点击 ----
mouseClicked: function(mx, my, button) {
if (button === 0 && mx >= 100 && mx <= 220 && my >= 80 && my <= 120) {
this.minecraft.player.displayClientMessage(Component.literal('点了按钮!滑块=' + this.sliderValue), false)
return true
}
return true
},
// ---- 拖动滑块 ----
mouseDragged: function(mx, my, button, dx, dy) {
if (button === 0 && mx >= 100 && mx <= 300 && my >= 140 && my <= 170) {
this.sliderValue = Math.max(0, Math.min(100, Math.round((mx - 100) / 2)))
return true
}
return false
},
// ---- 键盘输入 ----
charTyped: function(codePoint, modifiers) {
this.inputText += String.fromCharCode(codePoint)
return true
},
keyPressed: function(keyCode, scanCode, modifiers) {
if (keyCode === 259) { // Backspace
this.inputText = this.inputText.slice(0, -1)
return true
}
return false
},
// ---- 其他 ----
tick: function() {
// 每 tick 逻辑(比如倒计时)
},
isPauseScreen: function() {
return false
}
})
// 按 P 打开
KeyEvents.key(event => {
if (event.key === 'key.keyboard.p') {
Minecraft.getInstance().setScreen(new SettingScreen(Component.literal('settings')))
}
})
效果:按 P 打开 → 渐变背景 + 标题;鼠标移到按钮上变亮,点击发聊天消息;拖动金色滑块数值实时变化;点击输入框区域后打字直接进文本框(Backspace 删除);界面不暂停游戏。
7. 常见坑¶
Q:报 ClassNotFound / 类被拒绝访问?
A:KubeJS class filter 默认放行 net.minecraft,一般不会拦。若报 denied,检查是否有 mod 提供了更严格的 kubejs.classfilter.txt;也可以把脚本路径改成先 console.log(Java.loadClass(...)) 定位。
Q:鼠标点击没反应?
A:mouseClicked 的参数是 double(不是 int)。JS 里直接范围比较没问题,但别用 === 和整数精确比较;另外确认方法返回了 true。
Q:颜色画出来是透明的?
A:颜色是 ARGB,必须带 0xFF 前缀(0xFF409EFF)。写 0x409EFF 会被当成 0x00409EFF(完全透明)。
Q:文字模糊/位置不对?
A:drawCenteredString 的 x 是「文字中心」;drawString 的 x 是「文字左边」。混用会错位。
Q:能用在服务器上吗?
A:不能。Screen 是客户端类,必须 client_scripts,服务端脚本直接报 ClassNotFound。
Q:render 里做重活会卡? A:render 每帧调用(帧率越高调用越频繁)。不要在 render 里 new 对象、查数据库、写文件;状态变化放事件/tick 里,render 只画。
Q:KubeJS 升级/换 MC 版本会坏吗? A:会。Screen 和 GuiGraphics 的 API 每个 MC 版本都在变(1.21 已大改),本教程只保证 1.20.1 + KubeJS 6.x。
8. 方案取舍¶
| 方案 | 自由绘制 | 可交互 | 不写 Java | 稳定性 | 适合 |
|---|---|---|---|---|---|
| Java.extend 自定义 Screen(本篇) | ✅ 像素级 | ✅ 全事件 | ✅ | ⚠️ 强绑 1.20.1 | 小界面、自定义面板、HUD 风格菜单 |
| Chest GUI(27 篇) | ⚠️ 格子式 | ✅ | ✅ | ✅ 服务端驱动 | 商店、菜单、任务(多玩家场景) |
| Painter API(25 篇) | ✅ | ❌ | ✅ | ✅ | HUD 提示、屏幕特效 |
| Modern UI(33 篇) | ✅ | ✅ | ❌ 要 Java | ✅ | 大型专业 UI |
| ScreenJS(26 篇) | ⚠️ | ✅ | ✅ | ❌ 仅 1.19.2 | 别在 1.20.1 用 |
选型建议:
- 给单个玩家看的小面板(设置、信息、快捷操作)→ 本篇方案;
- 全服玩家都要用、要跨存档稳定 → Chest GUI(服务端权威,防作弊友好);
- 只要装饰性 HUD → Painter;
- 要做复杂大型界面 → 老实写 Java 模组(Modern UI 或原版 Screen)。
小结¶
Java.extend(Screen, {...}) 是 KubeJS 脚本能力的隐藏大招:不写 Java 文件,也能拥有像素级自由绘制 + 完整交互的自定义界面。核心就三步——Java.extend 继承 Screen、render 里用 GuiGraphics 画、事件方法里写交互,最后 setScreen() 打开。
相关资源:KubeJS 官方 Wiki(Java 类型访问)https://kubejs.com · Screen 源码(1.20.1)https://github.com/MinecraftForge/MinecraftForge/blob/1.20.x/patches/minecraft/net/minecraft/client/gui/screens/Screen.java.patch · GuiGraphics 源码 https://github.com/MinecraftForge/MinecraftForge/blob/1.20.x/patches/minecraft/net/minecraft/client/gui/GuiGraphics.java.patch