跳转至

原创教程(ぴよまる)· 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 或写模组。

目录

  1. 原理:为什么脚本能继承 Screen
  2. 准备工作
  3. 最小骨架:打开一个自定义界面
  4. 绘制层详解:GuiGraphics 全 API
  5. 交互层详解:事件方法签名表
  6. 完整事例:设置面板(按钮+滑块+输入框+物品图标)
  7. 常见坑
  8. 方案取舍

1. 原理:为什么脚本能继承 Screen

KubeJS 的脚本引擎是 Rhino(Mozilla 的 JS 引擎),它自带 Java.extend()——可以在 JS 里创建 Java 类的匿名子类:

const MyScreen = Java.extend(SomeJavaClass, {
    methodName: function(...) { ... }
})

net.minecraft.client.gui.screens.Screen 是原版所有界面(箱子、物品栏、设置页)的基类。我们用 Java.extend(Screen, {...}) 造一个自己的 Screen 子类,覆盖它的绘制方法(render)和交互方法(mouseClickedkeyPressed…),然后用 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;
  • 脚本在客户端主线程运行(KeyEventsClientEvents 等客户端事件里打开);
  • 只适用于 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)

🎨 颜色格式是 ARGB0xAARRGGBB0xFF409EFF = 不透明蓝色;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 表示「不处理,传给下层」。交互方法记得返回 truefalse,不要留空。

常用 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