Menu + Screen — GUI 구현
CrusherMenu로 컨테이너 슬롯을 선언하고 CrusherScreen으로 GUI 텍스처를 렌더링합니다. MENU_TYPES DeferredRegister와 클라이언트 사이드 Screen 등록까지 완성합니다.
Menu + Screen — GUI 구현
이 챕터에서는 분쇄기 블록에 인터랙티브 GUI를 붙입니다. NeoForge의 AbstractContainerMenu와 AbstractContainerScreen을 사용해 서버-클라이언트 분리 구조를 올바르게 구성하고, 플레이어가 분쇄기를 우클릭했을 때 인벤토리 창이 열리도록 합니다.
1. MENU_TYPES DeferredRegister 선언
MasterMachineMod에 MenuType 전용 DeferredRegister를 추가합니다. BuiltInRegistries.MENU.key()를 사용해 레지스트리 키를 지정하고, IMenuTypeExtension.create로 클라이언트 사이드에서 BlockPos를 읽어 CrusherMenu를 생성하는 팩토리를 등록합니다.
// examplemod-master-projects/machine/src/main/java/com/example/master/machine/MasterMachineMod.java
public static final DeferredRegister<MenuType<?>> MENU_TYPES =
DeferredRegister.create(BuiltInRegistries.MENU.key(), MOD_ID);
public static final Supplier<MenuType<CrusherMenu>> CRUSHER_MENU = MENU_TYPES.register(
"crusher",
() -> IMenuTypeExtension.create((id, inv, buf) -> {
BlockPos pos = buf.readBlockPos();
BlockEntity be = inv.player.level().getBlockEntity(pos);
return new CrusherMenu(id, inv, (CrusherBlockEntity) be);
})
);생성자에서 MENU_TYPES.register(modEventBus)를 호출해야 합니다.
public MasterMachineMod(IEventBus modEventBus, ModContainer container) {
LOGGER.info("Master Machine Mod 로드");
BLOCKS.register(modEventBus);
ITEMS.register(modEventBus);
BLOCK_ENTITY_TYPES.register(modEventBus);
MENU_TYPES.register(modEventBus); // 추가
}2. CrusherMenu 구현
AbstractContainerMenu를 상속해 슬롯 레이아웃을 선언합니다. 슬롯은 두 종류입니다:
- 머신 슬롯: NeoForge의
SlotItemHandler로ItemStackHandler에 직접 연결 - 플레이어 슬롯: 바닐라
Slot으로 플레이어 인벤토리 연결
// examplemod-master-projects/machine/src/main/java/com/example/master/machine/menu/CrusherMenu.java
package com.example.master.machine.menu;
import com.example.master.machine.MasterMachineMod;
import com.example.master.machine.block.CrusherBlockEntity;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.items.SlotItemHandler;
public class CrusherMenu extends AbstractContainerMenu {
public final CrusherBlockEntity blockEntity;
public CrusherMenu(int id, Inventory playerInv, CrusherBlockEntity be) {
super(MasterMachineMod.CRUSHER_MENU.get(), id);
this.blockEntity = be;
// 머신 슬롯 (input/output)
this.addSlot(new SlotItemHandler(be.getInventory(), 0, 56, 35));
this.addSlot(new SlotItemHandler(be.getInventory(), 1, 116, 35));
// 플레이어 인벤토리 슬롯 (9×3)
for (int row = 0; row < 3; row++)
for (int col = 0; col < 9; col++)
addSlot(new Slot(playerInv, col + row * 9 + 9, 8 + col * 18, 84 + row * 18));
// 플레이어 핫바 (1×9)
for (int col = 0; col < 9; col++)
addSlot(new Slot(playerInv, col, 8 + col * 18, 142));
}
@Override
public ItemStack quickMoveStack(Player player, int index) {
// Shift+Click 슬롯 이동 로직 (생략 가능)
return ItemStack.EMPTY;
}
@Override
public boolean stillValid(Player player) {
return true;
}
}슬롯 좌표 기준: 텍스처 이미지(176×166) 기준 픽셀 좌표입니다. 입력 슬롯은 (56, 35), 출력 슬롯은 (116, 35)에 위치합니다.
3. CrusherScreen (클라이언트 사이드)
AbstractContainerScreen을 상속해 GUI 텍스처를 렌더링합니다. extractContents에서 GuiGraphicsExtractor.blit으로 배경 텍스처를 그립니다.
📷 스크린샷 자리 (직접 캡처해 추가하세요)
CrusherScreen — GUI 텍스처 렌더링
// examplemod-master-projects/machine/src/main/java/com/example/master/machine/client/CrusherScreen.java
package com.example.master.machine.client;
import com.example.master.machine.menu.CrusherMenu;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import net.minecraft.world.entity.player.Inventory;
public class CrusherScreen extends AbstractContainerScreen<CrusherMenu> {
private static final Identifier TEXTURE =
Identifier.fromNamespaceAndPath("master_machine", "textures/gui/crusher.png");
public CrusherScreen(CrusherMenu menu, Inventory inv, Component title) {
super(menu, inv, title, 176, 166);
}
@Override
public void extractContents(GuiGraphicsExtractor gg, int mx, int my, float partialTick) {
super.extractContents(gg, mx, my, partialTick);
gg.blit(TEXTURE, leftPos, topPos, imageWidth, imageHeight, 0.0f, 0.0f, 1.0f, 1.0f);
}
}텍스처 파일은 src/main/resources/assets/master_machine/textures/gui/crusher.png에 위치해야 합니다 (176×166 PNG).
4. 클라이언트 사이드 Screen 등록
CrusherScreen은 클라이언트 전용 클래스입니다. @EventBusSubscriber(value = Dist.CLIENT)로 서버 사이드 로딩을 방지하고, RegisterMenuScreensEvent에서 Screen을 등록합니다.
// examplemod-master-projects/machine/src/main/java/com/example/master/machine/client/ClientSetup.java
package com.example.master.machine.client;
import com.example.master.machine.MasterMachineMod;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent;
@EventBusSubscriber(modid = MasterMachineMod.MOD_ID, value = Dist.CLIENT)
public class ClientSetup {
@SubscribeEvent
public static void registerScreens(RegisterMenuScreensEvent event) {
event.register(MasterMachineMod.CRUSHER_MENU.get(), CrusherScreen::new);
}
}⚠️ Screen 사이드 분리 누락
CrusherScreen은@OnlyIn(Dist.CLIENT)또는 클라이언트 setup에서만 등록해야 합니다. 서버 사이드에서 Screen 클래스를 참조하면NoClassDefFoundError가 발생합니다.
5. CrusherBlock — openMenu 호출
CrusherBlock의 useItemOn을 오버라이드해서 우클릭 시 분쇄기 GUI를 엽니다. 서버 사이드에서만 실행하고, BlockPos를 buf.writeBlockPos(pos)로 직렬화해 클라이언트 MenuType 팩토리로 전달합니다.
// examplemod-master-projects/machine/src/main/java/com/example/master/machine/block/CrusherBlock.java
@Override
protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level level,
BlockPos pos, Player player, InteractionHand hand,
BlockHitResult hitResult) {
if (!level.isClientSide() && level.getBlockEntity(pos) instanceof CrusherBlockEntity be) {
player.openMenu(new SimpleMenuProvider(
(id, inv, p) -> new CrusherMenu(id, inv, be),
Component.translatable("container.master_machine.crusher")),
buf -> buf.writeBlockPos(pos));
}
return InteractionResult.SUCCESS;
}Component.translatable("container.master_machine.crusher")는 lang/en_us.json의 번역 키와 일치해야 합니다:
{
"block.master_machine.crusher": "Crusher",
"container.master_machine.crusher": "Crusher"
}6. 동작 흐름 요약
정리
| 클래스 | 사이드 | 역할 |
|---|---|---|
CrusherMenu | 서버 + 클라이언트 | 슬롯 레이아웃, 아이템 이동 로직 |
CrusherScreen | 클라이언트 전용 | GUI 텍스처 렌더링 |
ClientSetup | 클라이언트 전용 | Screen 등록 (RegisterMenuScreensEvent) |
MasterMachineMod.MENU_TYPES | 서버 + 클라이언트 | MenuType 등록, 클라이언트 팩토리 |
CrusherBlock.useItemOn | 서버 | openMenu 트리거 |