屏幕
屏幕通常是 Minecraft 中所有图形用户界面(GUI)的基础:接收用户输入,在服务端验证,再将结果同步回客户端。屏幕可以与菜单结合,为类似物品栏的视图创建通信网络;也可以独立使用,由模组开发者通过自己的网络实现处理。
原创说明图,依据 Forge 1.21.x 官方文档 绘制。
屏幕由许多部分组成,不易一下理解 Minecraft 中的“屏幕”是什么。本文先介绍各组件及其应用,再讨论屏幕本身。
相对坐标
渲染内容需要指定其出现位置。经过多层抽象后,Minecraft 大多数渲染调用接受坐标平面中的 x、y、z 值:x 从左向右增加,y 从上向下增加,z 从远到近增加。坐标范围不是固定的,会随屏幕大小和选项中的缩放比例改变。因此必须特别注意,使渲染坐标随可变的屏幕大小正确缩放。相对坐标的处理参见下文屏幕子类部分。
重要:固定坐标或错误缩放可能使渲染对象看起来异常或错位。检查坐标是否正确相对化的一种简单方法是点击视频设置中的“GUI 缩放”按钮。确定 GUI 渲染比例时,该值用作显示宽度和高度的除数。
GuiGraphics
Minecraft 的 GUI 通常使用 GuiGraphics 渲染。它是几乎所有渲染方法的第一个参数,包含渲染常用对象的基础方法,分为彩色矩形、字符串、纹理、物品和提示框五类。它还提供渲染组件局部区域的方法(#enableScissor/#disableScissor),并公开 PoseStack,用于应用使组件在正确位置渲染所需的变换。颜色使用 ARGB 格式。
彩色矩形
彩色矩形使用位置颜色着色器绘制,有三种形式。
首先是宽度为一像素的水平线和垂直线,分别使用 #hLine 和 #vLine。#hLine 接受定义左右边界的两个 x 坐标(包含边界)、顶部 y 坐标和颜色;#vLine 接受左侧 x 坐标、定义上下边界的两个 y 坐标(包含边界)和颜色。
其次是向屏幕绘制矩形的 #fill,线条方法内部也调用它。它接受左侧 x、顶部 y、右侧 x、底部 y 和颜色。
最后是绘制垂直渐变矩形的 #fillGradient,接受右侧 x、底部 y、左侧 x、顶部 y、z 坐标及底部和顶部颜色。
字符串
字符串通过 Font 绘制,通常具有普通、透视和偏移模式各自的着色器。可渲染两种对齐方式,均带阴影:左对齐 #drawString 和居中 #drawCenteredString。两者都接受字体、待绘制字符串、分别表示字符串左侧或中心的 x 坐标、顶部 y 坐标和颜色。
注意:通常应将字符串作为 Component 传入,因为它能处理多种情况,包括该方法的另外两个重载。
纹理
纹理通过 blitting 绘制,因此方法名为 #blit:它复制图像的位并直接画到屏幕上,使用位置纹理着色器。虽然有许多重载,这里只讨论两个静态 #blit。
第一个接受六个整数,假定渲染纹理位于 256 × 256 PNG 文件中。参数为屏幕左侧 x 和顶部 y、PNG 内左侧 x 和顶部 y,以及待渲染图像的宽高。
注意:必须指定 PNG 文件大小,以便将坐标归一化,得到对应的 UV 值。
第一个方法调用的静态 #blit 扩展为九个整数,只假定图像位于 PNG 中。它接受屏幕左侧 x 和顶部 y、z 坐标(blit 偏移)、PNG 内左侧 x 和顶部 y、待渲染图像宽高以及 PNG 文件宽高。
Blit 偏移
渲染纹理时,z 坐标通常设为 blit 偏移,用于在屏幕上正确分层。较小 z 坐标的内容在背景,较大 z 坐标的内容在前景。可通过 PoseStack#translate 直接设置 z 偏移;GuiGraphics 某些方法(如物品渲染)内部会应用基本偏移逻辑。
重要:设置偏移后必须在渲染对象后将其重置,否则屏幕上的其他对象可能出现在错误层级。建议平移前 push 当前 pose,并在该偏移下的全部渲染完成后 pop。
Renderable
Renderable 本质上是被渲染的对象,包括屏幕、按钮、聊天框、列表等。它只有一个方法 #render,接受用于屏幕渲染的 GuiGraphics、按相对屏幕大小缩放的鼠标 x/y 位置,以及 tick delta(自上一帧经过的 tick 数)。
常见的可渲染对象包括屏幕和控件(widget):通常显示在屏幕上的可交互元素,如 Button、其子类 ImageButton,以及用于输入文本的 EditBox。
GuiEventListener
Minecraft 中渲染的屏幕都实现 GuiEventListener,负责处理与屏幕的用户交互,包括鼠标移动、按下、释放、拖动、滚动、悬停以及键盘按下、释放和输入。每个方法返回对应操作是否成功影响了屏幕。按钮、聊天框和列表等控件也实现此接口。
ContainerEventHandler
其子类型 ContainerEventHandler 负责处理包含控件的屏幕上的用户交互,管理当前焦点及交互方式。它增加三项功能:可交互子元素、拖动和焦点。
事件处理器持有子元素,用来决定元素的交互顺序。鼠标事件处理期间(拖动除外),列表中第一个被鼠标悬停的子元素会执行其逻辑。通过 #mouseClicked 和 #mouseReleased 实现的鼠标拖动可更精确地执行逻辑。
焦点允许在事件执行时优先检查和处理特定子元素,例如键盘事件或鼠标拖动。通常通过 #setFocused 设置;也可以使用 #nextFocusPath,根据传入的 FocusNavigationEvent 循环选择可交互子元素。
注意:屏幕通过
AbstractContainerEventHandler实现ContainerEventHandler,后者增加了拖动和子元素焦点的 getter 与 setter 逻辑。
NarratableEntry
NarratableEntry 是可由 Minecraft 无障碍旁白功能朗读的元素。每个元素可根据悬停或选中状态提供不同旁白,通常按焦点、悬停、其他情况的顺序确定优先级。
它有三个方法:#narrationPriority 确定元素优先级,#isActive 决定是否朗读,#updateNarration 将旁白提供给对应输出,用于朗读或阅读。
注意:所有 Minecraft 控件都是
NarratableEntry,使用现有子类型时通常无需手动实现。
Screen 子类
有了上述知识,就可以构造基本屏幕。为便于理解,下面按通常遇到的顺序介绍屏幕组件。
首先,所有屏幕都接收一个 Component,表示屏幕标题。通常由某个子类将其绘制到屏幕;基础屏幕只将其用于旁白消息。
// 在某个 Screen 子类中
public MyScreen(Component title) { super(title); }
初始化
屏幕初始化时调用 #init,设置 ItemRenderer、Minecraft 实例以及按游戏缩放的相对宽高。添加控件、预计算相对坐标等设置应在此方法中完成。游戏窗口调整大小时,会再次调用 #init 重新初始化屏幕。
添加控件有三种方式:
| 方法 | 用途 |
|---|---|
#addWidget |
添加可交互、可朗读但不渲染的控件。 |
#addRenderableOnly |
添加只渲染、不可交互也不朗读的控件。 |
#addRenderableWidget |
添加可交互、可朗读且渲染的控件。 |
通常最常用的是 #addRenderableWidget。
@Override
protected void init() {
super.init();
// 添加控件和预计算值
this.addRenderableWidget(new EditBox(/* ... */));
}
屏幕 tick
屏幕也通过 #tick 执行一些用于渲染的客户端逻辑。最常见的例子是 EditBox 的闪烁光标。
@Override
public void tick() {
super.tick();
this.editBox.tick();
}
输入处理
由于屏幕是 GuiEventListener 子类型,也可以重写输入处理器,例如处理特定按键。
渲染屏幕
屏幕通过 Renderable 提供的 #render 方法渲染。该方法每帧绘制屏幕需要显示的内容,如背景、控件和提示框;默认只渲染控件。通常需要自行处理的两项是背景和提示框。
背景可通过 #renderBackground 绘制;其中一个方法接收选项背景的 v 偏移,供屏幕后方的维度无法渲染时使用。提示框使用 GuiGraphics#renderTooltip 或 GuiGraphics#renderComponentTooltip 绘制,可传入文本组件、可选的自定义提示框组件,以及屏幕上的相对 x/y 坐标。
// mouseX 和 mouseY 是鼠标在屏幕上的缩放坐标
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
this.renderBackground(graphics); // 背景通常先渲染
// 在控件之前渲染背景纹理等
super.render(graphics, mouseX, mouseY, partialTick);
// 在控件之后渲染提示框等
}
关闭屏幕
关闭屏幕时,#onClose 和 #removed 负责清理。用户输入关闭屏幕时调用 #onClose,通常用作结束并保存屏幕内部流程的回调,包括向服务端发送数据包。#removed 在屏幕切换并交给垃圾回收器前调用,处理所有尚未恢复到打开屏幕前初始状态的内容。
@Override
public void onClose() {
// 在此停止处理器;最后调用 super,以免干扰重写逻辑
super.onClose();
}
@Override
public void removed() {
// 在此重置初始状态;最后调用 super
super.removed();
}
AbstractContainerScreen
如果屏幕直接附加到菜单,应继承 AbstractContainerScreen。它充当菜单的渲染器和输入处理器,包含同步和操作槽位的逻辑。通常只需重写或实现两个方法即可得到可用的容器屏幕。下面按通常遇到的顺序介绍其组件。
它通常需要三个参数:正在打开的容器菜单(泛型 T)、玩家物品栏(仅用于显示名称)和屏幕标题。可以设置以下定位字段:
| 字段 | 说明 |
|---|---|
imageWidth |
背景纹理宽度,通常位于 256 × 256 PNG 中,默认 176。 |
imageHeight |
背景纹理高度,通常位于 256 × 256 PNG 中,默认 166。 |
titleLabelX / titleLabelY |
屏幕标题渲染位置的相对 x/y 坐标。 |
inventoryLabelX / inventoryLabelY |
玩家物品栏名称渲染位置的相对 x/y 坐标。 |
重要:此前建议在
#init中设置预计算的相对坐标,仍然适用。这里列出的值不是预计算坐标,而是静态值和相对坐标。
图像值是静态不变的背景纹理尺寸。为方便渲染,#init 会预计算 leftPos 和 topPos,标记背景渲染位置的左上角;标签坐标相对于它们。它们也可直接作为 #blit 的背景位置。
public MyContainerScreen(MyMenu menu, Inventory playerInventory, Component title) {
super(menu, playerInventory, title);
this.titleLabelX = 10;
this.inventoryLabelX = 10;
// 修改 imageHeight 时也必须修改依赖它的 inventoryLabelY。
}
访问菜单
菜单传入屏幕后,可通过 menu 字段访问菜单中经槽位、数据槽位或自定义系统同步的值。
容器 tick
玩家存活且正在查看屏幕时,容器屏幕在 #tick 中通过 #containerTick 执行 tick。它基本取代容器屏幕中的 #tick,最常见的用途是更新配方书。
@Override
protected void containerTick() {
super.containerTick();
// 在此更新其他内容
}
渲染容器屏幕
容器屏幕由三个方法渲染:#renderBg 绘制背景纹理,#renderLabels 绘制背景上的文本,#render 包含前两者,同时提供灰色背景和提示框。
最常见的 #render 重写会先绘制背景,调用父类渲染容器屏幕,最后在顶部绘制提示框:
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
this.renderBackground(graphics);
super.render(graphics, mouseX, mouseY, partialTick);
this.renderTooltip(graphics, mouseX, mouseY); // 绘制悬停槽位的提示框
}
父类中调用 #renderBg 绘制屏幕背景。原文指出常见实现由两次设置调用和一次背景纹理绘制组成,示例如下:
// 背景纹理位置:assets/<namespace>/<path>
private static final ResourceLocation BACKGROUND_LOCATION = new ResourceLocation(MOD_ID, "textures/gui/container/my_container_screen.png");
@Override
protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) {
// leftPos 和 topPos 由图像宽高预计算;两个零是 256 × 256 PNG 内的 u/v 坐标。
graphics.blit(BACKGROUND_LOCATION, this.leftPos, this.topPos, 0, 0, this.imageWidth, this.imageHeight);
}
最后 #renderLabels 绘制背景上方、提示框下方的文本,使用字体绘制对应组件:
@Override
protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) {
super.renderLabels(graphics, mouseX, mouseY);
// 假设有 Component label,在 labelX、labelY 绘制
graphics.drawString(this.font, this.label, this.labelX, this.labelY, 0x404040);
}
注意:渲染标签时无需指定
leftPos和topPos偏移;PoseStack已完成平移,方法中的内容相对于这些坐标绘制。
注册 AbstractContainerScreen
要与菜单一起使用 AbstractContainerScreen,必须注册它。可在模组事件总线的 FMLClientSetupEvent 中调用 MenuScreens#register。
// 在模组事件总线上监听此事件
private void clientSetup(FMLClientSetupEvent event) {
event.enqueueWork(
// 假设存在 RegistryObject<MenuType<MyMenu>> MY_MENU 和接收三个参数的 MyContainerScreen<MyMenu>
() -> MenuScreens.register(MY_MENU.get(), MyContainerScreen::new)
);
}
警告:
MenuScreens#register不是线程安全的,因此必须在并行分发事件提供的#enqueueWork中调用。
原文:Screens。