界面
界面(Screen)通常是 Minecraft 中所有图形用户界面(GUI)的基础:接收用户输入、在服务器验证输入,再把产生的操作同步回客户端。它可以与菜单组合,为类似物品栏的视图建立通信机制;也可以独立使用,由 Mod 开发者通过自己的网络实现处理。
界面由许多部分组成,因此很难一下弄清 Minecraft 中的“界面”到底是什么。本篇会先介绍各组成部分及其用法,再介绍界面本身。
相对坐标
渲染任何内容时,都需要指定它出现的位置。经过多层抽象,Minecraft 的多数渲染调用都接收坐标平面中的 x、y、z 值:x 从左向右增大,y 从上向下增大,z 从远向近增大。不过,坐标并没有固定范围,它们会随窗口尺寸和游戏选项中的“GUI 缩放”变化。因此,传入渲染调用的坐标必须正确缩放,也就是相对于可变的屏幕尺寸进行正确计算。
如何计算相对坐标,请参阅界面子类一节。
注意 使用固定坐标或错误缩放界面,可能使渲染对象显示异常或错位。检查相对坐标是否正确的简单方法是单击视频设置中的“GUI 缩放”按钮。确定 GUI 渲染比例时,会用此值除以显示区域的宽高。
GuiGraphics
Minecraft 渲染 GUI 通常使用 GuiGraphics。它几乎是所有渲染方法的第一个参数,包含渲染常见对象的基本方法,可分为五类:彩色矩形、字符串、纹理、物品和提示框。此外,还有用于只渲染组件局部区域的方法(#enableScissor / #disableScissor)。GuiGraphics 也提供 PoseStack,用于施加必要变换,让组件渲染在正确位置。颜色采用 ARGB 格式。
彩色矩形
彩色矩形通过位置颜色着色器绘制。所有填充方法都可接收可选的 RenderType,指定矩形的渲染方式。可以绘制三类彩色矩形。
第一类是一像素宽的彩色水平线和竖直线,分别使用 #hLine 和 #vLine。#hLine 接收左右两个 x 坐标(包含端点)、顶部 y 坐标和颜色。#vLine 接收左侧 x 坐标、上下两个 y 坐标(包含端点)和颜色。
第二类使用 #fill 在屏幕上绘制矩形。线条方法内部也调用此方法。它接收左侧 x、顶部 y、右侧 x、底部 y 坐标和颜色。#fillRenderType 的用途相同,但绘制顶点前不会修正坐标位置。
最后,#fillGradient 绘制带竖直渐变的矩形。它接收右侧 x、底部 y、左侧 x、顶部 y、z 坐标,以及底部和顶部颜色。
字符串
字符串通过对应的 Font 绘制,通常为普通、透视和偏移模式使用各自的着色器。有两种带背景阴影的对齐方式:左对齐的 #drawString 和居中的 #drawCenteredString。两者都接收字体、待绘制字符串、表示左侧或中心位置的 x 坐标、顶部 y 坐标和颜色。
如果文本需要在给定边界内换行,可以改用 #drawWordWrap,默认以左对齐方式绘制。
提示 通常应使用
Component传递字符串,因为它支持多种使用场景,包括该方法的另外两种重载。
纹理
纹理通过位块传输(blitting)绘制,因此方法名为 #blit:它复制图像中的位并直接绘制到屏幕上,使用的是位置纹理着色器。
每个 #blit 方法都接收 ResourceLocation,表示纹理的完整位置:
// Points to 'assets/examplemod/textures/gui/container/example_container.png'
private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath("examplemod", "textures/gui/container/example_container.png");
#blit 有许多重载,这里只介绍两种。
第一种接收六个整数,假定纹理位于一张 256 × 256 PNG 文件中。参数依次表示屏幕左侧 x 和顶部 y 坐标、PNG 内左侧 x 和顶部 y 坐标,以及要渲染图像的宽高。
建议 必须指定 PNG 文件尺寸,才能归一化坐标,得到对应的 UV 值。
第一种重载内部调用第二种重载。后者扩展为七个整数和两个用于 PNG 坐标的浮点数,只假定图像位于 PNG 文件中。它接收屏幕左侧 x、顶部 y 坐标、z 坐标(称为 blit 偏移)、PNG 内左侧 x 和顶部 y 坐标、要渲染图像的宽高,以及 PNG 文件的宽高。
blitSprite
#blitSprite 是 #blit 的特殊实现,其纹理写入 GUI 纹理图集。覆盖在背景上的多数纹理(例如熔炉 GUI 的燃烧进度覆盖层)都是精灵。所有精灵纹理均相对于 textures/gui/sprites,无需指定文件扩展名。
// Points to 'assets/examplemod/textures/gui/sprites/container/example_container/example_sprite.png'
private static final ResourceLocation SPRITE = ResourceLocation.fromNamespaceAndPath("examplemod", "container/example_container/example_sprite");
一组 #blitSprite 方法与 #blit 具有相同参数,但不需要精灵内部的 x、y 坐标。
另一组方法接收更多纹理信息,允许只渲染精灵的一部分。参数为纹理宽高、精灵内的 x、y 坐标、屏幕左侧 x 和顶部 y 坐标、z 坐标(blit 偏移),以及要渲染图像的宽高。
如果精灵尺寸与纹理尺寸不匹配,可以使用三种缩放方式:stretch、tile 和 nine_slice。stretch 将图像从纹理尺寸拉伸到屏幕尺寸;tile 重复绘制纹理,直至铺满屏幕尺寸;nine_slice 将纹理划分为一个中心、四条边和四个角,通过平铺适应所需屏幕尺寸。
在与纹理同名的 mcmeta 文件中添加 gui.scaling JSON 对象即可设置:
// For some texture file example_sprite.png
// In example_sprite.png.mcmeta
// Stretch example
{
"gui": {
"scaling": {
"type": "stretch"
}
}
}
// Tile example
{
"gui": {
"scaling": {
"type": "tile",
// The size to begin tiling at
// This is usually the size of the texture
"width": 40,
"height": 40
}
}
}
// Nine slice example
{
"gui": {
"scaling": {
"type": "nine_slice",
// The size to begin tiling at
// This is usually the size of the texture
"width": 40,
"height": 40,
"border": {
// The padding of the texture that will be sliced into the border texture
"left": 1,
"right": 1,
"top": 1,
"bottom": 1
}
}
}
}
Blit 偏移
渲染纹理时,z 坐标通常设为 blit 偏移。此偏移负责让界面中的渲染内容正确分层:z 较小的内容在背景,z 较大的内容在前景。可以通过 #translate 直接在 PoseStack 上设置 z 偏移。GuiGraphics 的某些方法内部也会应用基本偏移逻辑,例如物品渲染。
注意 设置 blit 偏移后,必须在对象渲染结束时将其重置。否则,界面中的其他对象可能渲染到错误图层,导致显示问题。建议平移前先压入当前姿态,在该偏移下的所有渲染结束后再弹出。
Renderable
Renderable 本质上就是可渲染对象,包括界面、按钮、聊天框、列表等。它只有 #render 一个方法,接收用于向屏幕绘制内容的 GuiGraphics、按相对屏幕尺寸缩放的鼠标 x 和 y 位置,以及 tick delta(自上一帧以来经过的刻数)。
常见的可渲染对象包括界面和“控件”:通常显示在屏幕上的可交互元素,例如 Button、它的子类 ImageButton,以及用于输入文本的 EditBox。
GuiEventListener
Minecraft 渲染的所有界面都实现 GuiEventListener。它负责处理用户与界面的交互,包括鼠标输入(移动、单击、松开、拖动、滚动、悬停)和键盘输入(按下、松开、键入)。每个方法返回相应操作是否成功作用于界面。按钮、聊天框、列表等控件也实现此接口。
ContainerEventHandler
GuiEventListener 的子接口 ContainerEventHandler 与其关系密切。它负责处理包含控件的界面交互,管理当前焦点及交互的应用方式。ContainerEventHandler 增加了三项功能:可交互子元素、拖动和焦点。
事件处理器持有子元素,用于决定元素的交互顺序。处理鼠标事件时(拖动除外),会执行列表中第一个被鼠标悬停的子元素的逻辑。
通过 #mouseClicked 和 #mouseReleased 实现的鼠标拖动,可以更精确地控制执行逻辑。
焦点让特定子元素在事件执行期间优先得到检查和处理,例如键盘事件或鼠标拖动。通常通过 #setFocused 设置焦点。此外,也可以使用 #nextFocusPath 在可交互子元素之间切换,根据传入的 FocusNavigationEvent 选择子元素。
提示 界面通过
AbstractContainerEventHandler实现ContainerEventHandler,该类增加了子元素拖动与焦点的 getter 和 setter 逻辑。
NarratableEntry
NarratableEntry 是可以通过 Minecraft 无障碍朗读功能描述的元素。每个元素可根据悬停或选中状态提供不同朗读内容,优先级通常依次为获得焦点、鼠标悬停和其他情况。
它有三个方法:#narrationPriority 决定元素优先级,#isActive 决定是否朗读,#updateNarration 则向相应输出提供要朗读或阅读的内容。
提示 Minecraft 的所有控件都是
NarratableEntry。因此,使用现有子类型时通常无需手动实现。
Screen 子类
了解上述内容后,就可以构造基本界面。为了便于理解,下面按通常接触到的顺序介绍界面组成部分。
首先,所有界面都接收表示标题的 Component。通常由某个子类型将标题绘制到屏幕上;基础界面本身只将它用于朗读消息。
// In some Screen subclass
public MyScreen(Component title) {
super(title);
}
初始化
界面初始化后会调用 #init,从 Minecraft 实例到按游戏缩放后的相对宽高,设置界面的初始状态。添加控件、预先计算相对坐标等设置都应在此方法中完成。游戏窗口尺寸变化时,会再次调用 #init 重新初始化界面。
添加控件有三种方法,各有不同用途:
| 方法 | 描述 |
|---|---|
#addWidget |
添加可交互、可朗读,但不渲染的控件。 |
#addRenderableOnly |
添加只渲染,不可交互也不朗读的控件。 |
#addRenderableWidget |
添加可交互、可朗读且会渲染的控件。 |
通常最常用的是 #addRenderableWidget。
// In some Screen subclass
@Override
protected void init() {
super.init();
// Add widgets and precomputed values
this.addRenderableWidget(new EditBox(/* ... */));
}
界面刻更新
界面也会通过 #tick 进行刻更新,执行服务于渲染的客户端逻辑。
// In some Screen subclass
@Override
public void tick() {
super.tick();
// Execute some logic every frame
}
输入处理
界面是 GuiEventListener 的子类型,因此可以重写输入处理器,例如处理特定按键的逻辑。
渲染界面
最后,界面通过作为 Renderable 子类型获得的 #render 方法进行渲染。该方法每帧绘制界面所需的全部内容,如背景、控件和提示框等。默认实现只渲染控件。
通常不由子类型处理、却经常需要在界面中渲染的内容有两种:背景和提示框。
可通过 #renderBackground 渲染背景。其中一种重载接收选项背景的 v 偏移,用于界面背后的世界无法渲染时。
提示框通过 GuiGraphics#renderTooltip 或 GuiGraphics#renderComponentTooltip 渲染。这些方法可接收要渲染的文本组件、可选的自定义提示框组件,以及提示框在屏幕上的相对 x、y 坐标。
// In some Screen subclass
// mouseX and mouseY indicate the scaled coordinates of where the cursor is in on the screen
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
// Background is typically rendered first
this.renderBackground(graphics);
// Render things here before widgets (background textures)
// Then the widgets if this is a direct child of the Screen
super.render(graphics, mouseX, mouseY, partialTick);
// Render things after widgets (tooltips)
}
关闭界面
界面关闭时,由 #onClose 和 #removed 两个方法处理清理。
用户输入导致当前界面关闭时,会调用 #onClose。通常用它作为回调,销毁和保存界面内部进程,包括向服务器发送数据包。
界面切换并交由垃圾回收器回收前,会调用 #removed,处理尚未恢复到界面打开前初始状态的内容。
// In some Screen subclass
@Override
public void onClose() {
// Stop any handlers here
// Call last in case it interferes with the override
super.onClose();
}
@Override
public void removed() {
// Reset initial states here
// Call last in case it interferes with the override
super.removed()
;}
AbstractContainerScreen
如果界面直接关联某个菜单,应继承 AbstractContainerScreen。它作为菜单的渲染器和输入处理器,包含槽位同步和交互逻辑。因此,通常只需重写或实现两个方法,就能得到可用的容器界面。下面仍按通常接触到的顺序介绍其组成部分。
AbstractContainerScreen 通常需要三个参数:正在打开的容器菜单(以泛型 T 表示)、玩家物品栏(仅用于显示名称),以及界面标题。可以在构造函数中设置以下定位字段:
| 字段 | 描述 |
|---|---|
imageWidth |
背景纹理宽度。纹理通常位于 256 × 256 PNG 中,此值默认为 176。 |
imageHeight |
背景纹理高度。纹理通常位于 256 × 256 PNG 中,此值默认为 166。 |
titleLabelX |
界面标题渲染位置的相对 x 坐标。 |
titleLabelY |
界面标题渲染位置的相对 y 坐标。 |
inventoryLabelX |
玩家物品栏名称渲染位置的相对 x 坐标。 |
inventoryLabelY |
玩家物品栏名称渲染位置的相对 y 坐标。 |
注意 前文提到,预先计算的相对坐标应在
#init中设置,这仍然成立。这里列出的值并不是预先计算的坐标,而是静态值和相对坐标。图像尺寸表示背景纹理大小,是不变的静态值。为了便于渲染,
#init还会预先计算leftPos和topPos,表示背景渲染位置的左上角。标签坐标相对于这两个值。
leftPos和topPos也方便绘制背景,因为它们就是传入#blit的位置参数。
// In some AbstractContainerScreen subclass
public MyContainerScreen(MyMenu menu, Inventory playerInventory, Component title) {
super(menu, playerInventory, title);
this.titleLabelX = 10;
this.inventoryLabelX = 10;
/*
* If the 'imageHeight' is changed, 'inventoryLabelY' must also be
* changed as the value depends on the 'imageHeight' value.
*/
}
访问菜单
菜单被传入界面后,菜单中已同步的任何值,无论通过槽位、数据槽位还是自定义系统同步,都可以通过 menu 字段访问。
容器刻更新
玩家存活且正在查看界面时,容器界面的 #tick 会调用 #containerTick。它实际替代了容器界面中的 #tick,最常见的用途是更新配方书。
// In some AbstractContainerScreen subclass
@Override
protected void containerTick() {
super.containerTick();
// Tick things here
}
渲染容器界面
容器界面的渲染由三个方法完成:#renderBg 渲染背景纹理,#renderLabels 渲染背景上的文字,#render 则涵盖前两者,并提供灰暗背景和提示框。
从 #render 开始,最常见的重写方式(通常也是唯一需要的情况)是添加背景、调用父类渲染容器界面,最后在上方渲染提示框。
// In some AbstractContainerScreen subclass
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
this.renderBackground(graphics);
super.render(graphics, mouseX, mouseY, partialTick);
/*
* This method is added by the container screen to render
* the tooltip of the hovered slot.
*/
this.renderTooltip(graphics, mouseX, mouseY);
}
父类内部会调用 #renderBg 渲染界面背景。最常见的实现包含三次方法调用:两次设置,一次绘制背景纹理。
// In some AbstractContainerScreen subclass
// The location of the background texture (assets/<namespace>/<path>)
private static final ResourceLocation BACKGROUND_LOCATION = ResourceLocation.fromNamespaceAndPath(MOD_ID, "textures/gui/container/my_container_screen.png");
@Override
protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) {
/*
* Renders the background texture to the screen. 'leftPos' and
* 'topPos' should already represent the top left corner of where
* the texture should be rendered as it was precomputed from the
* 'imageWidth' and 'imageHeight'. The two zeros represent the
* integer u/v coordinates inside the 256 x 256 PNG file.
*/
graphics.blit(BACKGROUND_LOCATION, this.leftPos, this.topPos, 0, 0, this.imageWidth, this.imageHeight);
}
最后,#renderLabels 渲染位于背景上方、提示框下方的文字。它仅使用字体绘制相应组件。
// In some AbstractContainerScreen subclass
@Override
protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) {
super.renderLabels(graphics, mouseX, mouseY);
// Assume we have some Component 'label'
// 'label' is drawn at 'labelX' and 'labelY'
graphics.drawString(this.font, this.label, this.labelX, this.labelY, 0x404040);
}
提示 渲染标签时无需指定
leftPos和topPos偏移。PoseStack已应用这些平移,因此此方法内的所有绘制都相对于这些坐标进行。
注册 AbstractContainerScreen
要将 AbstractContainerScreen 用于菜单,必须注册它。可以在 Mod 事件总线的 RegisterMenuScreensEvent 中调用 register。
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerScreens(RegisterMenuScreensEvent event) {
event.register(MY_MENU.get(), MyContainerScreen::new);
}
来源: NeoForged/Documentation(1.21.1)。中文翻译依据提交
4a680cdd4d15。