界面

界面

界面(Screen)通常是 Minecraft 所有图形用户界面(GUI)的基础:接收用户输入,在服务器上验证,再将操作结果同步回客户端。它可以与菜单结合,为物品栏式视图建立通信,也可以独立存在,由开发者通过自己的网络实现处理。

界面由众多部分组成,因此很难直接理解 Minecraft 中的“界面”究竟是什么。本文先介绍各组成部分及其用法,再介绍界面本身。

渲染 GUI

GUI 渲染分为两个步骤:提交阶段和渲染阶段。

提交阶段收集要渲染到屏幕上的所有元素,例如按钮、文本和物品。每次提交都存入 GuiRenderState,供渲染阶段处理和绘制。原版提供四种元素:GuiElementRenderState、GuiItemRenderState、GuiTextRenderState 和 PictureInPictureRenderState。下文各节介绍的内容都发生在提交阶段,内部会创建上述某种渲染状态。

渲染阶段将元素绘制到屏幕。首先准备 PictureInPictureRenderState、GuiItemRenderState 和 GuiTextRenderState,将它们处理为 GuiElementRenderState。随后排序元素,最终绘制到屏幕。最后重置 GuiRenderState,供下一个 GUI 或渲染 tick 使用。

相对坐标

向渲染状态提交内容时,需要提供坐标来确定元素位置。经过多层抽象,Minecraft 的大多数渲染调用都接收 X、Y 坐标。X 从左向右增大,Y 从上向下增大。但坐标范围并不固定,而是随窗口大小和游戏选项中的 GUI 缩放而变化。因此,必须确保传给渲染调用的坐标能根据屏幕尺寸正确缩放,也就是正确计算相对坐标。

如何计算相对坐标,请参阅界面子类一节。

注意 固定坐标或错误缩放会让渲染对象显得奇怪或位置不正确。检查相对坐标是否正确的简便方法,是点击视频设置中的“GUI 缩放”按钮。确定 GUI 渲染比例时,该值用作显示宽度和高度的除数。

GuiGraphicsExtractor

向 GuiRenderState 提交元素,通常通过 GuiGraphicsExtractor 完成。它是提交阶段几乎所有方法的第一个参数,包含提交常见渲染对象的方法。

GuiGraphicsExtractor 将当前姿态暴露为 Matrix3x2fStack,以应用任意 XY 变换:

// For some GuiGraphicsExtractor graphics

// Push a new matrix onto the stack
graphics.pose().pushMatrix();

// Apply the transformations you want the element to render with

// Takes in some XY offset
graphics.pose().translate(10, 10);
// Takes in some rotation angle in radians
graphics.pose().rotate((float) Math.PI);
// Takes in some XY scalar
graphics.pose().scale(2f, 2f);

// Submit elements to the `GuiRenderState`
graphics.blitSprite(...);

// Pop the matrix to reset the transformations
graphics.pose().popMatrix();

还可以使用 enableScissor 和 disableScissor,将元素裁剪到指定区域:

// For some GuiGraphicsExtractor graphics

// Enable the scissor with the bounds to render within
graphics.enableScissor(
    // The left X coordinate
    0,
    // The top Y coordinate
    0,
    // The right X coordinate
    10,
    // The bottom Y coordinate
    10
);

// Submit elements to the `GuiRenderState`
graphics.blitSprite(...);

// Disable the scissor to reset the rendering area
graphics.disableScissor();

节点树与层次

元素提交到 GuiRenderState 时,并非简单地加入列表,否则某些元素会因提交顺序而完全被其他元素覆盖。为解决这个问题,元素先在某个层次(stratum)中整理成节点树。排序依据是元素定义的 ScreenArea#bounds;没有定义边界的元素不会提交渲染。

GuiRenderState 由单向链表形式的 GuiRenderState.Node 构成,通过“up”保存下一个元素的引用。节点从首个元素开始向“上”渲染。每个节点都有自己的层数据,包含渲染状态。首次提交元素时,会根据其 ScreenArea#bounds 决定使用或创建哪个节点:所选或新建节点位于最高相交元素节点的上方一层。

警告 虽然 ScreenArea#bounds 标记为可空,但若未定义边界,提交到渲染状态的元素不会被添加。该方法允许为空,是因为渲染阶段提交的元素直接加入当前节点,而不会根据边界计算节点。

渲染状态中的每个节点列表称为一个层次。调用 GuiGraphicsExtractor#nextStratum 会创建新的节点列表,使一个渲染状态包含多个层次。新层次绘制在之前所有层次的元素上方,例如物品提示框。调用 nextStratum 后不能返回之前的层次。

GuiElementRenderState

GuiElementRenderState 保存 GUI 元素如何绘制到屏幕的元数据。它继承 ScreenArea,定义屏幕上的 bounds。边界应始终覆盖整个渲染元素,使其在节点列表中正确排序。计算边界时,通常会用到下面介绍的位置、姿态等参数。

scissorArea 裁剪元素可渲染的区域。如果为 null,整个元素都会绘制;如果裁剪矩形与 bounds 不相交,则什么也不绘制。

其他三个方法负责实际绘制。pipeline 定义元素使用的着色器与元数据。textureSetup 可指定片元着色器中的 Sampler0、Sampler1、Sampler2 或其组合。buildVertices 则向接收的 VertexConsumer 传入要上传到缓冲区的顶点。

如果 GuiGraphicsExtractor 现有方法不足,NeoForge 添加的 GuiGraphicsExtractor#submitGuiElementRenderState 可提交自定义元素渲染状态。

// For some GuiGraphicsExtractor graphics
graphics.submitGuiElementRenderState(new GuiElementRenderState() {

    // Store the current pose of the stack
    private final Matrix3x2f pose = new Matrix3x2f(graphics.pose());
    // Store the current scissor area
    @Nullable
    private final ScreenRectangle scissorArea = graphics.peekScissorStack();

    @Override
    public ScreenRectangle bounds() {
        // We will assume the bounds is 0, 0, 10, 10
        
        // Compute the initial rectangle
        ScreenRectangle rectangle = new ScreenRectangle(
            // The XY position
            0, 0,
            // The width and height of the element
            10, 10
        );

        // Transform the rectangle to its appropriate location using the pose
        rectangle = rectangle.transformMaxBounds(this.pose);

        // If there is a scissor area defined, return the intersection of the two rectangles
        // Otherwise, return the full bounds
        return this.scissorArea != null
            ? this.scissorArea.intersection(rectangle)
            : rectangle;
    }

    @Override
    @Nullable
    public ScreenRectangle scissorArea() {
        return this.scissorArea;
    }

    @Override
    public RenderPipeline pipeline() {
        return RenderPipelines.GUI;
    }

    @Override
    public TextureSetup textureSetup() {
        // Returns the textures to be used by the samplers in a fragment shader
        // When used by the fragment shader:
        // - Sampler0 typically contains the element texture
        // - Sampler1 typically provides a second element texture, currently only used by the end portal pipeline
        // - Sampler2 typically contains the game's lightmap texture

        // Should generally specify at least one texture in Sampler0
        return TextureSetup.noTexture();
    }

    @Override
    public void buildVertices(VertexConsumer consumer) {
        // Build the vertices using the vertex format specified by the pipeline
        // For GUI, uses quads with position and color
        // Color must be in ARGB format
        consumer.addVertexWith2DPose(this.pose, 0,   0).setUv(0, 0).setColor(0xFFFFFFFF);
        consumer.addVertexWith2DPose(this.pose, 0,  10).setUv(0, 1).setColor(0xFFFFFFFF);
        consumer.addVertexWith2DPose(this.pose, 10, 10).setUv(1, 1).setColor(0xFFFFFFFF);
        consumer.addVertexWith2DPose(this.pose, 10,  0).setUv(1, 0).setColor(0xFFFFFFFF);
    }
});

元素排序

此前介绍的元素只使用 XY 坐标。GUI 渲染忽略 Z 坐标,因为屏幕元素使用的 RenderPipeline 都禁用了深度测试。即使采用较高级管线的 3D 元素,默认也使用同样禁用深度测试的 RenderPipeline#GUI_TEXTURED_PREMULTIPLIED_ALPHA 绘制到二维纹理,从而避免常见场景中的深度冲突。

因此,渲染阶段按顺序绘制每个层次,每个节点列表从首个元素向“上”绘制。同一节点内的顺序由 GuiRenderer#ELEMENT_SORT_COMPARATOR 决定,它依次根据 GuiElementRenderState#scissorArea、pipeline 和 textureSetup 排序。

警告 文本的字形不参与排序,始终在当前节点的所有元素之后绘制。

未指定 scissorArea 的元素先绘制,随后依次比较裁剪区域顶部 Y、底部 Y、左侧 X 和右侧 X。如果两个元素的裁剪区域相同,就使用 pipeline 的排序键(RenderPipeline#getSortKey)。该键取决于管线的构建顺序;在原版中,就是 RenderPipelines 静态常量的类加载顺序。如果管线排序键也相同,则使用 textureSetup:未指定纹理设置的元素在前,其后根据纹理元素的排序键(TextureSetup#getSortKey)排序。

警告 从技术上讲,受 RenderPipeline 和 TextureSetup 影响,元素排序并非确定性的。排序的目标是尽可能减少管线和纹理切换次数,而非保证固定顺序。

GuiGraphicsExtractor 中的方法

GuiGraphicsExtractor 包含提交常见渲染对象的方法,分为六类:彩色矩形、字符串、纹理、物品、提示框和画中画。每个方法都会提交一个元素,继承当前 pose,并根据 enableScissor/disableScissor 从 peekScissorStack 继承裁剪区域。方法接收的所有颜色必须采用 ARGB 格式。

彩色矩形

彩色矩形通过 ColoredRectangleRenderState 提交。所有填充方法都可以接收可选的 RenderPipeline 和 TextureSetup,指定矩形的渲染方式。原文概称三种,但下文实际介绍了以下四类操作。

首先是单像素宽的彩色水平线和垂直线,分别为 horizontalLine 与 verticalLine。前者接收包含端点的左右 X 坐标、顶部 Y 和颜色;后者接收左侧 X、包含端点的上下 Y 坐标和颜色。

其次,fill 提交要绘制到屏幕的矩形,线条方法内部也会调用它。它接收左侧 X、顶部 Y、右侧 X、底部 Y 和颜色。

第三,outline 提交四个单像素宽的矩形作为边框,接收左侧 X、顶部 Y、边框宽高和颜色。

最后,fillGradient 绘制垂直渐变矩形,接收左侧 X、顶部 Y、右侧 X、底部 Y,以及底部和顶部颜色。

字符串

字符串、Component 和 FormattedCharSequence 通过 GuiTextRenderState 提交。每个字符串由提供的 Font 绘制,用它创建 BakedGlyph.GlyphInstance,并按需创建 BakedGlyph.Effect,使用指定的 GlyphRenderTypes#guiPipeline。渲染阶段,文本渲染状态会为字符串中的每个字符转换出 GlyphRenderState,还可能产生 GlyphEffectRenderState。

字符串支持两种对齐:左对齐(text)和居中(centeredText)。它们都接收字体、文本、分别代表左侧或中心位置的 X、顶部 Y 和颜色。左对齐方法还可指定是否绘制文字阴影。

要在指定边界内换行,可以使用 textWithWordWrap;要添加矩形背景,可以使用 textWithBackdrop。两者默认都提交左对齐字符串。

也可以通过 ActiveTextCollector 提交字符串,它提供按指定对齐、不透明度和滚动等元数据渲染文本的方法。通过 GuiGraphicsExtractor#textRenderer 或 textRendererForWidget 创建收集器,也可以继承 ActiveTextCollector。它通常接收 $HoveredTextEffects,提供是否渲染提示框或改变光标等基本选项。随后使用 accept 或 acceptScrolling 渲染文本:接收相对于对齐方式的 X、Y、来自 GuiGraphicsExtractor 的一组参数、文本本身,以及可选的文本对齐方式。acceptScrolling 还接收最左、最右、最上、最下位置作为滚动边界。

提示 文本通常应以 Component 传入,因为它可处理多种用法,包括方法的另外两个重载。

纹理

纹理通过 BlitRenderState 提交,因此方法名为 blit。它复制图像位数据,通过 RenderPipeline 参数绘制到屏幕。每次 blit 还接收一个代表纹理绝对位置的 Identifier:

// Points to 'assets/examplemod/textures/gui/container/example_container.png'
private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath("examplemod", "textures/gui/container/example_container.png");

blit 有许多重载,这里只介绍两个。

第一个接收两个整数、两个浮点数,再加四个整数,假设图像位于 PNG 文件中。参数依次为屏幕左侧 X、顶部 Y,PNG 内左侧 X、顶部 Y,要渲染图像的宽高,以及 PNG 文件宽高。

建议 必须指定 PNG 文件尺寸,以便归一化坐标,得到相应 UV 值。

第二个重载在末尾增加一个整数,表示图像着色颜色。未指定时为 0xFFFFFFFF。

blitSprite

blitSprite 是从 GUI 纹理图集中获取纹理的特殊 blit 实现。覆盖在背景上的大多数纹理(例如熔炉 GUI 的燃烧进度)都是精灵。所有精灵纹理都相对于 textures/gui/sprites,且无需文件扩展名。

// Points to 'assets/examplemod/textures/gui/sprites/container/example_container/example_sprite.png'
private static final Identifier SPRITE = Identifier.fromNamespaceAndPath("examplemod", "container/example_container/example_sprite");

一组 blitSprite 方法的参数与 blit 相同,但省略与 PNG 坐标及宽高有关的四个整数。

另一组方法接收更多纹理信息,允许绘制精灵的一部分:精灵宽高、精灵内 X 和 Y、屏幕左侧 X 和顶部 Y、着色颜色,以及要绘制的图像宽高。

若精灵尺寸与纹理尺寸不一致,可采用三种缩放方式: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
            },
            // When true the center part of the texture will be applied like
            // the stretch type instead of a nine slice tiling.
            "stretch_inner": true
        }
    }
}

提示 使用平铺或九宫格纹理的 blitSprite 会通过 TiledBlitRenderState 提交元素。除 BlitRenderState 原有参数外,它还指定平铺块的宽高。

物品

物品通过 GuiItemRenderState 提交。渲染阶段,根据物品边界和客户端物品属性,它会转换为 BlitRenderState 或 OversizedItemRenderState。

item 接收 ItemStack 及屏幕左侧 X 和顶部 Y,也可选择接收持有物品的 LivingEntity、物品堆当前所在 Level 和种子值。另一方法 fakeItem 会将 LivingEntity 设为 null。

耐久条、冷却和数量等物品装饰通过 itemDecorations 处理。它除基本 item 参数外,还接收 Font 和覆盖数量显示的文本。

提示框

提示框通过上述多种渲染状态提交。相关方法分为“下一帧”和“立即”两类。两类都接收文本字体 Font、Component 列表、用于特殊渲染的可选 TooltipComponent、左侧 X 与顶部 Y、用于调整位置的 ClientTooltipPositioner,以及背景和边框纹理。

“下一帧”提示框实际上并非在下一帧提交,而是延迟到 Screen#render 调用后提交。提示框会加入新层次,因此显示在界面所有元素上方。这些方法的命名形式为 set*Tooltip*ForNextFrame,还可额外接收一个布尔值,决定是否覆盖已经延迟提交的提示框,以及该提示框应使用的 ItemStack。

“立即”提示框在方法调用时立刻提交,命名形式为 tooltip,还接收提示框所悬停物品的 ItemStack。

画中画

画中画(Picture-in-Picture,PiP)允许将任意对象绘制到屏幕。它先将对象绘制到中间纹理(“图片”),再在渲染阶段默认以 BlitRenderState 提交到 GuiRenderState,而非直接绘制到输出。GuiGraphicsExtractor 提供地图(map)、实体(entity)、玩家皮肤(skin)、书模型(book)、旗帜图案(bannerPattern)、告示牌(sign)和分析器图表(profilerChart)等方法。

提示 物品超出默认 16×16 边界且 ClientItem.Properties#oversizedInGui 为 true 时,使用 OversizedItemRenderer 画中画机制渲染。

每个 PiP 提交一个 PictureInPictureRenderState 来绘制对象。它与 GuiElementRenderState 类似,也继承 ScreenArea,定义 bounds 和 scissorArea。它还定义图片位置和尺寸,指定左 X(x0)、右 X(x1)、上 Y(y0)和下 Y(y1)。图片内的元素可按浮点值 scale 缩放。还可通过额外的 pose 变换图片的 XY 坐标;默认使用单位变换,因为对象通常已在图片内部变换。可以通过 PictureInPictureRenderState#getBounds 方便地计算边界,但修改 pose 后,需要自行实现边界逻辑。

// Other parameters can be added, but this is the minimum required to implement all methods
public record ExampleRenderState(
    int x0, // The left X
    int x1, // The right X
    int y0, // The top Y
    int y1, // The bottom Y
    float scale, // The scale factor when drawing to the picture
    @Nullable ScreenRectangle scissorArea, // The rendering area
    @Nullable ScreenRectangle bounds // The bounds of the element
) implements PictureInPictureRenderState {

    // Additional constructors
    public ExampleRenderState(int x, int y, int width, int height, @Nullable ScreenRectangle scissorArea) {
        this(
            x, // x0
            x + width, // x1
            y, // y0
            y + height, // y1
            1f, // scale
            scissorArea,
            PictureInPictureRenderState.getBounds(x, y, x + width, y + height, scissorArea)
        );
    }
}

每个 PiP 都有自己的 PictureInPictureRenderer<T>,负责将渲染状态绘制并提交到图片,其中 T 是实现的 PictureInPictureRenderState。可以重写许多方法,几乎完整控制整个管线,但有三个方法必须实现。

第一个是 getRenderStateClass,仅返回 PictureInPictureRenderState 的类。原版曾用它注册渲染器对应的状态。NeoForge 仍使用渲染状态类,但通过事件将其映射到动态渲染器池,而非调用此方法。

getTextureLabel 为正在写入的图片提供唯一调试标签。renderToTexture 则实际将对象绘制到图片,与其他渲染方法类似。

public class ExampleRenderer extends PictureInPictureRenderer<ExampleRenderState> {

    // Takes in the buffers used to write the object to the picture
    public ExampleRenderer(MultiBufferSource.BufferSource bufferSource) {
        super(bufferSource);
    }

    @Override
    public Class<ExampleRenderState> getRenderStateClass() {
        // Returns the render state class
        return ExampleRenderState.class;
    }

    @Override
    protected String getTextureLabel() {
        // Can be any string, but should be unique
        // Prefix with mod id for greater clarity
        return "examplemod: example pip";
    }

    @Override
    protected void renderToTexture(ExampleRenderState renderState, PoseStack pose) {
        // Modify pose if desired
        // Can push/pop if wanted, but a new `PoseStack` is created for writing to the picture
        pose.translate(...);

        // Render the object to the screen
        VertexConsumer consumer = this.bufferSource.getBuffer(RenderType.lines());
        consumer.addVertex(...).setColor(...).setNormal(...);
        consumer.addVertex(...).setColor(...).setNormal(...);
    }

    // Additional methods

    @Override
    protected void blitTexture(ExampleRenderState renderState, GuiRenderState guiState) {
        // Submits the picture to the gui render state as a `BlitRenderState` by default
        // Override this if you want to modify the `BlitRenderState`
        // Should call `GuiRenderState#submitBlitToCurrentLayer`
        // Bounds can be `null`
        super.blitTexture(renderState, guiState);
    }

    @Override
    protected boolean textureIsReadyToBlit(ExampleRenderState renderState) {
        // When true, this reuses the already written-to picture instead of
        // constructing a new picture and writing to it using `renderToTexture`.
        // This should only be true if it is guaranteed that two elements will
        // be rendered *exactly* the same.
        return super.textureIsReadyToBlit(renderState);
    }

    @Override
    protected float getTranslateY(int scaledHeight, int guiScale) {
        // Sets the initial offset the `PoseStack` is translated by in the Y direction.
        // Common implementations use `scaledHeight / 2f` to center the Y coordinate similar to X.
        return scaledHeight;
    }

    @Override
    public boolean canBeReusedFor(ExampleRenderState state, int textureWidth, int textureHeight) {
        // A NeoForge-added method used to check if this renderer can be reused on a subsequent frame.
        // When true, this will reuse the constructed state and renderer from the previous frame.
        // When false, a new renderer will be created.
        return super.canBeReusedFor(state, textureWidth, textureHeight);
    }
}

要使用 PiP,必须在 Mod 事件总线的 RegisterPictureInPictureRenderersEvent 中注册渲染器。

@SubscribeEvent // on the mod event bus
public static void registerPip(RegisterPictureInPictureRenderersEvent event) {
    event.register(
        // The PiP render state class
        ExampleRenderState.class,
        // A factory that takes in the `MultiBufferSource.BufferSource` and returns the PiP renderer
        ExampleRenderer::new
    );
}

随后可通过 NeoForge 添加的 GuiGraphicsExtractor#submitPictureInPictureRenderState 提交 PiP 渲染状态:

// For some GuiGraphicsExtractor graphics
graphics.submitPictureInPictureRenderState(new ExampleRenderState(
    0, 0,
    10, 10,
    // Get the scissor area from the stack
    graphics.peekScissorStack()
));

提示 NeoForge 修复了在同一帧无法提交同一种 PiP 渲染状态多个实例的问题。

Renderable

Renderable 就是可渲染对象,包括界面、按钮、聊天框、列表等。它只有 #extractRenderState 一个方法,接收用于提交屏幕元素的 GuiGraphicsExtractor、按相对界面尺寸缩放的鼠标 x 和 y,以及 tick delta(距上一帧经过了多少 tick)。

常见可渲染对象包括界面和“控件”:例如可交互的 Button、其子类 ImageButton,以及用于输入文本的 EditBox。

GuiEventListener

Minecraft 的所有界面都实现 GuiEventListener,负责处理用户与界面的交互,包括鼠标移动、点击、松开、拖动、滚动、悬停,以及键盘按下、松开和键入。每个方法返回相应操作是否成功影响界面。按钮、聊天框、列表等控件也实现此接口。

ContainerEventHandler

GuiEventListener 的子类型 ContainerEventHandler 与它密切相关,负责处理包含控件界面的用户交互,管理当前焦点及交互的应用方式。它增加三项功能:可交互子元素、拖动和焦点。

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

通过 #mouseClicked 和 #mouseReleased 实现鼠标拖动元素,可以更精确地执行逻辑。

焦点使特定子元素在事件执行期间优先检查和处理,例如键盘事件或鼠标拖动。通常通过 #setFocused 设置焦点。还可以使用 #nextFocusPath,根据传入的 FocusNavigationEvent 在可交互子元素之间切换。

提示 界面通过 AbstractContainerEventHandler 实现此接口,后者增加了拖动和子元素焦点的 setter、getter 逻辑。

NarratableEntry

NarratableEntry 是可由 Minecraft 无障碍旁白功能朗读的元素。每个元素可根据悬停或选择状态提供不同旁白,优先级通常为焦点、悬停,再到其他情况。

它有四个方法:#narrationPriority 和 #getTabOrderGroup 决定朗读优先级;#isActive 决定是否朗读;#updateNarration 则向相应输出提供用于朗读或阅读的旁白内容。

提示 Minecraft 所有控件都是 NarratableEntry,因此使用现有子类型时通常无需手动实现。

界面子类

掌握上述知识后,就可以构建基本界面。为便于理解,以下按通常遇到的顺序介绍界面组件。

所有界面首先接收表示标题的 Component。它通常由某个子类绘制到屏幕,基类仅用它生成旁白消息。界面还可接收 Minecraft 实例和渲染文本的 Font;未指定时使用默认实例和字体。

// In some Screen subclass
public MyScreen(Component title) {
    super(Minecraft.getInstance(), Minecraft.getInstance().font, 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 子类型,因此可以重写输入处理器,例如处理特定按键的逻辑。

渲染界面

界面通过 #extractRenderStateWithTooltipAndSubtitles,在三个不同层次提交元素:背景层次、元素层次,以及可选的悬浮层次。

背景元素最先通过 #extractBackground 提交,通常包括模糊效果和背景纹理。

警告 GuiGraphicsExtractor#blurBeforeThisStratum 处理的模糊,每帧只能调用一次。尝试第二次提交模糊会抛出异常。

接着,作为 Renderable 子类型提供的 #extractRenderState 提交元素层次,主要包括控件、标签,并设置待提交的悬浮内容。

最后,悬浮层次提交显示在先前元素上方的内容,例如提示框。

// In some Screen subclass

// mouseX and mouseY indicate the scaled coordinates of where the cursor is in on the screen
@Override
public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
    // Submit things on the background stratum
    this.extractTransparentBackground(graphics);
}

@Override
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
    // Submit things before widgets

    // Then the widgets if this is a direct child of the Screen
    super.extractRenderState(graphics, mouseX, mouseY, partialTick);

    // Submit things after widgets

    // Set the tooltip to be added above everything in this method
    graphics.setTooltipForNextFrame(...);
}

关闭界面

关闭界面时,#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。它充当菜单的界面和输入处理器,包含槽位同步与交互逻辑,因此通常只需重写或实现两个方法,就能得到可用容器界面。下面仍按通常遇到的顺序介绍组件。

它通常需要五个参数:打开的容器菜单(由泛型 T 表示)、玩家物品栏(仅用于显示名称)、界面标题,以及背景纹理宽高。

提示 背景纹理为 176×166 时,可以在父构造函数中省略宽高。这里并非通常为 256×256 的 PNG 图像尺寸,而是其中实际使用的纹理区域尺寸。

可以设置以下定位字段:

字段 描述
titleLabelX 界面标题绘制位置的相对 x。
titleLabelY 界面标题绘制位置的相对 y。
inventoryLabelX 玩家物品栏名称绘制位置的相对 x。
inventoryLabelY 玩家物品栏名称绘制位置的相对 y。

注意 前文提到,预计算相对坐标应放在 #init 中,这仍然适用。这里提到的是静态值及相对坐标,而非预计算坐标。

图像尺寸是静态、不变的,因为它表示背景纹理尺寸。为方便渲染,init 会预计算 leftPos 和 topPos,标记背景绘制位置的左上角。标签坐标相对于这两个值。

这两个值本身也正是 GuiGraphicsExtractor#blit 接收的位置,因此可以方便地用于绘制背景。

// In some AbstractContainerScreen subclass
public MyContainerScreen(MyMenu menu, Inventory playerInventory, Component title) {
    super(menu, playerInventory, title, 176, 166);

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

访问菜单

菜单已经传入界面,因此菜单中通过槽位、数据槽位或自定义系统同步的所有值,现在都能通过 menu 字段访问。

容器逐刻更新

玩家存活且正在查看界面时,容器界面会在 #tick 中调用 #containerTick。它基本取代容器界面的 tick,最常用于更新配方书。

// In some AbstractContainerScreen subclass
@Override
protected void containerTick() {
    super.containerTick();

    // Tick things here
}

渲染容器界面

容器界面使用全部三个层次。首先,重写 #extractBackground,向背景层次提交背景纹理。随后元素层次在 #extractContents 中提交控件,再在 #extractLabels 中提交标签。最后,AbstractContainerScreen 设置通过 extractTooltip 向悬浮层次提交的提示框。

先从背景开始,调用 extractBackground 向背景层次提交背景元素。

// In some AbstractContainerScreen subclass

// The location of the background texture (assets/<namespace>/<path>)
private static final Identifier BACKGROUND_LOCATION = Identifier.fromNamespaceAndPath(MOD_ID, "textures/gui/container/my_container_screen.png");

@Override
protected void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
    super.extractBackground(graphics, mouseX, mouseY, a);

    // Submits the background texture. '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 PNG file, whose size is represented by
    // the last two integers (typically 256 x 256).
    graphics.blit(
        RenderPipelines.GUI_TEXTURED,
        BACKGROUND_LOCATION,
        this.leftPos, this.topPos,
        0, 0,
        this.imageWidth, this.imageHeight,
        256, 256
    );
}

extractLabels 在渲染层次的控件之后提交文本,使用界面字体调用 text 提交相应组件。

// In some AbstractContainerScreen subclass
@Override
protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
    super.extractLabels(graphics, mouseX, mouseY);

    // Assume we have some Component 'label'
    // 'label' is drawn at 'labelX' and 'labelY'
    // The color is an ARGB value
    // The final boolean renders the drop shadow when true
    graphics.text(this.font, this.label, this.labelX, this.labelY, 0xFF404040, false);
}

提示 提交标签时不需要指定 leftPos 和 topPos 偏移。Matrix3x2fStack 中已经完成平移,此方法内的所有内容都相对于这两个坐标提交。

注册 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(26.1)。中文翻译依据提交 4a680cdd4d15。