这是适用于Minecraft Java版1.21.4的Fabric模组开发系列教程专栏第十四章——方块实体。想要阅读其他内容,请查看或订阅上面的专栏。
方块实体(Block Entity) 指的是一种用于存储方块额外数据的方法。但这种数据和为了控制方块状态而在自定义方块类中创建的属性不太相同,通常方块实体中的数据可能需要进行逻辑处理和计算,且一般不会直接体现在方块上。
不过,本章中创建的带有方块实体的自定义方块始终使用了方块实体中的数据来控制方块外观——光源方块(light_block),玩家通过使用(右键点击)“光源方块”来动态切换其光照强度,每点击一下,方块的光照强度将+1;如果光照强度已经为15,则重置光照强度为0。
要创建“光源方块”,需要按照以下步骤推进:
- 创建方块实体类;
- 创建方块实体注册类;
- 注册方块实体;
- 创建带有方块实体功能的方块类;
- 注册方块并将其添加到物品组中;
- 完成创建方块的其他工作;
- 在方块实体类中添加数据;
- 在方块类中使用方块实体;
创建方块实体类
要创建方块对应的实体,需要创建一个方块实体类,并使其继承BlockEntity
类。
方块实体类BlockEntity
BlockEntity
类一般作为方块实体类的父类,用于保存一个世界中方块的额外数据。通常,方块的数据由预定义的、有限的方块状态数据集合保存。但是,方块需要存储一些不能被预定义的数据,例如箱子里的物品、告示牌上的文字等,方块实体可以存储这些数据。
通常,继承了BlockEntity
类的自定义方块实体类需要调用其构造方法;
public BlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {this.type = type;this.pos = pos.toImmutable();this.validateSupports(state);this.cachedState = state;
}
其中需要传递方块实体类型BlockEntityType
对象、方块位置BlockPos
对象和方块状态BlockState
对象。
方块实体类型类BlockEntityType
BlockEntityType
类用于定义和管理方块实体类型。在创建完实体类后,还需要在游戏注册表中注册实体类,此时将返回一个BlockEntityType
对象,需要在实体类的构造方法中使用。
1.创建com/example/test/block/entity/custom
目录,然后在其中创建LightBlockEntity
并继承BlockEntity
类;
public class LightBlockEntity extends BlockEntity {}
2.声明构造方法,在方法体中调用其父类的构造方法;
public LightBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {super(type, pos, state);
}
稍后我们将修改此处的BlockEntityType
对象。
创建方块实体注册类
方块实体注册类用于将所有方块实体注册到游戏注册表中。一般,其中有三部分内容:
- 封装的
register()
方法:用于注册方块实体,并返回一个BlockEntityType
对象; - 类型为
BlockEntityType<T>
的静态常量:代表已经注册的方块实体。稍后将被对应的方块实体类的构造方法所使用; initialize()
:用于初始化方块实体注册类。需要在入口点类的onInitialize()
方法中调用;
1.在com/example/test/block/entity
目录中创建ModBlockEntities.java
;
public class ModBlockEntities {}
2.声明静态私有方法register()
。根据官方文档,register()
的写法应该如下:
private static <T extends BlockEntity> BlockEntityType<T> register(String name,FabricBlockEntityTypeBuilder.Factory<? extends T> entityFactory,Block... blocks
) {Identifier id = Identifier.of(FabricDocsReference.MOD_ID, name);return Registry.register(Registries.BLOCK_ENTITY_TYPE, id, FabricBlockEntityTypeBuilder.<T>create(entityFactory, blocks).build());
}
方法提供三个参数,分别为注册的方块实体名name
、Fabric方块实体类型构造器工厂内部类FabricBlockEntityTypeBuilder.Factory
对象的表达式和方块对象block
。方法首先调用了Identifier.of`()
方法创建方块实体的标识符对象,其中使用了模组Id和参数中的方块实体名name
,最后返回BlockEntityType
对象,同时也是Registry.register()
方法的返回值,其中传递了注册键类型为方块实体类型Registries.BLOCK_ENTITY_TYPE
、标识符对象以及build()
方法返回的BlockEntityType
对象;
3.声明initialize()
方法,用于初始化方块实体注册类;
public static void initialize() {Test.LOGGER.info("Registering ModBlockEntities !!!");
}
其中可以输出日志信息;
4.在入口点类的onInitialize()
方法中调用ModBlockEntities.initialize()
方法;
public void onInitialize() {//...ModBlockEntities.initialize();//...
}
完成方块实体注册类的初始化。
注册方块实体
现在,我们可以直接在方块实体注册类中将方块实体注册到游戏注册表中。
1.在ModBlockEntities
类中声明静态常量LIGHT_BLOCK_ENTITY
,调用register()
方法对其初始化;
public static final BlockEntityType<LightBlockEntity> LIGHT_BLOCK_ENTITY =register("light", LightBlockEntity::new, ModBlocks.LIGHT_BLOCK);
方法中分别传递了方块实体路径名“light”、Fabric方块实体类型构造器工厂内部类FabricBlockEntityTypeBuilder.Factory
对象的表达式LightBlockEntity::new
,这里使用了方块实体类构造方法的表达式,以及方块对象ModBlocks.LIGHT_BLOCK
;
这里的ModBlocks.LIGHT_BLOCK
指的是方块对象,稍后我们将在方块注册类中声明并对其初始化;
2.完成后,回到方块实体类LightBlockEntity
,修改构造方法;
public LightBlockEntity(BlockPos pos, BlockState state) {super(ModBlockEntities.LIGHT_BLOCK_ENTITY,pos, state);
}
首先删除BlockEntityType<?> type
参数,然后将super()
方法中第一个参数改为刚刚注册完毕的方块实体对象ModBlockEntities.LIGHT_BLOCK_ENTITY
。
创建带有方块实体功能的方块类
与普通方块创建的过程有所不同,创建带有方块实体功能的方块类时,方块类需要继承BlockWithEntity
类并重写相关方法。
1.创建com/example/test/block/custom
目录,在其中创建LightBlock.java
并继承BlockWithEntity
类;
public class LightBlock extends BlockWithEntity {}
2.声明构造方法;
public LightBlock(Settings settings) {super(settings);
}
3.重写getCodec()
方法,处理方块实体中数据的序列化与反序列化;
@Override
protected MapCodec<? extends BlockWithEntity> getCodec() {return createCodec(LightBlock::new);
}
其中调用createCodec()
方法,其中传递抽象方块设置内部类和自定义方块类的函数表达式,此处使用了方块类的构造方法;
4.重写createBlockEntity()
方法,在方法体中调用方块实体类的构造方法,用于在创建方块时完成方块实体的初始化。
@Nullable
@Override
public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {return new LightBlockEntity(pos, state);
}
注册方块并将其添加到物品组中
现在,我们在方块注册类中使用自定义方块类注册方块,这与其他方块的注册方式相同。
1.在方块注册类ModBlocks
中声明静态常量LIGHT_BLOCK
,调用register()
方法对其进行初始化;
public static final Block LIGHT_BLOCK = register("light_block",LightBlock::new,AbstractBlock.Settings.create(),true
);
按顺序依次设置了方块名为“light_block”、抽象方块设置内部类对象和方块对象的函数式LightBlock::new
、抽象方块设置内部类对象并要求注册方块对应的方块物品;
2.在入口点类的onInitialize()
方法中,将方块添加到指定物品组中。
ItemGroupEvents.modifyEntriesEvent(CUSTOM_ITEM_GROUP_KEY).register((itemGroup) -> {...itemGroup.add(ModBlocks.LIGHT_BLOCK.asItem());...});
完成创建方块的其他工作
与创建普通方块相同,我们还要为方块:
- 创建模型文件;
- 创建模型描述文件;
- 创建方块纹理;
- 创建方块状态文件;
- 在语言文件中为其添加翻译键值对;
1.在assets/test/models/block
目录中创建light_block.json
作为方块的模型文件;
{"parent": "minecraft:block/cube_all","textures": {"all": "test:block/light_block"}
}
2.在assets/test/items
目录中创建light_block.json
作为方块的模型描述文件;
{"model": {"type": "minecraft:model","model": "test:block/ice_wood"}
}
3.在assets/test/textures/block
目录中创建方块的纹理图light_block.png
;
4.在assets/test/blockstates
目录中创建light_block.json
作为方块状态文件;
{"variants": {"": {"model": "test:block/light_block"}}
}
5.在zh-cn.json
中添加一条翻译键值对;
{..."item.test.light_block": "光源方块",...
}
启动游戏测试
现在,方块的基础内容以及创建完毕,游戏中应当可以正常显示方块。
1.打开游戏,在创造模式物品栏中找到“光源方块”;
名称、纹理等可以正常显示;
2.将其放置在地面上,可以看到方块模型显示正常;
但是方块现在没有任何功能,我们要通过方块实体为其添加数据。
在方块实体类中添加数据
下面,我们将方块发光时的光照等级数值存储在方块实体中并编写数据变化的逻辑。
1.声明私有变量level
,使其初始值为0,用于存储光照等级;
private int level = 0;
2.为level
变量声明一个getLevels()
方法,用于获取光照等级;
public int getLevels() {return level;
}
3.声明calculateLevels()
方法,用于为光照等级数值添加计算逻辑;
public void calculateLevels() {if(level >= 15){level = 0;}else {level++;}
}
一般方块的最高光照等级为15。因此,右键使用方块时,当光照等级小于15时,光照等级将+1;否则,将光照等级重置为0。
在方块类中使用方块实体
现在,我们可以使用方块实体中的数据来控制“光源方块”的光照强度。
不过,想要控制方块的光照等级,依然需要在方块类中创建用于修改方块状态的属性,然后通过方块实体将已经计算完毕,或者说已经执行完逻辑处理的数据传递给方块属性,从而修改方块的光照强度;
实际上,方块实体中的数据是可以直接使用的。只是在这个例子中,修改方块光源的方法luminance()
由内部类AbstractBlock.Settings
提供,而其中传递的是泛型为BlockState
的函数式接口ToIntFunction
的函数表达式,这使得方块的光照强度只能通过方块状态修改;
所以,我们要先为方块类添加用于修改方块状态的属性,然后再使用方块实体。
整型属性类IntProperty
IntProperty
类用于为方块提供控制方块状态的整型属性。其构造方法已经私有化,所以想要创建一个IntProperty
对象,通常调用其静态方法of()
;
public static IntProperty of(String name, int min, int max) {return new IntProperty(name, min, max);
}
可以看到方法体中直接调用了构造方法,其中需要传递三个参数:
String name
:指定属性名;int min
:指定属性的最小值;int max
:指定属性的最大值;
通过min
和max
变量限制整型数据值的范围。
1.声明静态常量LIGHT_LEVEL
,类型为IntProperty
,调用静态方法IntProperty.of()
对其初始化;
public static final IntProperty LIGHT_LEVEL = IntProperty.of("light_level", 0, 15);
方法中分别设置了属性名为"light_level"以及数值的范围在0~15;
2.重写appendProperties()
方法,将属性LIGHT_LEVEL
添加到方块中;
@Override
protected void appendProperties(StateManager.Builder<Block, BlockState> builder) {builder.add(LIGHT_LEVEL);
}
调用StateManager.Builder
对象的add()
方法,将属性附加到方块中;
3.修改构造方法,在super()
方法中调用AbstractBlock.Settings
对象的luminance()
方法,在构造方块对象时直接修改方块的光照强度;
public LightBlock(Settings settings) {super(settings.luminance(state -> state.get(LIGHT_LEVEL)));
}
在luminance()
方法中传递通过当前方块状态对象的get()
方法返回的光照强度的LIGHT_LEVEL
的值;
4.重写onUse()
方法,在方法体中使用方块实体中的数据来修改当前方块的光照强度LIGHT_LEVEL
;
@Override
protected ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, BlockHitResult hit) {LightBlockEntity lightBlockEntity = (LightBlockEntity) world.getBlockEntity(pos);if(!world.isClient && lightBlockEntity != null) {lightBlockEntity.calculateLevels();world.setBlockState(pos, state.with(LIGHT_LEVEL, lightBlockEntity.getLevels()));}return ActionResult.SUCCESS;
}
首先调用world.getBlockEntity()
方法获取指定方块坐标的方块实体对象,方法中传递一个BlockPos
对象,即当前方块的坐标对象,将返回的方块实体对象存储在lightBlockEntity
中;
接着,在确保当前世界对象为服务器世界对象且方块实体对象不为null的基础上,调用方块实体对象的calculateLevels()
方法,处理光照强度的变化逻辑;
然后调用world.setBlockState()
方法修改当前方块的方块状态,方法中传递要修改的方块坐标对象pos
,以及修改后的方块状态对象,这里调用了state.with()
方法,将当前光照等级修改为方块实体中光照等级lightBlockEntity.getLevels()
的值;
最后返回ActionResult.SUCCESS
,代表操作成功;
5.现在,方块所有功能已经通过方块实体实现,可以再次启动游戏进行测试;
打开游戏,修改世界时间为13000,将“光源方块”放置在地面上,持续右键点击方块,可以发现方块光照强度越来越高。
本章小结
本章详细阐述了方块实体的作用与创建的过程。如果没有方块创建和方块状态等前置知识的支撑,阅读本文的难度较高,还请参考专栏的其他文章。感谢各位的阅读,有兴趣可以订阅此专栏!