跳转至

原文:MinecraftForge/Documentation · 目标版本:Forge 1.20.1

屏幕(Screen)

屏幕(Screen)通常是 Minecraft 中所有图形用户界面(Graphical User Interface,GUI)的基础:接收用户输入、在服务器上验证、并将结果动作同步回客户端。它们可以与菜单(Menu)结合,为类似物品栏的视图创建通信网络;也可以独立存在,由模组开发者通过自己的网络(Network)实现来处理。

屏幕由众多部分组成,这使得完整理解 Minecraft 中「屏幕」究竟是什么变得困难。因此,本文将先逐一介绍屏幕的各个组成部分及其应用方式,然后再讨论屏幕本身。

相对坐标(Relative Coordinates)

每当渲染任何东西时,都需要某种标识符来指定它将出现在哪里。经过大量的抽象之后,Minecraft 的大多数渲染调用都接收坐标系中的 x、y、z 值。x 值从左到右递增,y 值从上到下递增,z 值从远到近递增。但这些坐标并不固定在一个特定的范围内。它们会根据屏幕大小以及选项(Options)中指定的缩放比例而变化。因此,必须格外小心,确保渲染时坐标的值能随可变的屏幕大小正确缩放。

关于如何将坐标相对化的信息,请参见屏幕一节。

Important

如果你选择使用固定坐标或错误地缩放屏幕,渲染出的对象可能会看起来很奇怪或错位。检查坐标是否相对化正确的一个简单方法是点击视频设置中的「界面尺寸(Gui Scale)」按钮。在确定 GUI 的渲染缩放比例时,该值会作为显示宽度和高度的除数。

GUI 图形(Gui Graphics)

Minecraft 渲染的任何 GUI 通常都使用 GuiGraphics 完成。GuiGraphics 几乎是所有渲染方法的第一个参数;它包含渲染常用对象的基础方法。这些方法分为五类:彩色矩形、字符串、纹理、物品和提示文本(Tooltip)。此外还有一个用于渲染组件片段的方法(#enableScissor / #disableScissor)。GuiGraphics 还暴露了 PoseStack,它应用必要的变换,使组件能在正确的位置渲染。另外,颜色采用 ARGB 格式。

彩色矩形(Colored Rectangles)

彩色矩形通过位置颜色着色器(position color shader)绘制。可以绘制三种类型的彩色矩形。

第一种是彩色的一像素宽的水平和垂直线,分别是 #hLine#vLine#hLine 接收两个 x 坐标,定义左右边界(均含),以及顶部的 y 坐标和颜色。#vLine 接收左边的 x 坐标、两个定义上下边界(均含)的 y 坐标,以及颜色。

第二种是 #fill 方法,它在屏幕上绘制一个矩形。上述两个画线方法在内部都会调用它。它接收左 x 坐标、上 y 坐标、右 x 坐标、下 y 坐标以及颜色。

最后是 #fillGradient 方法,它绘制一个带垂直渐变的矩形。它接收右 x 坐标、下 y 坐标、左 x 坐标、上 y 坐标、z 坐标,以及底部和顶部的颜色。

字符串(Strings)

字符串通过其 Font 绘制,通常使用它们自己的着色器来处理普通、透视和偏移模式。可以渲染两种对齐方式的字符串,每种都带背景阴影:左对齐字符串(#drawString)和居中对齐字符串(#drawCenteredString)。两者都接收用于渲染字符串的字体、要绘制的字符串、分别代表字符串左侧或中心的 x 坐标、顶部的 y 坐标以及颜色。

Note

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

纹理(Textures)

纹理通过 blitting 绘制,因此方法名为 #blit——就此处而言,它复制图像的位并直接绘制到屏幕上。这些绘制通过位置纹理着色器(position texture shader)完成。虽然 #blit 有许多不同的重载,但这里只讨论两个静态的 #blit

第一个静态 #blit 接收六个整数,并假定所渲染的纹理位于一个 256 x 256 的 PNG 文件中。它接收屏幕坐标中的左 x 和上 y、PNG 文件内的左 x 和上 y,以及要渲染图像的宽度和高度。

Note

必须指定 PNG 文件的大小,以便对坐标进行归一化,从而获得相应的 UV 值。

第一个方法调用的静态 #blit 将此扩展为九个整数,仅假定图像位于 PNG 文件中。它接收屏幕坐标中的左 x 和上 y、z 坐标(称为 blit 偏移)、PNG 文件内的左 x 和上 y、要渲染图像的宽度和高度,以及 PNG 文件的宽度和高度。

Blit 偏移(Blit Offset)

渲染纹理时的 z 坐标通常设置为 blit 偏移。该偏移负责在查看屏幕时正确地分层渲染。z 坐标较小的渲染会渲染在背景中,反之,z 坐标较大的渲染会渲染在前景中。z 偏移可以直接通过 PoseStack 上的 #translate 设置。GuiGraphics 的某些方法(例如物品渲染)会在内部应用一些基本的偏移逻辑。

Important

设置 blit 偏移后,必须在渲染完对象后将其重置。否则,屏幕内的其他对象可能会在错误的图层中渲染,导致图形问题。建议在平移之前 push 当前的姿态(pose),并在该偏移处完成所有渲染之后 pop。

Renderable

Renderable 本质上就是会被渲染的对象。它们包括屏幕、按钮、聊天框、列表等。Renderable 只有一个方法:#render。它接收用于向屏幕渲染内容的 GuiGraphics、缩放至相对屏幕尺寸的鼠标 x、y 位置,以及 tick 增量(自上一帧以来经过的 tick 数)。

一些常见的 renderable 是屏幕和「控件(widget)」:通常在屏幕上渲染的可交互元素,例如 Button、其子类型 ImageButton,以及用于在屏幕上输入文本的 EditBox

GuiEventListener

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

ContainerEventHandler

GuiEventListener 几乎同义的是它们的子类型:ContainerEventHandler。它们负责处理包含控件的屏幕上的用户交互,管理当前焦点在哪个控件上以及相关的交互如何应用。ContainerEventHandler 增加了三个额外特性:可交互的子元素、拖拽和焦点。

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

通过 #mouseClicked#mouseReleased 实现的鼠标拖拽元素,提供了更精确的执行逻辑。

焦点允许某个特定的子元素在事件执行期间(例如键盘事件或鼠标拖拽期间)被优先检查和优先处理。焦点通常通过 #setFocused 设置。此外,可交互子元素可以通过 #nextFocusPath 循环切换,它会根据传入的 FocusNavigationEvent 选择子元素。

Note

屏幕通过 AbstractContainerEventHandler 实现 ContainerEventHandler,它添加了拖拽和聚焦子元素的 setter 与 getter 逻辑。

NarratableEntry

NarratableEntry 是可以通过 Minecraft 的无障碍朗读(narration)功能进行播报的元素。每个元素可以根据悬停或选中的内容提供不同的朗读内容,优先级通常依次是焦点、悬停,然后是其他所有情况。

NarratableEntry 有三个方法:一个决定元素的优先级(#narrationPriority),一个决定是否朗读(#isActive),最后一个为关联的输出提供朗读内容(无论是朗读还是阅读文本,#updateNarration)。

Note

Minecraft 的所有控件都是 NarratableEntry,因此如果使用现有的子类型,通常不需要手动实现它。

屏幕子类型(The Screen Subtype)

有了以上所有知识,就可以构造一个基础屏幕了。为了更容易理解,将按照通常遇到的顺序来介绍屏幕的组成部分。

首先,所有屏幕都接收一个代表屏幕标题的 Component。该组件通常由其某个子类型绘制到屏幕上。在基础屏幕中,它仅用于朗读消息。

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

初始化(Initialization)

一旦屏幕被初始化,就会调用 #init 方法。#init 方法会根据游戏缩放后的相对宽度和高度,从 ItemRendererMinecraft 实例开始设置屏幕内的初始设置。任何设置工作,如添加控件或预计算相对坐标,都应在此方法中完成。如果游戏窗口调整了大小,屏幕会通过再次调用 #init 方法重新初始化。

向屏幕添加控件有三种方式,每种都有不同的用途:

方法 描述
#addWidget 添加一个可交互、可朗读但不渲染的控件。
#addRenderableOnly 添加一个只渲染的控件;它不可交互也不可朗读。
#addRenderableWidget 添加一个可交互、可朗读且会渲染的控件。

通常,#addRenderableWidget 的使用频率最高。

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

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

屏幕的 Tick(Ticking Screens)

屏幕还会通过 #tick 方法进行 tick,以执行一定程度的客户端逻辑用于渲染。最常见的例子是 EditBox 的光标闪烁。

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

    // 为 editBox 添加 tick 逻辑
    this.editBox.tick();
}

输入处理(Input Handling)

由于屏幕是 GuiEventListener 的子类型,因此也可以覆写输入处理器,例如处理特定按键按下的逻辑。

渲染屏幕(Rendering the Screen)

最后,屏幕通过 Renderable 子类型提供的 #render 方法进行渲染。如前所述,#render 方法每帧都会绘制屏幕需要渲染的一切内容,例如背景、控件、提示文本等。默认情况下,#render 方法只将控件渲染到屏幕上。

屏幕中通常不由子类型处理的两个最常见渲染内容是背景和提示文本。

背景可以使用 #renderBackground 渲染,其中一个方法重载接收一个 v 偏移,用于在无法渲染屏幕后方关卡(Level)时渲染选项(Options)背景。

提示文本通过 GuiGraphics#renderTooltipGuiGraphics#renderComponentTooltip 渲染,它们可以接收要渲染的文本组件、可选的自定义提示文本组件,以及提示文本应在屏幕上渲染的 x / y 相对坐标。

// 在某个 Screen 子类中

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

    // 在控件之前渲染内容(背景纹理)

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

    // 在控件之后渲染内容(提示文本)
}

关闭屏幕(Closing the Screen)

当屏幕关闭时,有两个方法负责拆除工作:#onClose#removed

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

#removed 在屏幕切换并即将被垃圾回收器释放之前调用。它负责处理任何尚未重置回屏幕打开前初始状态的内容。

// 在某个 Screen 子类中

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

    // 最后调用,以免干扰覆写
    super.onClose();
}

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

    // 最后调用,以免干扰覆写
    super.removed();
}

AbstractContainerScreen

如果屏幕直接附着在菜单上,则应继承 AbstractContainerScreen 而不是 ScreenAbstractContainerScreen 充当菜单的渲染器和输入处理器,并包含与槽位同步和交互的逻辑。因此,通常只需要覆写或实现两个方法,就能让容器屏幕(container screen)正常工作。同样,为了更容易理解,将按照通常遇到的顺序来介绍容器屏幕的组成部分。

AbstractContainerScreen 通常需要三个参数:正在打开的容器菜单(由泛型 T 表示)、玩家物品栏(仅用于显示名称)以及屏幕自身的标题。在这里面,可以设置许多定位字段:

字段 描述
imageWidth 用于背景的纹理宽度。通常位于 256 x 256 的 PNG 内,默认为 176。
imageHeight 用于背景的纹理高度。通常位于 256 x 256 的 PNG 内,默认为 166。
titleLabelX 屏幕标题将渲染的相对 x 坐标。
titleLabelY 屏幕标题将渲染的相对 y 坐标。
inventoryLabelX 玩家物品栏名称将渲染的相对 x 坐标。
inventoryLabelY 玩家物品栏名称将渲染的相对 y 坐标。

Important

在之前的章节中提到,预计算的相对坐标应在 #init 方法中设置。这里依然如此,因为此处提到的值不是预计算的坐标,而是静态值和相对化坐标。

图像相关值是静态且不变的,因为它们表示背景纹理的大小。为了方便渲染,另外两个值(leftPostopPos)会在 #init 方法中预计算,它们标记了背景将渲染的左上角。标签坐标是相对于这些值的。

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 字段访问。

容器 Tick(Container Tick)

当玩家存活并且正在查看屏幕时,容器屏幕会在 #tick 方法内通过 #containerTick 进行 tick。这实质上是取代了容器屏幕中的 #tick,其最常见的用途是对配方书进行 tick。

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

    // 在这里 tick 一些内容
}

渲染容器屏幕(Rendering the Container Screen)

容器屏幕通过三个方法渲染:#renderBg 渲染背景纹理,#renderLabels 渲染背景之上的任何文本,而 #render 则包含前两个方法,此外还提供灰色背景和提示文本。

先从 #render 开始,最常见的覆写(通常也是唯一的覆写情况)会添加背景、调用 super 渲染容器屏幕,最后在其上渲染提示文本。

// 在某个 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);
}

在 super 内部,会调用 #renderBg 来渲染屏幕的背景。最标准的表示使用三个方法调用:两个用于设置,一个用于绘制背景纹理。

// 在某个 AbstractContainerScreen 子类中

// 背景纹理的位置(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'
     * 应该已经表示纹理应渲染的左上角,因为它们是由
     * 'imageWidth' 和 'imageHeight' 预计算出来的。
     * 两个 0 代表 256 x 256 PNG 文件内的整数 u/v 坐标。
     */
    graphics.blit(BACKGROUND_LOCATION, this.leftPos, this.topPos, 0, 0, this.imageWidth, this.imageHeight);
}

最后,调用 #renderLabels 来渲染背景之上、提示文本之下的任何文本。它只是使用字体绘制相关的组件。

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

    // 假设我们有一些 Component 'label'
    // 'label' 绘制在 'labelX' 和 'labelY' 处
    graphics.drawString(this.font, this.label, this.labelX, this.labelY, 0x404040);
}

Note

渲染标签时,你需要指定 leftPostopPos 偏移。它们已经被平移进 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)
    );
}

Warning

MenuScreens#register 不是线程安全的,因此需要在并行分发事件提供的 #enqueueWork 中调用。

待复核清单

  • GuiGraphics#enableScissor / disableScissor 在 1.20.1 中为原版 API(net.minecraft.client.gui.GuiGraphics),按原文直译,未做 javap 验证。
  • MenuScreens#register + FMLClientSetupEvent#enqueueWork 为 1.20.1 标准注册方式,按术语表校准翻译,未做深度验证。
  • #renderBackground 带 v 偏移的重载、#fillGradient 的参数顺序(右、下、左、上、z)在 1.20.1 中与原文描述一致,按原文直译,未做 javap 验证。
  • AbstractContainerScreen#renderLabels#containerTick 在 1.20.1 中有效,按原文直译。