跳到主要内容

第五章 第一个方块(Block)

yhz_cake
yhz_cake
一句

ℹ️提示

在这里,我将尽量详细的对写的每一段代码进行讲解,以让你对自己到底在写什么有充分的了解,而不是仅仅的这里应该这样写。

ℹ️提示

本章为合作者提供,作者: yhz_cake

方块与方块实体

在Minecraft中,方块所代表的并非方块本身,而是包含了方块Block方块实体Block Entity两部分。最简单的石头Stone,泥土Dirt等方块,就是普通的方块,它们不包含方块实体,而工作台Crafting Table、熔炉Furnace等,就同时包含方块与方块实体两部分。

方块的注册

现在我们知道了注册的基本流程,现在我们来看看方块的注册:

//Blocks类
public static final Block DIRT = register("dirt", new Block(BlockBehaviour.Properties.of().mapColor(MapColor.DIRT).strength(0.5F).sound(SoundType.GRAVEL)));

这是原版中泥土的注册代码,声明了一个Block类的常量DIRTregisterBlocks类定义的方法,用来在注册表注册方块类。但是我们使用NeoForge进行modding的过程中,在很多的情况下原版的代码是private的,无法直接使用。因此NeoForge提供了一个类用来专门进行注册,包括对原版的注册:

public static final DeferredBlock<Block> TEST_BLOCK = registerBlocks("test_block", () -> new Block(BlockBehaviour.Properties.ofFullCopy(Blocks.DIRT)));

可以看到使用上跟原版是一致的(即使在实现上存在不小的区别)。所有跟原版相关的注册表都可以搜索Deferred来查找,后面也会再次提到。

其中的registerBlocks是一个自定义的方法,主要用来直接注册方块对应的物品,因为只注册了方块我们是无法在游戏中正常使用的,我们没有对应的物品去放置这个方块。如果你想要得到一个简单的对应方块的物品,我们就可以用这样的方法进行简化:

//ModBlocks类
private static <T extends Block> void registerBlockItems(String name, DeferredBlock<T> block) {
ModItems.ITEMS.register(name, () -> new BlockItem(block.get(), new Item.Properties()));
}

private static <T extends Block> DeferredBlock<T> registerBlocks(String name, Supplier<T> block) {
DeferredBlock<T> blocks = BLOCKS.register(name, block);
registerBlockItems(name, blocks);
return blocks;
}

当然如果你对方块对应的物品有额外的想法和设计,参考这个方法也可以只单独地注册方块。

不过你还需要注意注册表需要手动添加进EventBus中,来让NeoForge明白这里有一个注册表需要进行注册:

//你的方块注册类{ModBlocks}
public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(Example_Mod.MOD_ID);
...//这里可以添加你的注册逻辑

public static void register(IEventBus eventBus) {
BLOCKS.register(eventBus);
}

//你的模组主类{Example_Mod}
public Example_Mod(IEventBus modEventBus, ModContainer modContainer) {
...
ModBlocks.register(modEventBus);
...
}

这样就完成了一个简单方块的注册。

方块属性

不过你可能会好奇,BlockBehaviour.Properties是用来干什么的?

这其实就相当于方块的属性,方块的硬度,爆炸抗性等等,都是由Properties控制的,具体的可以在BlockBehaviour.Properties类里自行查看,这里就不作过多介绍了。

更强大的方块

现在,你想要完成一个伟大的使命,想要在Minecraft中添加竖半砖!这是一个前所未有的伟大事业,以至于原版的Block类根本无法支持这项任务,因此你需要对Block类进行修改。但是直接修改原版的Block类很显然是极其愚蠢且不优雅的,其实我们只需要继承Block类就可以创建一个新的方块类而不需要"修改"Block类。

public class ExampleBlock extends Block {
public ExampleBlock(Properties properties) {
super(properties);
...
}
}

以下是一些常用的修改方法:

@Override
public VoxelShape getShape(BlockState state, BlockGetter getter, BlockPos pos, CollisionContext context) {
//此项控制方块的碰撞箱,也就是这个方块实际的"占地面积"
//一个不能换方向的半砖
return Block.box(0, 0, 0, 8, 16, 16);
}

//原版的编解码器写法,此处可借鉴
public static final MapCodec<ExampleBlock> CODEC = simpleCodec(ExampleBlock::new);

@Override
protected MapCodec<? extends ExampleBlock> codec() {
//编解码器,用于编解码json与nbt
return CODEC;
}

@Override
public RenderShape getRenderShape(BlockState state) {
//RenderShape是一个枚举类,一共有三种模型类型
//MODEL就是正常的方块模型,一般都用这个
//ENTITYBLOCK_ANIMATED是方块实体动画,比如箱子
//INVISIBLE是不可见,字面意思
return RenderShape.MODEL;
}

@Override
public void setPlacedBy(Level level, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) {
//根据放置所使用的物品来做出改变
super.setPlacedBy(level, pos, state, placer, stack);
}

@Override
public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) {
//在移除时执行的方法
super.onRemove(state, level, pos, newState, isMoving);
}

@Override
public ItemStack getCloneItemStack(BlockState state, HitResult target, LevelReader level, BlockPos pos, Player player) {
//中键获取的物品
}

现在再在方块注册类中注册,就可以在游戏中看到你的新方块了:

public static final DeferredBlock<ExampleBlock> EXAMPLE_BLOCK = registerBlocks("example_block", () -> new ExampleBlock(BlockBehaviour.Properties.ofFullCopy(Blocks.DIRT)));

方块状态

虽然在刚刚我们简单的写了一个竖半砖,但是它只能在一个位置,这显然跟我们的构想是不符合的,显然我们需要让它能够"旋转"。这就需要使用到方块状态,通过识别在放置时的信息来让竖半砖符合我们的预期。

下面是原版中带釉陶瓦的方块状态代码,可以给我们一些灵感:

//创建一个只在水平方向变化的方向状态,关于如何创建自定义的方块状态标签请参考BlockStateProperties
public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;

@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
//为方块添加方块状态的标签
builder.add(FACING);
}

@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
//根据放置时的玩家方位、点击方块面等设置方块状态
return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
}

@Override
protected BlockState rotate(BlockState state, Rotation rot) {
//从传入的方块状态中返回具有指定旋转的方块状态。如果不适用,则返回传入的方块状态。
return state.setValue(FACING, rot.rotate(state.getValue(FACING)));
}

@Override
protected BlockState mirror(BlockState state, Mirror mirror) {
//从传入的方块状态中返回具有给定镜像的传入方块状态。如果不适用,则返回传入的方块状态。
return state.rotate(mirror.getRotation(state.getValue(FACING)));
}

方块状态的编写就需要自己的想象力了,从这里开始就要加入自己的理解来编写属于自己的奇迹。本教程只能提供基础,让开发者更快地熟悉各种方法属性对应着什么,其他的都只能靠自己来创造。

包含方块实体的方块

以上我们便能写出一个完整的方块了,但如果你希望自己的方块拥有工作台、箱子、熔炉那样的作用的话,只是以上的部分显然还远远不够,这就需要你的方块拥有一个对应的方块实体了。

当然,这时如果只是简单的继承Block类就有点不够了,你可以在继承Block类的同时实现EntityBlock接口,或者简单点直接继承BaseEntityBlock类:

public class ExampleEntityBlock extends Block implements EntityBlock {
}

//或者

public class ExampleEntityBlock extends BaseEntityBlock {
}

以下是一些常见的修改方法:

@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
//指定方块对应的方块实体
return new ExampleEBEntity(pos, state);
//EB就是指代EntityBlock,全写太长了
}

@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> type) {
if (type == ModBlockEntities.EXAMPLE_ENTITY_BLOCK.get()) {
//用于执行方块的刻方法,在Entity类里也会提到
return ExampleEBEntity::tick;
}
return null;
}

@Override
public InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
if (!level.isClientSide()) {
BlockEntity entity = level.getBlockEntity(pos);
if (entity instanceof ExampleEBEntity) {
player.openMenu(state.getMenuProvider(level, pos));
//打开方块对应的GUI,后面会提到GUI的编写
return InteractionResult.CONSUME;
}
}
return InteractionResult.SUCCESS;
}

方块实体的注册

在上面的代码中有一个ModBlockEntities类需要解释一下,在原版中,BlockEntityType类中包含了所有方块与方块实体类的对应关系,在之后的代码编写中这几乎是无法绕开的一环,当然的,其中注册的方法是私有的,导致我们无法直接注册新的方块实体类型。那么当然的,前面提到的DeferredRegister就可以帮助我们注册自己的方块实体类型。它跟方块的注册写法略有不同:

public static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITIES =
DeferredRegister.create(BuiltInRegistries.BLOCK_ENTITY_TYPE, Example_Mod.MOD_ID);

public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<ExampleEBEntity>> EXAMPLE_ENTITY_BLOCK =
BLOCK_ENTITIES.register("example_entity_block", new Supplier<BlockEntityType<ExampleEBEntity>>() {
@Override
public BlockEntityType<ExampleEBEntity> get() {
return BlockEntityType.Builder.of(ExampleEBEntity::new,
ModBlocks.EXAMPLE_ENTITY_BLOCK.get()).build(null);
}
});

当然,记得在方块注册类注册方块,此处不再赘述。

方块实体类

现在所有的准备工作已经基本完成了,接下来就是最重要的一步,entity类的编写。我们以钟为参考,编写一个空白的方块实体框架:

public class ExampleEBEntity extends BlockEntity {
public ExampleEBEntity(BlockPos pos, BlockState blockState) {
super(ModBlockEntities.EXAMPLE_ENTITY_BLOCK.get(), pos, blockState);
}

private static void tick(Level level, BlockPos pos, BlockState state, BlockEntity entity) {
//在ExampleEntityBlock中调用的方法,方块随机刻
if (entity instanceof ExampleEBEntity) {
...
}
}
}

接下来我们需要了解每个方块有哪些地方我们可以实现我们的想法:

一种是单次调用的,比如onHitonBreak等。

另一种也是最主要的,就是tick方法,在接下来会主要讲tick方法的使用,因为它们之间的使用几乎是一致的。

最简单的,我们可以尝试每个随机刻向控制台输出一些自身的信息,比如方块实体的位置:

@Override
public void tick(Level level, BlockPos pos, BlockState state, BlockEntity entity) {
if (entity instanceof ExampleEBEntity) {
System.out.println("ExampleEBEntity tick at " + pos);
}
}

下一章我们将从各个方向来完善方块实体的编写。


yhz_cake于2025年12月14日起稿并于15日暂时中止

yhz_cake修正于2025年12月21日,又继续终止

yhz_cake于2026年4月3日转移至仓库并重新排版

Copyright © 2026 yhz_cake. 保留所有权利。

在明确注明原文出处(包括作者名与原始链接)的前提下,允许非商业性地引用本作品片段。引用内容不得超过原文的 20%,不得歪曲原意或用于误导性语境。整篇转载或复制使用需获得作者授权。本网站所有教程不允许商用,也不会授予商用授权。