10-gui · diff

git:20260711.586c86f to git:20260818.b1ccba0

67 added, 75 removed. Audit A to A.

# 10 — GUI / Screen 开发
> 适用版本:Fabric 1.14.4
---
## 约束
### 核心原则
- GUI 代码**仅在客户端**执行(`ClientModInitializer`)
- - Screen 类继承 `Screen` 或其子类
- - Screen 通过 `ScreenRegistry` 注册
- - **禁止**在服务端引用任何 GUI 类
+ - Screen 类继承 `Screen` 或其子类(容器屏通常再包一层 HandledScreen / ContainerScreen)
+ - 容器屏通过 `ScreenProviderRegistry.INSTANCE.registerFactory` 注册
+ - **禁止**在服务端引用任何 GUI 类(`Screen`、`ButtonWidget`、`DrawContext` 等)
---
## Decision Flow
### Decision: 选择 GUI 框架
```
- IF 简单的输入界面(文本框)
- → Screen + TextFieldWidget
+ IF 简单的输入界面(文本框、按钮、说明)
+ → Screen + TextFieldWidget + ButtonWidget
- IF 容器型 GUI(如箱子、熔炉)
- → Screen + HandledContainerScreen + ScreenHandler
+ IF 容器型 GUI(箱子、熔炉、机器)
+ → Container + ContainerScreen + ScreenProviderRegistry / ContainerProviderRegistry
- IF 使用 Fabric API Screen 模块
- → ScreenApi + HandledContainerScreen
+ IF 打开时要同步额外数据(坐标、流体量等)
+ → 额外数据:openContainer 的 PacketByteBuf writer
+
+ IF 给原版 Screen 加控件
+ → 只能改自己的 Screen.init();本档无 ScreenEvents.AFTER_INIT
```
---
## 基本 Screen
+ 按钮在 `init()` 里创建;**标题/说明文字在 `render()` 里画**,不要在 `init()` 里 `drawString`。
+ 不要用 `TextWidget`(部分档没有)或 `SimpleNamedWidget`(编造)。
+
```java
public class MyScreen extends Screen {
- private final Text title;
-
- public MyScreen(Text title) {
- super(title);
- this.title = title;
+ public MyScreen() {
+ super();
}
@Override
protected void init() {
- // 添加按钮
addButton(new ButtonWidget(width / 2 - 50, height / 2 + 20, 100, 20,
- new TextComponent("Click Me"), btn -> {
+ "Click Me", btn -> {
// 按钮点击逻辑
this.closeScreen();
}));
-
- // 添加文本
- drawString(this.font, "Hello Fabric!", width / 2 - 50, height / 2 - 20, 0xFFFFFF);
}
@Override
- public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
- this.renderBackground(matrices);
- super.render(matrices, mouseX, mouseY, delta);
+ public void render(int mouseX, int mouseY, float delta) {
+ this.renderBackground();
+ this.drawString(this.font, "Hello Fabric!", width / 2 - 50, height / 2 - 20, 0xFFFFFF);
+ super.render(mouseX, mouseY, delta);
}
}
```
## 注册 Screen
```java
- // 在 ClientModInitializer 中注册
public class ExampleModClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
- ScreenRegistry.register(
- ModScreenHandlers.MY_SCREEN_HANDLER,
- MyScreen::new
+ ScreenProviderRegistry.INSTANCE.registerFactory(
+ new Identifier("examplemod", "my_screen"),
+ (ContainerScreenFactory<MyContainer>) container -> new MyScreen()
);
}
}
```
- ## ScreenHandler(服务端数据)
+ ## Container(服务端数据)
```java
- // ScreenHandler 在服务端创建
- public class MyScreenHandler extends HandledContainerScreenHandler {
+ public class MyContainer extends Container {
private final Inventory playerInventory;
private final Inventory blockInventory;
- public MyScreenHandler(int syncId, PlayerInventory playerInventory, Inventory blockInventory) {
- super(new SimpleNamedContainerProvider(
- (id, inv, player) -> new MyScreenHandler(id, inv, blockInventory),
- new TextComponent("My Block")
- ), syncId);
+ public MyContainer(int syncId, PlayerInventory playerInventory) {
+ this(syncId, playerInventory, new SimpleInventory(9));
+ }
+
+ public MyContainer(int syncId, PlayerInventory playerInventory, Inventory blockInventory) {
+ super();
this.playerInventory = playerInventory;
this.blockInventory = blockInventory;
- // ...
+ // 给方块槽和玩家物品栏 addSlot(...)
}
@Override
public boolean canUse(PlayerEntity player) {
return blockInventory.canPlayerUse(player);
}
}
- // 注册 ScreenHandler Type
- public static final RegistryObject<ContainerType<MyScreenHandler>> MY_SCREEN_HANDLER =
- Registry.register(Registry.MENU,
- new Identifier(MOD_ID, "my_screen"),
- new ContainerType<>(MyScreenHandler::new)
- );
+ public static final Identifier MY_SCREEN_ID = new Identifier("examplemod", "my_screen");
+
+ // 1.14.4 用 Container / ContainerType / Registry.CONTAINER,不是 ScreenHandler
+ public static final ContainerType<MyContainer> MY_CONTAINER = Registry.register(
+ Registry.CONTAINER,
+ MY_SCREEN_ID,
+ new ContainerType<>(MyContainer::new)
+ );
```
- ## 打开 Screen
+ ## 打开容器(服务端)
+ 1.14.4 用 Fabric `ContainerProviderRegistry`,不要 `player.openHandledScreen` / `NamedScreenHandlerFactory`。
+
```java
- // 在服务端触发打开(通过网络包)
public void openScreen(PlayerEntity player) {
- player.openContainer(new SimpleNamedContainerProvider(
- (id, inv, p) -> new MyScreenHandler(id, inv, blockInventory),
- new TextComponent("My Block")
- ));
+ if (!(player instanceof ServerPlayerEntity serverPlayer)) return;
+ ContainerProviderRegistry.INSTANCE.openContainer(
+ MY_SCREEN_ID,
+ serverPlayer,
+ buf -> {
+ // 需要同步到客户端的额外数据写进 PacketByteBuf
+ }
+ );
}
```
- ## Fabric Screen API(fabric-screen-api-v1)
-
- ```groovy
- // build.gradle
- modImplementation "net.fabric.sdk:fabric-screen-api-v1:1.1.2+build.8"
- ```
-
- ```java
- // 使用 Screen API 的 Widget
- public class MyWidgetScreen extends Screen {
- private SimpleWidget titleWidget;
+ ## 给已有 Screen 加控件
- @Override
- protected void init() {
- titleWidget = new SimpleWidget(width / 2, height / 2, new TextComponent("My Title"));
- addChild(titleWidget);
- }
- }
- ```
+ 1.14.4 索引里**没有** `ScreenEvents.AFTER_INIT`。给自己的 Screen 在 `init()` 里 `addButton`。
+ 不要编造 `SimpleWidget` / `SimpleNamedWidget`,也不要写 `net.fabricmc.fabric-api:...`。
## 常见错误
- - ❌ 在 `onInitialize()` 中注册 `ScreenRegistry` — 服务端崩溃,应在 `ClientModInitializer`
- - ❌ 在 Screen 类中直接操作服务端数据 — 应通过 `ScreenHandler` 同步
- - ❌ 在 `render()` 方法中创建新对象 — 性能问题,应在 `init()` 中创建
- - ❌ `ScreenHandler` 中忘记调用 `putStackInSlot` — 玩家物品栏不显示
- - ❌ 在 Screen 中忘记调用 `super.render()` — 背景和子元素不渲染
+ - ❌ 在 `onInitialize()`(主 entrypoint)里注册客户端 Screen — 专用服务端会崩溃,应在 `ClientModInitializer`
+ - ❌ 在 Screen 类中直接改服务端库存/方块实体 — 应通过 Container 的槽位/同步
+ - ❌ 在 `render()` 里 `new` 按钮或分配大对象 — 控件在 `init()` 创建
+ - ❌ 忘记给玩家物品栏 `addSlot` — 物品栏不显示(没有 `addPlayerInventory()` 这种原版方法)
+ - ❌ 在 Screen 中忘记 `super.render(...)` — 背景和子控件不渲染
+ - ❌ 使用 `TypedScreenHandlerFactory` / `HandlerScreen` / `Menu`(Yarn 容器是 Container,不是 Mojmap Menu)
## 扩展点
| 配合 Skill | 协作说明 |
- |------------|---------|
- | `mc-registry` | ScreenHandler 通过 Registry.register() 注册 |
- | `mc-item` | 物品可以触发打开 Screen |
- | `mc-block` | 方块实体可以提供 ScreenHandler |
- | `mc-networking` | 网络包用于打开/关闭 Screen |
+ |-----------|---------|
+ | `mc-registry` | Container / ContainerType 通过 `Registry.register` 注册 |
+ | `mc-item` | 物品右键可 `openHandledScreen` / `openContainer` |
+ | `mc-block` | 方块实体提供 Factory / 打开包数据 |
+ | `mc-networking` | 槽位不够时再用自定义 Payload 同步进度条等 |