Skip to main content
Version: 1.21.4

屏幕(Screens)

屏幕(Screens)通常是 Minecraft 中所有图形用户界面(GUI)的基础:负责接收用户输入,在服务器端进行验证,并将结果同步回客户端。它们可以与 菜单 结合,创建类似背包视图的通信网络,也可以作为独立界面,由模组开发者通过自己的 网络 实现进行处理。

屏幕由许多部分组成,因此要完全理解 Minecraft 中的“屏幕”实际上是什么并不容易。因此,本文档将依次介绍屏幕的各个组成部分及其应用方式,最后再讨论屏幕本身。

相对坐标(Relative Coordinates)

任何内容在被渲染时,都需要某种标识来指定其显示位置。由于有许多抽象层,大多数 Minecraft 的渲染调用都使用 x、y、z 三个值来表示坐标平面。x 值从左到右递增,y 值从上到下递增,z 值则是从远到近递增。然而,这些坐标并不是固定在某个特定范围内的。它们的取值范围会根据屏幕大小以及游戏选项中的“GUI 缩放比例”而变化。因此,必须格外注意,确保传递给渲染调用的坐标值能够正确缩放——也就是要相对化(relativize)到可变的屏幕尺寸。

关于如何相对化你的坐标,请参考 屏幕 部分。

caution

如果你选择使用固定坐标,或者屏幕缩放处理不正确,渲染出来的对象可能会显示异常或者位置错乱。检查坐标相对化是否正确的一个简便方法,是在视频设置中点击“GUI 缩放”按钮。这个值会作为你的显示宽高的除数,用于决定 GUI 的实际渲染比例。

GUI 图形(Gui Graphics)

Minecraft 渲染任何 GUI 通常都是通过 GuiGraphics 实现的。GuiGraphics 是几乎所有渲染方法的第一个参数;它包含了一些用于渲染常用对象的基础方法。这些方法主要分为五类:有色矩形、字符串、纹理、物品和提示框(tooltips)。此外,还有一个用于渲染组件片段的方法(#enableScissor / #disableScissor)。GuiGraphics 还暴露了 PoseStack,用于应用各种变换,以确保组件能被正确渲染到指定位置。此外,颜色采用 ARGB 格式。

有色矩形(Colored Rectangles)

有色矩形是通过位置颜色着色器(position color shader)绘制的。所有填充方法都可以选择性地传入一个 RenderType,以指定矩形的渲染方式。有三种类型的有色矩形可以被绘制。

首先,是水平方向和垂直方向的单像素宽有色线条,分别为 #hLine#vLine#hLine 需要传入两个 x 坐标,定义左端和右端(包含两端),一个顶部 y 坐标,以及颜色。#vLine 需要传入左侧 x 坐标,两个 y 坐标(分别定义顶部和底部,包含两端),以及颜色。 其次,是 #fill 方法(method),用于在屏幕上绘制一个矩形。线段相关的方法内部实际上也会调用这个方法。该方法需要传入左侧 x 坐标、顶部 y 坐标、右侧 x 坐标、底部 y 坐标以及颜色。#fillRenderType 方法的作用类似,不过它在绘制顶点时不会对坐标位置进行修正。

最后,还有 #fillGradient 方法(method),它可以绘制带有垂直渐变效果的矩形。该方法需要传入右侧 x 坐标、底部 y 坐标、左侧 x 坐标、顶部 y 坐标、z 坐标,以及底部和顶部的颜色。

字符串(Strings)

字符串的绘制是通过其 Font 完成的,通常会包含用于普通、可透视和偏移模式的专用着色器(shader)。有两种字符串对齐方式可以被渲染,并且都带有阴影效果:左对齐字符串(#drawString 方法)和居中对齐字符串(#drawCenteredString 方法)。这两种方法都需要传入用于渲染的字体、要绘制的字符串、表示字符串左侧或中心的 x 坐标、顶部 y 坐标,以及颜色。

如果需要让文本在指定范围内自动换行,可以使用 #drawWordWrap 方法。该方法默认渲染为左对齐字符串。

note

字符串通常应以 Component 形式传递,因为它们能处理多种用例,包括该方法的另外两种重载形式。

纹理(Textures)

纹理的绘制是通过“位块传输”(blitting)实现的,因此相关方法名为 #blit。在此过程中,图片的位数据会被直接复制并绘制到屏幕上。这些操作会使用位置纹理着色器进行渲染。

每个 #blit 方法都需要传入一个 Function<ResourceLocation, RenderType>,用于决定如何渲染纹理,以及一个 ResourceLocation,它代表纹理的绝对路径:

// 指向 'assets/examplemod/textures/gui/container/example_container.png'
private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath("examplemod", "textures/gui/container/example_container.png");

虽然 #blit 方法有许多不同的重载版本,这里我们只讨论其中的两种。

第一种 #blit 方法需要传入两个整数、两个浮点数,最后再传入四个整数,假定图片为 PNG 文件。参数分别为:屏幕上的左侧 x 和顶部 y 坐标、PNG 内部的左侧 x 和顶部 y 坐标、要渲染的图片宽度和高度,以及 PNG 文件的整体宽度和高度。

tip

必须指定 PNG 文件的尺寸,这样才能将坐标归一化,获得正确的 UV 值。

第二种 #blit 方法在参数末尾多了一个整数,用于指定要绘制图片的色彩遮罩(tint color)。这个颜色会被视为 ARGB 值,如果未指定则默认为 0xFFFFFFFF

blitSprite

#blitSprite#blit 的一种特殊实现,用于将纹理绘制到 GUI 纹理图集(texture atlas)上。大多数覆盖在背景之上的纹理,比如熔炉 GUI 中的“燃烧进度”覆盖层,都是精灵(sprite)。所有精灵纹理的位置都是相对于 textures/gui/sprites,并且不需要指定文件扩展名。

// 指向 '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 相同,只是涉及 PNG 的坐标、宽度和高度的四个整数参数有所不同。

另一组 #blitSprite 方法则接收更多的纹理信息,以便渲染精灵(sprite)的一部分。这些方法会接收纹理的宽度和高度、精灵中的 x 和 y 坐标、屏幕上的左上角 x 和 y 坐标、色彩遮罩(ARGB 格式),以及要渲染的图像宽度和高度。

如果精灵的尺寸与纹理尺寸不一致,那么可以通过三种方式对精灵进行缩放:stretch(拉伸)、tile(平铺)和 nine_slice(九宫格)。stretch 会将图像从纹理尺寸拉伸到屏幕尺寸;tile 会将纹理不断重复绘制,直到填满屏幕尺寸;nine_slice 会将纹理分为一个中心、四条边和四个角,以九宫格方式平铺到目标屏幕尺寸。

这种缩放方式通过在与纹理文件同名的 mcmeta 文件中添加 gui.scaling JSON 对象来设置。

// 针对某个纹理文件 example_sprite.png
// 位于 example_sprite.png.mcmeta

// 拉伸(stretch)示例
{
"gui": {
"scaling": {
"type": "stretch"
}
}
}

// 平铺(tile)示例
{
"gui": {
"scaling": {
"type": "tile",
// 开始平铺的尺寸
// 通常就是纹理的尺寸
"width": 40,
"height": 40
}
}
}

// 九宫格(nine slice)示例
{
"gui": {
"scaling": {
"type": "nine_slice",
// 开始平铺的尺寸
// 通常就是纹理的尺寸
"width": 40,
"height": 40,
"border": {
// 被切分为边框纹理的纹理边距
"left": 1,
"right": 1,
"top": 1,
"bottom": 1
},
// 如果为 true,则纹理的中心部分会像
// stretch 类型一样拉伸,而不是九宫格平铺
"stretch_inner": true
}
}
}

绘制偏移量(Blit Offset)

渲染纹理时的 z 坐标通常被设置为 blit 偏移量(blit offset)。这个偏移量负责在屏幕上正确地分层渲染内容。z 坐标较小的内容会被渲染在背景,而 z 坐标较大的内容则会被渲染在前景。可以通过 PoseStack 本身的 #translate 方法直接设置 z 偏移量。在 GuiGraphics 的某些方法(例如物品渲染)内部也会应用一些基础的偏移逻辑。

caution

在设置 blit 偏移量后,必须在渲染完你的对象后重置它。否则,屏幕上的其他对象可能会被渲染到错误的层级,导致图形显示问题。推荐在平移(translate)前先 push 当前姿态(pose),并在完成所有偏移渲染后再 pop 回来。

可渲染对象(Renderable)

Renderable 本质上是指可以被渲染的对象。这包括屏幕、按钮、聊天框、列表等。Renderable 只包含一个方法:#render。该方法接收用于在屏幕上渲染内容的 GuiGraphics,鼠标的 x 和 y 坐标(已根据屏幕相对大小缩放),以及 tick delta(自上一帧以来经过的 tick 数)。

常见的可渲染对象有屏幕和“小部件(widget)”:通常显示在屏幕上的可交互元素,例如 Button、其子类型 ImageButton,以及用于在屏幕上输入文本的 EditBox

GuiEventListener

在 Minecraft 中渲染的任何屏幕都实现了 GuiEventListenerGuiEventListener 负责处理用户与屏幕的交互,包括来自鼠标的输入(移动、点击、释放、拖动、滚动、鼠标悬停)以及键盘输入(按下、释放、输入)。每个方法都会返回该操作是否成功影响了屏幕。像按钮、聊天框、列表等小部件也都实现了这个接口。

ContainerEventHandler

GuiEventListener 几乎同义的是其子类型:ContainerEventHandler。它们负责处理包含小部件的屏幕上的用户交互,管理当前聚焦的是哪个元素,以及如何应用相关的交互操作。ContainerEventHandler 增加了三个额外功能:可交互子元素、拖动和聚焦。

事件处理器会持有子元素,用于决定元素的交互顺序。在鼠标事件处理器中(拖动除外),鼠标悬停的第一个子元素会被执行其逻辑。

通过鼠标拖动元素(通过 #mouseClicked#mouseReleased 实现)可以提供更精确的执行逻辑。

聚焦(Focusing)允许在事件执行期间(比如键盘事件或鼠标拖动)优先检查并处理某个特定的子元素。通常通过 #setFocused 方法设置聚焦。此外,可以使用 #nextFocusPath 方法循环切换可交互的子元素,具体选择哪个子元素由传入的 FocusNavigationEvent 决定。

note

Screen(屏幕)通过 AbstractContainerEventHandler 实现了 ContainerEventHandler 接口,这为拖拽和聚焦子元素提供了 setter 和 getter 的逻辑。

可旁白条目(NarratableEntry)

NarratableEntry 是一种可以被 Minecraft 辅助功能旁白朗读的元素。每个元素可以根据当前悬停或选中的状态提供不同的旁白内容,优先级通常为聚焦、悬停,然后是其他情况。

NarratableEntry 具有四个方法:其中两个用于确定元素被朗读时的优先级(#narrationPriority#getTabOrderGroup),一个用于判断是否需要朗读旁白(#isActive),最后一个用于将旁白内容提供给相关输出(无论是语音还是文本)(#updateNarration)。

note

Minecraft 中的所有 widget(控件)都是 NarratableEntry,因此如果你使用现有的子类型,通常无需手动实现该接口。

Screen 子类型(The Screen Subtype)

有了以上知识,我们就可以构建一个基础的 screen(屏幕)。为了便于理解,下面会按通常的顺序介绍 screen 的各个组成部分。

首先,所有 screen 都接收一个 Component,它表示该 screen 的标题。这个组件通常由其子类型负责绘制到屏幕上。在基础 screen 类中,它只用于旁白消息。

// 在某个 Screen 子类中
public MyScreen(Component title) {
super(title);
}

初始化(Initialization)

当 screen 初始化完成后,会调用 #init 方法。#init 方法会根据 Minecraft 实例设置 screen 的初始状态,包括相对宽高等信息,这些尺寸会根据游戏窗口缩放。任何控件的添加、相对坐标的预计算等初始化操作,都应该在这个方法中完成。如果游戏窗口尺寸发生变化,screen 会通过再次调用 #init 方法进行重新初始化。

有三种方式可以向 screen 添加 widget(控件),每种方式适用于不同的场景:

方法描述
#addWidget添加一个可交互且可旁白朗读的 widget,但不会被渲染。
#addRenderableOnly添加一个仅用于渲染的 widget;不可交互也不会被旁白朗读。
#addRenderableWidget添加一个可交互、可旁白朗读且会被渲染的 widget。

通常情况下,最常用的是 #addRenderableWidget

// 在某个 Screen 子类中
@Override
protected void init() {
super.init();

// 添加控件和预计算的值
this.addRenderableWidget(new EditBox(/* ... */));
}

屏幕更新(Ticking Screens)

Screen 还可以通过 #tick 方法进行每帧的客户端逻辑处理,通常用于渲染相关的操作。

// 在某个 Screen 子类中
@Override
public void tick() {
super.tick();

// 每帧执行某些逻辑
}

输入处理(Input Handling)

由于屏幕(screen)是 GuiEventListener 的子类型,因此你也可以重写输入处理器(input handler),比如用于处理特定 按键 的逻辑。

渲染屏幕(Rendering the Screen)

最后,屏幕的渲染是通过其作为 Renderable 子类型所提供的 #render 方法完成的。如前所述,#render 方法会在每一帧绘制屏幕上所有需要显示的内容,比如背景、控件(widgets)、提示信息(tooltips)等。默认情况下,#render 方法只会将控件渲染到屏幕上。

在一个屏幕中,最常见但通常不会由子类型处理的两项渲染内容是背景和提示信息。

背景可以通过 #renderBackground 方法进行渲染,这个方法有一个参数用于指定选项背景的垂直偏移量(v Offset),当当前屏幕后方的世界(level)无法渲染时,会使用该参数。

提示信息(tooltips)则通过 GuiGraphics#renderTooltipGuiGraphics#renderComponentTooltip 方法进行渲染。这些方法可以接收要渲染的文本组件、一个可选的自定义提示组件,以及提示框在屏幕上的 x/y 相对坐标。

// 在某个 Screen 子类中

// mouseX 和 mouseY 表示鼠标在屏幕上的缩放坐标
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
// 通常先渲染背景
this.renderBackground(graphics, mouseX, mouseY, partialTick);

// 在这里渲染控件之前的内容(比如背景纹理)

// 如果这是 Screen 的直接子类,则渲染控件
super.render(graphics, mouseX, mouseY, partialTick);

// 在控件之后渲染内容(比如提示信息)
}

关闭屏幕(Closing the Screen)

当一个屏幕被关闭时,有两个方法负责处理清理工作:#onClose#removed

#onClose 会在用户输入关闭当前屏幕时被调用。这个方法通常用作回调,用于销毁和保存屏幕内部的任何进程,包括向服务器发送数据包。

#removed 则会在屏幕切换且即将被垃圾回收前调用。它用于处理那些在屏幕打开前未被重置回初始状态的内容。

// 在某个 Screen 子类中

@Override
public void onClose() {
// 在这里停止任何处理器

// 最后调用父类方法,以防干扰重写逻辑
super.onClose();
}

@Override
public void removed() {
// 在这里重置初始状态

// 最后调用父类方法,以防干扰重写逻辑
super.removed();
}

AbstractContainerScreen

如果一个界面(screen)是直接附加到 菜单 上的,那么应该继承 AbstractContainerScreenAbstractContainerScreen 既作为菜单的渲染器,也是输入处理器,并包含了用于同步和与槽位交互的逻辑。因此,通常只需要重写或实现两个方法,就可以拥有一个可用的容器界面。为了便于理解,容器界面的各个组成部分会按照实际使用时的顺序进行介绍。

一个 AbstractContainerScreen 通常需要三个参数:被打开的容器菜单(由泛型 T 表示)、玩家背包(仅用于显示名称)以及界面本身的标题。在这里,可以设置一系列定位字段:

字段(Field)描述(Description)
imageWidth用于背景的纹理宽度。通常位于 256 x 256 的 PNG 文件中,默认值为 176。
imageHeight用于背景的纹理高度。通常位于 256 x 256 的 PNG 文件中,默认值为 166。
titleLabelX屏幕标题渲染时的相对 x 坐标。
titleLabelY屏幕标题渲染时的相对 y 坐标。
inventoryLabelX玩家背包名称渲染时的相对 x 坐标。
inventoryLabelY玩家背包名称渲染时的相对 y 坐标。
caution

在前面的章节中提到,预计算的相对坐标应在 #init 方法中设置。这一点依然适用,因为这里提到的值并不是预计算好的坐标,而是静态值和相对化的坐标。

图片相关的值是静态且不会变化的,因为它们表示背景纹理的尺寸。为了方便渲染,在 #init 方法中会预先计算出两个额外的值(leftPostopPos),它们标记了背景渲染时的左上角坐标。标签的坐标都是相对于这两个值的。

leftPostopPos 也常用于渲染背景,因为它们已经是可以直接传递给 #blit 方法的位置参数。

// 在某个 AbstractContainerScreen 子类中
public MyContainerScreen(MyMenu menu, Inventory playerInventory, Component title) {
super(menu, playerInventory, title);

this.titleLabelX = 10;
this.inventoryLabelX = 10;

/*
* 如果更改了 'imageHeight',则必须同时更改 'inventoryLabelY',
* 因为该值依赖于 'imageHeight' 的取值。
*/
}

由于菜单会作为参数传递给界面,因此菜单内的所有已同步值(无论是通过槽位、数据槽,还是自定义同步系统)都可以通过 menu 字段访问。

容器定时(Container Tick)

容器界面(container screen)在玩家存活且正在查看该界面时,会通过 #containerTick 方法在 #tick 方法中被定期调用。这个方法本质上取代了容器界面中的 #tick 方法,最常见的用途是用于定期更新合成书(recipe book)。

// 在某个 AbstractContainerScreen 子类中
@Override
protected void containerTick() {
super.containerTick();

// 在这里处理需要定期更新的内容
}

渲染容器界面(Rendering the Container Screen)

容器界面的渲染分为三个方法:#renderBg 用于渲染背景纹理,#renderLabels 用于在背景上方渲染文本,而 #render 方法则包含前两个方法,并额外提供灰色背景和工具提示(tooltip)。

从最常见的 #render 方法重写开始(通常也是唯一需要重写的地方),一般会先渲染背景,再调用父类以渲染容器界面,最后在其上方渲染工具提示。

// 在某个 AbstractContainerScreen 子类中
@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 方法来渲染界面的背景。最常规的做法是调用三次方法:两次用于初始化,一次用于绘制背景纹理。

// 在某个 AbstractContainerScreen 子类中

// 背景纹理的位置(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) {
/*
* 将背景纹理渲染到界面上。'leftPos' 和 'topPos' 已经代表了
* 纹理应该被渲染的左上角位置,这两个值是根据 'imageWidth'
* 和 'imageHeight' 预先计算好的。两个零表示 PNG 文件内部的
* 整数 u/v 坐标,最后两个整数(通常为 256 x 256)表示纹理的实际大小。
*/
graphics.blit(
RenderType::guiTextured,
BACKGROUND_LOCATION,
this.leftPos, this.topPos,
0, 0,
this.imageWidth, this.imageHeight,
256, 256
);
}

最后,#renderLabels 方法会被调用,用于在背景上方、工具提示下方渲染文本。这个方法通常会直接使用字体对象来绘制相关的组件。

// 在某个 AbstractContainerScreen 子类中
@Override
protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) {
super.renderLabels(graphics, mouseX, mouseY);

// 假设我们有一个 Component(组件)类型的 'label'
// 'label' 会被绘制在 'labelX' 和 'labelY' 坐标位置
// 颜色值为 ARGB,如果 alpha 小于 4,则 alpha 会被设置为 255
// 最后的布尔值为 true 时会渲染阴影
graphics.drawString(this.font, this.label, this.labelX, this.labelY, 0x404040, false);
}
note

在渲染标签(label)时,不需要再指定 leftPostopPos 的偏移量。这些偏移已经在 PoseStack 内部完成了,因此在这个方法中绘制的所有内容,都是相对于这些坐标的。

注册 AbstractContainerScreen

要让一个 AbstractContainerScreen 与菜单(menu)配合使用,必须先进行注册。这可以通过在 mod 事件总线 上监听 RegisterMenuScreensEvent 事件,并在事件中调用 register 方法来实现。

// 该事件需要在 mod 事件总线上监听
private void registerScreens(RegisterMenuScreensEvent event) {
event.register(MY_MENU.get(), MyContainerScreen::new);
}