界面

界面

界面(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。