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