跳转至

18. 方块注册(BB 模型 + 箱子)

本章讲三件事: 1. 方块怎么注册(含方块物品、资源文件、数据生成) 2. 怎么用 Blockbench(BB)模型当方块外观 3. 怎么做一个「箱子」(BlockEntity + 容器 + 界面 + 掉落/漏斗/比较器)

环境:Minecraft 1.20.1 / Forge 47.x。示例 modid 用 mymod,包名 com.example.mymod


0. 最小可用清单

一个"能放出来、能拿在手里、能挖掉、有贴图"的方块,需要这些东西:

类别 文件/代码
代码 ModBlocks(方块注册)、ModItems(方块物品)、主类里挂到事件总线
方块状态 assets/mymod/blockstates/my_block.json
方块模型 assets/mymod/models/block/my_block.json
物品模型 assets/mymod/models/item/my_block.json
贴图 assets/mymod/textures/block/my_block.png
掉落表 data/mymod/loot_tables/blocks/my_block.json
语言 assets/mymod/lang/en_us.jsonzh_cn.json
箱子额外 BlockEntity 注册、容器/Menu、BER(可选)、方块实体语言键

1. 注册方块本体

1.1 注册器

package com.example.mymod.block;

import com.example.mymod.MyMod;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.material.MapColor;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;

public class ModBlocks {
    public static final DeferredRegister<Block> BLOCKS =
            DeferredRegister.create(ForgeRegistries.BLOCKS, MyMod.MODID);

    public static final RegistryObject<Block> MY_BLOCK = BLOCKS.register("my_block",
            () -> new Block(BlockBehaviour.Properties.of()
                    .mapColor(MapColor.STONE)
                    .strength(2.0F, 6.0F)      // 硬度、抗爆
                    .sound(SoundType.STONE)    // 音效/挖掘音
                    .requiresCorrectToolForDrops()  // 需要正确工具才掉落
            ));
}

主类里挂上(方块走 mod 事件总线):

public MyMod() {
    IEventBus bus = FMLJavaModLoadingContext.get().getModEventBus();
    ModBlocks.BLOCKS.register(bus);
    ModItems.ITEMS.register(bus);
}

1.2 常用 Properties(1.20.1)

BlockBehaviour.Properties.of()                    // 全默认
        .mapColor(MapColor.WOOD)                  // 地图颜色
        .strength(2.0F)                           // 硬度(挖的时间)
        .strength(2.0F, 6.0F)                     // 硬度 + 抗爆
        .sound(SoundType.WOOD)
        .lightLevel(s -> 12)                      // 发光
        .noCollission()                           // 无碰撞(花草)
        .noOcclusion()                            // 不遮挡相邻面(模型有洞/透明必须加)
        .dynamicShape()                           // 形状随状态变化
        .randomTicks()
        .instabreak()
        .requiresCorrectToolForDrops()
        .explosionResistance(1200F)
        .copy(Blocks.STONE)                       // 复制原版方块属性(最省事)

💡 Properties.copy(Blocks.OAK_PLANKS) 是最常用的偷懒写法;想微调再链式覆盖。

1.3 方块物品(BlockItem)

方块要进背包就得注册 BlockItem,名字必须和方块同名(同一个 id):

public class ModItems {
    public static final DeferredRegister<Item> ITEMS =
            DeferredRegister.create(ForgeRegistries.ITEMS, MyMod.MODID);

    public static final RegistryObject<Item> MY_BLOCK_ITEM = ITEMS.register("my_block",
            () -> new BlockItem(ModBlocks.MY_BLOCK.get(), new Item.Properties()));
}

1.4 加进创造模式标签页

public class ModTabs {
    public static final DeferredRegister<CreativeModeTab> TABS =
            DeferredRegister.create(Registries.CREATIVE_MODE_TAB, MyMod.MODID);

    public static final RegistryObject<CreativeModeTab> MAIN = TABS.register("main",
            () -> CreativeModeTab.builder()
                    .title(Component.translatable("itemGroup.mymod"))
                    .icon(() -> new ItemStack(ModItems.MY_BLOCK_ITEM.get()))
                    .displayItems((params, output) -> {
                        output.accept(ModItems.MY_BLOCK_ITEM.get());
                    })
                    .build());
}

⚠️ 别用 FMLCommonSetupEventCreativeModeTab.builder() 之外的老写法;DeferredRegister + mod 事件总线是在 1.20.1 的标准做法。


2. 资源文件

2.1 方块状态 blockstates

assets/mymod/blockstates/my_block.json

{
  "variants": {
    "": { "model": "mymod:block/my_block" }
  }
}

有朝向后(比如箱子)就要写四个朝向:

{
  "variants": {
    "facing=north": { "model": "mymod:block/my_chest" },
    "facing=east":  { "model": "mymod:block/my_chest", "y": 90 },
    "facing=south": { "model": "mymod:block/my_chest", "y": 180 },
    "facing=west":  { "model": "mymod:block/my_chest", "y": 270 }
  }
}

2.2 模型

assets/mymod/models/block/my_block.json(手写最简版)

{
  "parent": "block/block",
  "textures": {
    "particle": "mymod:block/my_block",
    "all": "mymod:block/my_block"
  },
  "elements": [
    {
      "from": [0, 0, 0],
      "to": [16, 16, 16],
      "faces": {
        "down":  { "texture": "#all", "cullface": "down" },
        "up":    { "texture": "#all", "cullface": "up" },
        "north": { "texture": "#all", "cullface": "north" },
        "south": { "texture": "#all", "cullface": "south" },
        "west":  { "texture": "#all", "cullface": "west" },
        "east":  { "texture": "#all", "cullface": "east" }
      }
    }
  ]
}

assets/mymod/models/item/my_block.json(方块物品直接套用方块模型)

{ "parent": "mymod:block/my_block" }

2.3 贴图与语言

  • 贴图:assets/mymod/textures/block/my_block.png16×16 起步,建议 16 的整数倍
  • 语言键(lang/zh_cn.json / en_us.json):
{
  "block.mymod.my_block": "示例方块",
  "itemGroup.mymod": "示例模组"
}

方块是 block.<modid>.<id>;方块物品不写 item. 键(它跟方块共用 block.)。

2.4 掉落表

data/mymod/loot_tables/blocks/my_block.json(1.20.1 是 loot_tables;1.21 才改叫 loot_table

{
  "type": "minecraft:block",
  "pools": [
    {
      "rolls": 1,
      "entries": [ { "type": "minecraft:item", "name": "mymod:my_block" } ],
      "conditions": [ { "condition": "minecraft:survives_explosion" } ]
    }
  ]
}

用 DataGen(GatherDataEvent)能自动生成 loot table / 模型 / blockstate / lang,长期项目强烈建议开;本章手写为主,方便照着改。


3. 用 Blockbench(BB)模型

3.1 三种"方块外观"路线,先选对

需求 用什么 BB 导出格式
普通方块,形状在 1×1×1 左右,静态 方块模型 JSON(baked model) Java Block/Item Model
形状超过 1 格、部件很多、要单独贴图/动态 BlockEntityRenderer(BER) Java Block/Item Model(当几何源)或 Modded Entity Model
需要骨骼动画(转动/挥舞) GeckoLib 或自写 BER 动画 Modded Entity Model(GeckoLib)/ Animated Java 插件

一句话:能塞进方块模型就别上 BER;超出了、要动了,才上 BlockEntity。

3.2 BB 建模 → 导出的标准流程

  1. 打开 Blockbench → 新建时选 Java Block/Item Model 模板(不要选 Bedrock 系列!)
  2. Textures 面板导入你的 png(建议在 BB 里直接画或从像素画工具导入)
  3. 摆方块(Add Cube),16 单位 = 1 格,原点在方块西北下角
  4. UV:小模型推荐 Per-face UV(比 Box UV 更省、更可控)
  5. 想调物品拿在手里/在 GUI 里的角度 → Display 面板调(导出会写进 display 段)
  6. File → Export → Java Block/Item Model,导出 JSON
  7. 把导出文件放 assets/mymod/models/block/<名字>.json,贴图放 textures/block/
  8. 手改两处:textures 里的贴图路径加命名空间;需要透明/半透明时加 render_type

3.3 导出后必查的 6 项

  1. textures 键值:BB 导出常写成 "texture": "my_block"(裸文件名),要改成 "mymod:block/my_block",否则紫黑格子
  2. parent:保留 "block/block"(它带方块的基础显示变换与 AO 设置)。
  3. render_type(Forge 1.19+ 支持):模型有透明/镂空必须加,否则透明处发黑或不通透:
    { "render_type": "cutout" }       // 镂空(树叶、栅栏):非全透明剔除
    { "render_type": "translucent" }  // 半透明(玻璃、水)
    
  4. ambientocclusion:模型细节多、想更"平"可以设 false
  5. 尺寸from/to 超出 [-16, 32] 范围时,方块模型会被剔除/裁剪(相邻区块看不到),这时请改走 BER。
  6. 面朝向:法线朝外的面才受光正常;朝里的面会发黑,BB 里用 Flip/重新拉面修正。

3.4 BB 建模注意事项(踩坑合集)

  • 源文件要留.bbmodel 一定保留(后续改模全靠它),别只存导出的 JSON。
  • 纹理尺寸:方块模型建议 16/32/64;128+ 会增加显存与加载时间,复杂模型建议交给 BER 单独贴图。
  • cube 数量:单个方块模型 cube 数建议 < 100;全服铺满时每个 cube 都要进区块渲染,多了直接掉帧。能合面就合面,能用一张贴图就别用十张。
  • 透明与 AO 冲突:半透明模型 + AO 容易出现"黑边/闪烁",可试 "ambientocclusion": false + translucent
  • 不要指望 BB 动画直接跑:BB 里的动画不会自动在 Java 生效;动画要 GeckoLib(或第三方 Animated Java 插件生成代码)。用第三方插件前先确认它支持的 MC/加载器版本。
  • 物品形态别忘 models/item:BB 导出的只是方块模型,方块物品要另写 models/item/<id>.json{"parent": "mymod:block/<id>"}),或者在导出模型里把 display 调好再让它当物品模型。
  • 光照/阴影参数:BB 导出的 shade 默认 true;关掉会有"平涂"效果,看风格决定。
  • 单位/网格:BB 里 1 像素 = 1 单位 = 1/16 格;别用"整格"思维直接填 1、2、3。
  • 模型名与文件名blockstates → models → textures 三层命名保持统一(都用 my_block),出问题时最省排查。
  • 多部件模型:BB 的 "Groups"(组)导出成 Java 模型时会丢掉组名(除非用 BER/GeckoLib),别指望在 Java 里按组操作。

3.5 BB 里的「分组」到底要不要做?

结论:看渲染路线。分两种情况,别一腔热血分了一堆组结果游戏里全丢。

你的做法 分组还有用吗?
导出成普通方块模型 JSON(静态外观) 没用——导出时分组被拍平,JSON 里只剩一个 elements 数组,组名/层级全丢;分组此时只是方便你自己编辑
BER / GeckoLib(要开合、动画、隐藏部件) 必须做——分组 = 骨骼 = 代码里的 ModelPart名字就是接口,改个名字代码就编译不过

做箱子推荐的三个组

组名 内容 备注
base(或 body 箱体 一个组就够
lid 盖子 独立组,Pivot 必须设在铰链上
lock / latch 锁扣、把手 可单独,也可并进 lid

组名用英文小写、无空格;不要用中文/空格/特殊符号(代码里要当标识符用)。

最关键的一步:盖子的旋转原点(Pivot)放在铰链位置

  • 放在盖子中心 → 转起来像翻跟头;放在箱体后上边缘才会像真箱子开盖
  • 操作:BB 里选中 lid 组 → 点 Pivot/Origin 按钮 → 把原点拖到铰链处
  • 例:16×14×16 的箱子、朝北(-Z),铰链大约在 [8, 14.5, 15.5](按你的模型微调)
  • 开合角度:常见做法 0 → -90°(绕 X 轴),用 openness(0~1)做插值;叠加上方块朝向的 y 旋转即可

导出格式怎么选

  • 静态(盖子不开)→ Java Block/Item Model → 放 models/block/<id>.json
  • 要开合/动画 → Modded Entity Model(骨骼会保留,转成 ModelPart)或 GeckoLib 格式; Blockbench 还有第三方插件 Animated Java(能直接生成 Java 代码/资源),用前先确认它支持的 MC / 加载器版本
  • 走 BER 时:方块 getRenderShape() 返回 ENTITYBLOCK_ANIMATED,并注册 BlockEntityRenderer(见 4.5)

再提醒一句:方块四个朝向不要在模型里做四份,交给 blockstatesy 旋转(见 2.1),模型只做朝北那一张。


4. 做一个箱子(BlockEntity + 容器)

"箱子"= 方块 + 方块实体(BlockEntity) + 容器逻辑 + 界面。分四步。

4.1 方块:BaseEntityBlock + 朝向 + 打开界面

public class MyChestBlock extends BaseEntityBlock implements SimpleWaterloggedBlock /* 可选 */ {
    public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;

    public MyChestBlock(Properties props) {
        super(props);
        registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH));
    }

    // 1) 让方块知道自己有 BlockEntity
    @Override
    public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
        return new MyChestBlockEntity(pos, state);
    }

    @Override
    public RenderShape getRenderShape(BlockState state) {
        return RenderShape.ENTITYBLOCK_ANIMATED;   // 交给 BER 渲染(用方块 JSON 模型就写 MODEL)
    }

    // 2) 右键打开界面
    @Override
    public InteractionResult use(BlockState state, Level level, BlockPos pos,
                                 Player player, InteractionHand hand, BlockHitResult hit) {
        if (!level.isClientSide && level.getBlockEntity(pos) instanceof MyChestBlockEntity be) {
            player.openMenu(be);              // be 实现 MenuProvider
        }
        return InteractionResult.sidedSuccess(level.isClientSide());
    }

    // 3) 放置朝向
    @Override
    public BlockState getStateForPlacement(BlockPlaceContext ctx) {
        return defaultBlockState().setValue(FACING, ctx.getHorizontalDirection().getOpposite());
    }

    @Override
    protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
        builder.add(FACING);
    }

    // 4) 被破坏时把里面的东西吐出来(否则物品凭空消失)
    @Override
    public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) {
        if (!state.is(newState.getBlock())) {
            BlockEntity be = level.getBlockEntity(pos);
            if (be instanceof MyChestBlockEntity chest) {
                Containers.dropContents(level, pos, chest);   // 需要 chest 实现 net.minecraft.world.Container
            }
        }
        super.onRemove(state, level, pos, newState, movedByPiston);
    }

    // 5) 比较器输出(原版箱子那样)
    @Override
    public boolean hasAnalogOutputSignal(BlockState state) { return true; }

    @Override
    public int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos) {
        BlockEntity be = level.getBlockEntity(pos);
        return be instanceof MyChestBlockEntity chest
                ? AbstractContainerMenu.getRedstoneSignalFromContainer(chest) : 0;
    }

    @Override
    public BlockState rotate(BlockState state, Rotation rot) { return state.setValue(FACING, rot.rotate(state.getValue(FACING))); }

    @Override
    public BlockState mirror(BlockState state, Mirror mirror) { return state.rotate(mirror.getRotation(state.getValue(FACING))); }

    @Override
    public BlockEntityType<?> getBlockEntityType() { return ModBlockEntities.MY_CHEST.get(); }
}

4.2 注册 BlockEntityType

public class ModBlockEntities {
    public static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITIES =
            DeferredRegister.create(ForgeRegistries.BLOCK_ENTITY_TYPES, MyMod.MODID);

    public static final RegistryObject<BlockEntityType<MyChestBlockEntity>> MY_CHEST =
            BLOCK_ENTITIES.register("my_chest",
                    () -> BlockEntityType.Builder
                            .of(MyChestBlockEntity::new, ModBlocks.MY_CHEST.get())
                            .build(null));   // null = 用默认(方块实体不参与数据修复)
}

4.3 BlockEntity:存东西 + 存盘 + 提供菜单

推荐做法:直接实现原版 Container(这样漏斗、比较器、Containers.dropContents 都能直接吃)。

public class MyChestBlockEntity extends BlockEntity implements Container, MenuProvider {
    public static final int SLOTS = 27;
    private final NonNullList<ItemStack> items = NonNullList.withSize(SLOTS, ItemStack.EMPTY);
    private int openCount;   // 用于盖子动画/音效(可选)

    public MyChestBlockEntity(BlockPos pos, BlockState state) {
        super(ModBlockEntities.MY_CHEST.get(), pos, state);
    }

    // ---------- Container 接口 ----------
    @Override public int getContainerSize() { return SLOTS; }
    @Override public boolean isEmpty() { return items.stream().allMatch(ItemStack::isEmpty); }
    @Override public ItemStack getItem(int slot) { return items.get(slot); }
    @Override public ItemStack removeItem(int slot, int amount) {
        ItemStack s = ContainerHelper.removeItem(items, slot, amount);
        if (!s.isEmpty()) setChanged();
        return s;
    }
    @Override public ItemStack removeItemNoUpdate(int slot) { return ContainerHelper.takeItem(items, slot); }
    @Override public void setItem(int slot, ItemStack stack) {
        items.set(slot, stack);
        if (stack.getCount() > getMaxStackSize()) stack.setCount(getMaxStackSize());
        setChanged();
    }
    @Override public boolean stillValid(Player player) {
        return Container.stillValidBlockEntity(this, player);   // 距离校验
    }
    @Override public void clearContent() { items.clear(); }
    @Override public int getMaxStackSize() { return 64; }

    // ---------- MenuProvider ----------
    @Override public Component getDisplayName() { return Component.translatable("block.mymod.my_chest"); }

    @Override
    public AbstractContainerMenu createMenu(int id, Inventory playerInv, Player player) {
        return new MyChestMenu(id, playerInv, this);
    }

    // ---------- 存盘 ----------
    @Override
    protected void saveAdditional(CompoundTag tag) {
        super.saveAdditional(tag);
        ContainerHelper.saveAllItems(tag, items);
    }

    @Override
    public void load(CompoundTag tag) {
        super.load(tag);
        items.clear();
        ContainerHelper.loadAllItems(tag, items);
    }

    // ---------- 让漏斗能从上面塞、下面抽(可选,进阶)----------
    // 想精细控制就实现 WorldlyContainer,重写 getSlotsForFace / canPlaceItemThroughFace / canTakeItemThroughFace
}

如果更习惯 Forge 的写法,把 NonNullList<ItemStack> 换成 ItemStackHandlernet.minecraftforge.items.ItemStackHandler),再实现 ICapabilityProvider 暴露 CapabilityItemHandler.ITEM_HANDLER_CAPABILITY。代价是:漏斗/比较器/Containers.dropContents 不会自动认识它,需要额外适配。

4.4 菜单 + 界面

Menu(服务端/客户端共用逻辑):

public class MyChestMenu extends AbstractContainerMenu {
    private final Container container;

    // 客户端构造(由 MenuType 调用)
    public MyChestMenu(int id, Inventory playerInv, FriendlyByteBuf extra) {
        this(id, playerInv, new SimpleContainer(MyChestBlockEntity.SLOTS));
    }

    public MyChestMenu(int id, Inventory playerInv, Container container) {
        super(ModMenus.MY_CHEST.get(), id);
        this.container = container;
        container.startOpen(playerInv.player);

        // 箱子本体:3 行 × 9 列
        for (int row = 0; row < 3; row++)
            for (int col = 0; col < 9; col++)
                addSlot(new Slot(container, col + row * 9, 8 + col * 18, 18 + row * 18));

        // 玩家背包
        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));
        for (int col = 0; col < 9; col++)
            addSlot(new Slot(playerInv, col, 8 + col * 18, 142));

        addDataSlots(...);   // 需要同步数字(比如盖子开合进度)时用
    }

    @Override
    public boolean stillValid(Player player) { return container.stillValid(player); }

    // Shift 点击:把物品在箱子和背包之间搬
    @Override
    public ItemStack quickMoveStack(Player player, int index) {
        ItemStack result = ItemStack.EMPTY;
        Slot slot = slots.get(index);
        if (slot.hasItem()) {
            ItemStack stack = slot.getItem();
            result = stack.copy();
            int chestSize = MyChestBlockEntity.SLOTS;
            if (index < chestSize) {
                if (!moveItemStackTo(stack, chestSize, slots.size(), true)) return ItemStack.EMPTY;
            } else {
                if (!moveItemStackTo(stack, 0, chestSize, false)) return ItemStack.EMPTY;
            }
            if (stack.isEmpty()) slot.set(ItemStack.EMPTY); else slot.setChanged();
        }
        return result;
    }

    @Override
    public void removed(Player player) {
        super.removed(player);
        container.stopOpen(player);
    }
}

MenuType 注册

public class ModMenus {
    public static final DeferredRegister<MenuType<?>> MENUS =
            DeferredRegister.create(ForgeRegistries.MENU_TYPES, MyMod.MODID);

    public static final RegistryObject<MenuType<MyChestMenu>> MY_CHEST =
            MENUS.register("my_chest",
                    () -> IForgeMenuType.create(MyChestMenu::new));   // 带 FriendlyByteBuf 的工厂
}

Screen(客户端)

public class MyChestScreen extends AbstractContainerScreen<MyChestMenu> {
    private static final ResourceLocation TEX = new ResourceLocation("mymod", "textures/gui/my_chest.png");

    public MyChestScreen(MyChestMenu menu, Inventory inv, Component title) {
        super(menu, inv, title);
        this.imageWidth = 176;
        this.imageHeight = 166;
        this.inventoryLabelY = 72;
    }

    @Override
    protected void renderBg(GuiGraphics g, float partialTick, int mouseX, int mouseY) {
        g.blit(TEX, leftPos, topPos, 0, 0, imageWidth, imageHeight);
    }

    @Override
    public void render(GuiGraphics g, int mouseX, int mouseY, float partialTick) {
        renderBackground(g);
        super.render(g, mouseX, mouseY, partialTick);
        renderTooltip(g, mouseX, mouseY);
    }
}

注册 Screen(客户端):

// Forge 1.20.1 推荐:mod 事件总线
@Mod.EventBusSubscriber(modid = MyMod.MODID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
public class ClientSetup {
    @SubscribeEvent
    public static void onRegisterScreens(RegisterMenuScreensEvent event) {
        event.register(ModMenus.MY_CHEST.get(), MyChestScreen::new);
    }
}

如果用的 Forge 版本没有 RegisterMenuScreensEvent,就退回 FMLClientSetupEvent 里调 MenuScreens.register(ModMenus.MY_CHEST.get(), MyChestScreen::new)

4.5 可选:RenderShape 与 BER

  • 方块 JSON 模型(静态外观)→ getRenderShape 返回 MODEL,不用 BER。
  • BB 导出的模型 + 自定义渲染(要盖子开合、旋转、超格模型)→ ENTITYBLOCK_ANIMATED + 写 BER:
public class MyChestRenderer implements BlockEntityRenderer<MyChestBlockEntity> {
    public MyChestRenderer(BlockEntityRendererProvider.Context ctx) { }

    @Override
    public void render(MyChestBlockEntity be, float partialTick, PoseStack pose,
                       MultiBufferSource buffers, int packedLight, int packedOverlay) {
        pose.pushPose();
        pose.translate(0.5, 0.0, 0.5);
        // 这里画盖子/箱体:用 ModelPart(实体模型)或自定义 Model,光照用 packedLight
        pose.popPose();
    }
}
@Mod.EventBusSubscriber(modid = MyMod.MODID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
public class ClientSetup {
    @SubscribeEvent
    public static void onRegisterRenderers(EntityRenderersEvent.RegisterRenderers event) {
        event.registerBlockEntityRenderer(ModBlockEntities.MY_CHEST.get(), MyChestRenderer::new);
    }
}

只想要"原版那样的箱子"?最省事的偏方:方块继承 ChestBlock、BlockEntity 继承 ChestBlockEntity,直接白嫖原版的箱子渲染与开合动画(还要实现 ChestBlockEntity.ITickableChest / 提供 ChestBlockEntity.getOpenNess)。缺点是外观/交互被原版逻辑绑死,改造空间小。

4.6 箱子必须补的资源

  • 方块模型(或 BER 用的几何)、blockstates 四个朝向(4.1 的 FACING 对应)
  • 掉落表:箱子本体要能掉(别写把内容物也掉出来的逻辑,内容物由 onRemove 吐)
  • GUI 贴图:assets/mymod/textures/gui/my_chest.png(176×166)
  • 语言:block.mymod.my_chest

5. 自测清单(发版前逐条过)

  • 放下来:朝向对不对(getStateForPlacement
  • 挖掉:掉落自己 + 里面的东西全吐出来onRemove
  • 存盘:退出世界再进,东西还在(saveAdditional/load
  • 漏斗:能从上面塞、从下面抽(实现 WorldlyContainer 或补 capability)
  • 比较器:满箱时信号增强(getAnalogOutputSignal
  • 多人:两个人同时开箱子不崩、stillValid 距离校验生效
  • 数据包/资源包:模型紫黑?九成是 textures 路径或命名空间问题
  • 性能:F3 看渲染、区块里多放几个模型看帧率(cube 太多会很明显)

6. 一页纸速查

方块           DeferredRegister<Block> + BlockBehaviour.Properties + 挂 mod 事件总线
方块物品       BlockItem,id 与方块同名
创造标签页     DeferredRegister<CREATIVE_MODE_TAB> + CreativeModeTab.builder()
资源           blockstates → models/block → textures/block(三层同名)
物品模型       models/item/<id>.json = {"parent": "mymod:block/<id>"}
掉落           data/<modid>/loot_tables/blocks/<id>.json
方块实体       BaseEntityBlock.newBlockEntity + DeferredRegister<BLOCK_ENTITY_TYPE> + save/load
容器           实现 Container(原版兼容最好)/ 或 ItemStackHandler + capability
界面           MenuType(IForgeMenuType) + AbstractContainerMenu + AbstractContainerScreen
BER            BlockEntityRenderer + EntityRenderersEvent.RegisterRenderers
BB 模型        选 Java Block/Item 模板 → 导出 → 改 textures 命名空间 → 需要透明加 render_type
超 1 格/要动    上 BER / GeckoLib

相关章节