我的世界Java版1.21.4的Fabric模组开发教程(十三)自定义方块状态
这是适用于Minecraft Java版1.21.4的Fabric模组开发系列教程专栏第十三章——自定义方块状态。想要阅读其他内容,请查看或订阅上面的专栏。
当方块的功能足够复杂,需要配置更多的方块状态,以至于Minecraft API提供的自定义方块类中的功能已经无法满足时,就需要手动创建自定义方块类并提供相关属性来设置方块的状态。
一般,我们需要在自定义方块类中定义引发方块状态修改的属性,然后在特定的情况修改这些属性,从而修改方块的状态。例如,在本章我们将创建一个带有自定义方块状态的方块——烟花发射器(fireworks_emitter),首次放置方块后,右键使用方块,令方块变为激活状态,然后向空中不停发射烟花;再次右键使用方块,令方块变为非激活状态,停止发射烟花。
想要创建“烟花发射器”方块,需要按照以下步骤推进:
- 创建自定义方块类;
- 创建用于修改方块状态的属性;
- 定义修改属性的情况;
- 为方块创建模型文件、模型描述文件和方块纹理;
- 创建方块状态定义文件;
- 使用自定义方块类注册方块并将其添加到物品组中;
- 在语言文件中为方块添加翻译键值对;
- 解决方块朝向问题;
- 渲染烟花;
在开始前,请确保已经掌握了方块创建的基本流程。如有需要,请参考我的世界Java版1.21.4的Fabric模组开发教程(十一)创建方块。
创建自定义方块类
当需要创建足够复杂的方块时,在方块注册类中直接注册方块已经不能为方块添加更多功能。这时就需要创建属于这个方块的方块类,然后在注册时使用自定义方块类完成方块的注册。
在com/example/test
目录下创建block
文件夹,用于存放自定义方块类。在block
文件夹中创建FireworksEmitterBlock.java
,并作为“烟花发射器”的自定义方块类;
使其继承Block
类并实现构造方法;
public class FireworksEmitterBlock extends Block {public FireworksEmitterBlock(Settings settings) {super(settings);}
}
一般,自定义方块类都需要继承Block
类,以此为方块增加更多功能。
创建用于修改方块状态的属性
我们为“烟花发射器”添加一个布尔类型的属性ACTIVATED
。当ACTIVATED
为true
时,“烟花发射器”处于激活状态,将不停向空中发射烟花;当ACTIVATED
为false
时,“烟花发射器”处于非激活状态,即默认状态,将停止发射烟花。
布尔类型属性类BooleanProperty
与EnumProperty
类似,BooleanProperty
类用于为方块提供控制方块状态的布尔型属性。由于其构造方法已被私有化,我们只能调用其静态方法of()
来创建其对象。of()
方法中调用了BooleanProperty
类的构造方法;
public static BooleanProperty of(String name) {return new BooleanProperty(name);
}
方法传递一个字符串name
,即属性名,用于在方块状态定义文件中使用。
1.在FireworksEmitterBlock
类中声明静态常量ACTIVATED
,调用BooleanProperty.of()
方法对其初始化;
public static final BooleanProperty ACTIVATED = BooleanProperty.of("activated");
设置属性名为“activated”;
2.此外,我们还要设置ACTIVATED
属性的默认值为false
,即放置方块时,方块应当处于非激活状态。我们可以在构造方法中设置方块的默认状态;
在构造方法中调用setDefaultState()
方法,设置方块的默认状态;
public FireworksEmitterBlock(Settings settings) {super(settings);setDefaultState(getDefaultState().with(ACTIVATED, false));
}
setDefaultState()
方法中传递一个BlockState
对象,这里可以直接调用getDefaultState()
方法获取当前方块的默认状态对象,然后调用with()
方法设置属性ACTIVATED
的默认值为false
。
方块状态类BlockState
BlockState
类继承了AbstractBlock.AbstractBlockState
内部类,用于代表当前方块的状态对象。通常,我们调用其对象的方法均继承自其顶级超类State
,例如:
get(Property<T> property)
:用于获取指定属性的值,传递一个Property
对象;with(Property<T> property, V value)
:用于修改指定属性的值,方法传递两个参数;Property<T> property
:需要修改属性的对象;V value
:属性的新值;
在后续章节中用到的方法将在此处补充。
定义修改属性的情况
现在,我们令ACTIVATED
属性在玩家使用(右键点击)方块时取反,即玩家最开始使用方块时设置方块状态为“激活”(ACTIVATED=true
),再次使用方块时设置方块状态为“非激活”(ACTIVATED=false
)。
当玩家使用(右键点击)方块时,继承自Block
类中的onUse()
方法会被调用,所以我们要在自定义方块类中重写此方法;
1.在FireworksEmitterBlock
类中重写onUse()
方法;
protected ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, BlockHitResult hit) {...}
方法提供了几个参数;
BlockState state
:方块状态对象;World world
:世界对象;BlockPos pos
:方块位置对象;PlayerEntity player
:玩家实体对象;BlockHitResult hit
:方块碰撞结果对象;
对于部分API的用法,请参考我的世界Java版1.21.4的Fabric模组开发教程(七)创建自定义魔咒效果。
2.在onUse()
方法中调用world.setBlockState()
方法设置方块状态,然后调用world.playSound()
方法设置使用方块时发出的声音;
@Override
protected ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, BlockHitResult hit) {world.setBlockState(pos, state.with(ACTIVATED, !state.get(ACTIVATED)));world.playSound(player, pos, SoundEvents.BLOCK_COMPARATOR_CLICK, SoundCategory.BLOCKS, 1.0F, 1.0F);return ActionResult.SUCCESS;
}
在setBlockState()
方法中首先传递要修改状态的方块坐标,然后调用state.with()
方法传递一个修改后的BlockState
对象,with()
方法中首先传递要修改的属性,然后调用state.get()
方法返回当前的属性值并取反,作为属性的新值;
在playSound()
方法中依次设置玩家对象为玩家本人、方块位置对象为当前方块、要播放的声音事件为红石比较器在切换模式或状态时发出的声音SoundEvents.BLOCK_COMPARATOR_CLICK
、声音类别为方块类声音SoundCategory.BLOCKS
、音量为1和音高为1;
最后返回操作结果ActionResult.SUCCESS
为成功。
为方块创建模型文件、模型描述文件和方块纹理
与常规物品和方块的创建流程相同,“烟花发射器”同样需要模型文件、模型描述文件以及方块的纹理。不过与其他物品和方块相比,“烟花发射器”的模型稍显复杂。
我们约定,除了“烟花发射器”的顶部纹理之外,其他面使用的纹理都是相同的。方块处于激活状态时,顶部纹理不变,但方块其余面的纹理将发生变化;
1.在assets/test/models/block
目录中创建fireworks_emitter.json
,作为默认(非激活)状态下方块的模型;
{"parent": "minecraft:block/cube_top","textures": {"up": "test:block/fireworks_emitter_top","side": "test:block/fireworks_emitter_round"}
}
这里需要使用block/cube_top
父模型。使用block/cube_top
模型创建的方块,顶部和其他面的纹理可以是不同的,完全与“烟花发射器”方块的功能需求相吻合。根据Minecraft API中cube_top
模型的约定,方块顶部使用一个纹理,配置项为"up"
;方块除顶部外其余面使用同一个纹理,配置项为"side"
。
然后再创建fireworks_emitter_activated.json
,作为激活状态下方块的模型;
{"parent": "minecraft:block/cube_top","textures": {"up": "test:block/fireworks_emitter_top","side": "test:block/fireworks_emitter_activated"}
}
稍后我们马上为方块添加fireworks_emitter_top
、fireworks_emitter_round
和fireworks_emitter_activated
纹理;
2.在assets/test/items
目录中创建创建fireworks_emitter.json
,代表方块的模型描述文件;
{"model": {"type": "minecraft:model","model": "test:block/fireworks_emitter"}
}
与其他物品和方块的模型描述文件大致相同;
3.在assets/test/textures/block
目录中创建fireworks_emitter_round.png
,作为方块除顶部以外其他面的纹理;
然后再创建fireworks_emitter_top.png
,作为方块顶部纹理;
最后创建fireworks_emitter_activated.png
,作为方块激活时,除顶部外其他面的纹理。
创建方块状态定义文件
现在,我们根据自定义方块类中的属性,来定义指定方块状态下的方块模型,即方块的外观。
在assets/test/blockstates
目录中创建fireworks_emitter.json
,作为方块的状态定义文件;
{"variants": ["activated=true":{"model": "test:block/fireworks_emitter_activated"},"activated=false":{"model": "test:block/fireworks_emitter"}]
}
使用"variants"
配置项,其中添加两种变种,分别设置当属性ACTIVATED=true
时(方块为激活状态)显示的模型为test:block/fireworks_emitter_activated
,反之(方块为非激活状态)则显示test:block/fireworks_emitter
。
使用自定义方块类注册方块并将其添加到物品组中
现在,方块的大部分配置文件已经编写完毕,可以在游戏注册表中注册方块并且将其添加到创造模式物品栏中指定的物品组中了。
1.在方块注册类中声明静态常量FIREWORKS_EMITTER
,调用register()
方法对其初始化;
public static final Block FIREWORKS_EMITTER = register("fireworks_emitter",FireworksEmitterBlock::new,AbstractBlock.Settings.create().sounds(BlockSoundGroup.STONE),true
);
方法中依次传递方块名为“fireworks_emitter”、抽象方块设置内部类和自定义方块类的函数表达式FireworksEmitterBlock::new
、抽象方块设置内部类对象,其中设置了交互声音为石头音效BlockSoundGroup.STONE
,以及要求注册方块对应的方块物品。
2.在入口点类的onInitialize()
方法中,将方块添加到指定物品组中;
ItemGroupEvents.modifyEntriesEvent(CUSTOM_ITEM_GROUP_KEY).register((itemGroup) -> {//...itemGroup.add(ModBlocks.FIREWORKS_EMITTER.asItem());//...});
在语言文件中为方块添加翻译键值对并启动游戏测试
在启动游戏前,我们为方块添加中文翻译。
1.打开语言文件zh-cn.json
,添加一条键值对;
{..."item.test.fireworks_emitter": "烟花发射器",...
}
2.现在我们启动游戏检验目前方块已经实现的功能,稍后我们将完善方块的朝向问题、烟花渲染等更多功能。
启动游戏,在创造模式物品栏中找到“烟花发射器”;
可以看到方块名称和纹理显示正常;
3.将其放置在地面上,可以看到模型显示正常;
右键点击(使用)方块,可以发现方块外观发生改变,进入激活状态;再次右键点击方块,可以取消激活状态;
但现在方块始终朝北,朝向并不会因为玩家的朝向而自行旋转。
解决方块朝向问题
想让方块摆放时朝向玩家,依然需要为方块添加更多用于控制方块状态的属性。对于此功能,我们可以参考原版游戏中“熔炉”方块提供的解决方案,即在自定义方块类中添加FACING
属性,然后在状态定义文件添加更加复杂的变种。
1.在FireworksEmitterBlock
类中声明静态常量FACING
,类型为EnumProperty<Direction>
;
public static final EnumProperty<Direction> FACING = Properties.HORIZONTAL_FACING;
使用Properties.HORIZONTAL_FACING
对其初始化。HORIZONTAL_FACING
常量指定是方块在水平方向上(X轴和Z轴)的朝向,其中包含四个方向的枚举常量Direction.EAST
、Direction.SOUTH
、Direction.WEST
和Direction.NORTH
,分别代表东南西北四个方向,在方块状态定义文件中使用小写字母表示。
2.编辑方块状态定义文件,将方块朝向属性添加到文件中。
打开resources/assets/test/blockstates/fireworks_emitter.json
,将facing
属性和activated
属性组合起来,创建新的变种。加入facing
属性后,变种的数量会变得很多,因为要考虑到方块以东、南、西、北四个方向的摆放情况;
{"variants": {"facing=east,activated=false": {"model": "test:block/fireworks_emitter","y": 90},"facing=east,activated=true": {"model": "test:block/fireworks_emitter_activated","y": 90},"facing=north,activated=false": {"model": "test:block/fireworks_emitter"},"facing=north,activated=true": {"model": "test:block/fireworks_emitter_activated"},"facing=south,activated=false": {"model": "test:block/fireworks_emitter","y": 180},"facing=south,activated=true": {"model": "test:block/fireworks_emitter_activated","y": 180},"facing=west,activated=false": {"model": "test:block/fireworks_emitter","y": 270},"facing=west,activated=true": {"model": "test:block/fireworks_emitter_activated","y": 270}}
}
当方块朝向为东、南、和西时,方块需要分别沿Y轴旋转90度、180度和270度,且每个朝向都要给出方块激活和非激活两种情况。
3.状态定义文件只能根据变种名完成模型的切换,并不能动态计算游戏中玩家的朝向和位置等信息。我们还需要在自定义方块类中完成相关的逻辑处理;
在FireworksEmitterBlock
类中重写getPlacementState()
方法,方法中提供了一个ItemPlacementContext
对象当玩家右键放置一个方块时,此方法将被调用,并返回方块的默认状态;
public BlockState getPlacementState(ItemPlacementContext ctx) {return this.getDefaultState().with(FACING, ctx.getHorizontalPlayerFacing());
}
物品放置上下文类ItemPlacementContext
ItemPlacementContext
类用于提供玩家放置方块或物品时的上下文信息,继承了ItemUsageContext
类。一般作为getPlacementState()
方法中的参数。ItemPlacementContext
类的对象可以用于获取方块放置后的初始状态信息。例如,可以调用:
getVerticalPlayerLookDirection()
:用于获取玩家垂直方向(上下)的视角朝向;getHorizontalPlayerFacing()
:用于根据旋转角度获取玩家的当前水平朝向。方法继承自其父类ItemUsageContext
。
方法中首先调用了getDefaultState()
方法获取当前方块的默认状态,然后调用with()
方法,将FACING
属性的值改为方法getHorizontalPlayerFacing()
返回的玩家当前的朝向,是一个Direction
对象。随后根据方块状态定义文件中的配置,来决定方块最终的朝向。
如果发现方块与玩家同向,即方块正面与玩家同时朝着一个方向,可能还需要继续调用Direction
类对象的getOpposite()
方法,将朝向翻转。
现在我们启动游戏,方块朝向正确,即与玩家朝向相反。
渲染烟花
最后,也是方块最重要的功能便是烟花渲染。在Minecraft中,烟花是一种实体,且烟花的一些属性,包括飞行速度、爆炸效果、形状等均需要使用数据组件来设置。
总的来说,想要渲染烟花,需要按照以下步骤推进:
- 创建烟花爆炸组件对象;
- 创建烟花组件对象;
- 创建烟花实体对象;
- 将实体渲染到世界中;
- 配置计划性方块更新;
1.在FireworksEmitterBlock
类中声明静态私有方法spawnFirework()
,用于创建烟花实体;
private static void spawnFirework(World world, BlockPos pos) {}
提供两个参数分别代表烟花渲染的世界对象world
和烟花渲染的位置pos
;
烟花爆炸组件记录类FireworkExplosionComponent
FireworkExplosionComponent
以记录类的形式声明,用于定义单个烟花爆炸时的所有视觉和行为属性,是创建定制化烟花效果必不可少的API。想要创建FireworkExplosionComponent
类的对象,需要调用其构造方法;
public record FireworkExplosionComponent(FireworkExplosionComponent.Type shape, IntList colors, IntList fadeColors, boolean hasTrail, boolean hasTwinkle)implements TooltipAppender {...}
记录类头中的参数会在类完成编译后自动生成对应的构造方法,其中的参数有:
FireworkExplosionComponent.Type shape
:指定烟花形状,需要传递枚举内部类FireworkExplosionComponent.Type
中的枚举常量,包括小型球状SMALL_BALL
、大型球状LARGE_BALL
、星星形状STAR
、苦力怕脸形状CREEPER
和爆炸形状BURST
;IntList colors
:指定烟花爆炸后的颜色。可以提供一组16进制RGB颜色码的集合,需要传递IntList.of()
方法返回的对象;IntList fadeColors
:指定烟花爆炸后的渐变色,即爆炸后淡出的颜色。传递与上一个参数的形式相同的对象;boolean hasTrail
:指定是否有拖尾效果,即烟花飞向空中时的轨迹。直接传递布尔型数据。boolean hasTwinkle
:指定是否有烟花爆炸后的闪烁效果。直接传递布尔型数据。
2.创建名为explosion
的烟花爆炸组件FireworkExplosionComponent
对象,使用其构造方法完成对象的实例化过程;
FireworkExplosionComponent explosion = new FireworkExplosionComponent(FireworkExplosionComponent.Type.BURST,IntList.of(0x00FFFF, 0xFF69B4, 0xFFFF00, 0xFFFFFF), // Cyan, Pink, Yellow, WhiteIntList.of(),true,true);
构造方法中的参数按顺序设置了烟花形状为爆炸形状FireworkExplosionComponent.Type.BURST
、烟花颜色由青色0x00FFFF
、粉色0xFF69B4
、黄色0xFFFF00
和白色0xFFFFFF
四种颜色构成、无渐变色、需要拖尾以及爆炸闪烁效果。
烟花组件记录类FireworksComponent
FireworksComponent
同样以记录类的形式声明,用于存储烟花的全局属性和完整配置。FireworksComponent
的构造方法中同样可以设置烟花的相关属性,但不会和FireworkExplosionComponent
类的构造方法设置的内容一样具体而复杂。其构造方法中只有两个参数;
public record FireworksComponent(int flightDuration, List<FireworkExplosionComponent> explosions) implements TooltipAppender {...}
同样地,其构造方法将于类编译完成后自动生成,参数包括:
int flightDuration
:指定烟花的飞行时间。单位:秒,取值范围在0~3之间;List<FireworkExplosionComponent> explosions
:指定烟花爆炸组件对象列表。用于设置烟花其他具体的效果,一般传递List.of()
返回的值;
FireworksComponent
的对象将在最终设置烟花时添加到一个ItemStack
对象中,供FireworkRocketEntity
使用。
3.创建名为fireworksComponent
的烟花组件FireworksComponent
对象,使用其构造方法完成对象的实例化过程;
FireworksComponent fireworksComponent = new FireworksComponent(1, // Flight durationList.of(explosion)
);
构造方法中分别设置了烟花飞行时间为1秒和烟花爆炸组件对象列表,列表中添加了刚刚创建FireworkExplosionComponent
对象;
4.想要将FireworksComponent
对象中的设置应用到烟花实体上,还需要创建一个物品堆叠ItemStack
对象作为媒介,因为FireworkRocketEntity
类的构造方法中需要一个带有数据组件设置的ItemStack
对象来配置烟花的具体行为和属性;
创建名为stack
的ItemStack
对象,使用其构造方法完成对象的实例化过程;
ItemStack stack = new ItemStack(Items.FIREWORK_ROCKET);
传递一个Items.FIREWORK_ROCKET
常量作为参数,代表物品堆叠对象的类型。然后调用set()
方法设置物品堆叠对象应用的数据组件;
stack.set(DataComponentTypes.FIREWORKS, fireworksComponent);
方法参数列表中设置了数据组件类型为烟花DataComponentTypes.FIREWORKS
并应用了烟花组件对象fireworksComponent
。
烟花实体类FireworkRocketEntity
FireworkRocketEntity
类用于创建烟花实体本身,也就是最终需要渲染到世界中的烟花实体。其中提供了烟花其他属性和行为的配置空间,即需要间接地用到已经创建好的FireworksComponent
和FireworkExplosionComponent
对象。想要创建FireworkRocketEntity
对象,同样需要调用其构造方法;
public FireworkRocketEntity(World world, double x, double y, double z, ItemStack stack) {super(EntityType.FIREWORK_ROCKET, world);...
}
其中提供的参数包括:
World world
:用于指定需要渲染烟花实体的世界对象;double x
:指定将烟花实体渲染到世界中的X轴坐标;double y
:指定将烟花实体渲染到世界中的Y轴坐标;double z
:指定将烟花实体渲染到世界中的Z轴坐标;ItemStack stack
:指定物品堆叠对象,对象中需要用到设置好的烟花组件,即FireworksComponent
对象。
5.创建名为firework
的烟花实体FireworkRocketEntity
对象,使用其构造方法完成对象的实例化过程;
FireworkRocketEntity firework = new FireworkRocketEntity(world,pos.getX() + 0.5, pos.getY() + 1.0, pos.getZ() + 0.5, stack);
构造方法中按顺序设置了需要渲染烟花的世界对象world
,以及烟花渲染的位置坐标为pos.getX() + 0.5
、pos.getY() + 1.0
、pos.getZ() + 0.5
,在坐标上添加的形如1.0或0.5的偏移量是为了能让烟花在方块正上方渲染,最后传递一个带有烟花组件设置的stack
对象。
6.调用world.spawnEntity()
方法将烟花实体渲染到世界中;
world.spawnEntity(firework);
传递一个烟花实体对象firework
。
7.在onUse()
方法中调用spawnFirework()
,传递当前世界对象world
和方块位置对象pos
;
@Override
protected ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, BlockHitResult hit) {//...spawnFirework(world,pos);return ActionResult.SUCCESS;
}
8.我们可以设置在激活方块后使方块发光。首先在FireworksEmitterBlock
类中声明静态方法getLuminance()
,提供一个BlockState
对象的参数;
public static int getLuminance(BlockState currentBlockState) {return currentBlockState.get(FireworksEmitterBlock.ACTIVATED) ? 10 : 0;
}
调用get()
方法获取ACTIVATED
属性的值,如果为true
则返回光照强度为10,否则返回0;
然后在方块注册类中方块注册FIREWORKS_EMITTER
静态常量中调用luminance()
方法设置方块的光照强度;
public static final Block FIREWORKS_EMITTER = register("fireworks_emitter",FireworksEmitterBlock::new,AbstractBlock.Settings.create().sounds(BlockSoundGroup.STONE).luminance(FireworksEmitterBlock::getLuminance),true
);
方法中传递FireworksEmitterBlock
类getLuminance()
方法的表达式;
现在可以启动游戏,检验代码是否生效。打开游戏,放置方块,右键使用方块;
可以看到方块激活后能够正常发光,烟花可以正常发射,烟花的设置没有问题。但方块激活后,只发射了一个烟花,并没有持续的发射烟花。
想要在方块激活后不停的发射烟花,可以重写scheduledTick()
方法,来创建计划性方块更新行为。
1.在FireworksEmitterBlock
类中,重写scheduledTick()
方法;
@Override
protected void scheduledTick(BlockState state, ServerWorld world, BlockPos pos, Random random) {...}
方法提供了方块状态对象state
、服务器世界对象world
和随机数对象random
;
2.在方法中首先调用服务器世界对象的getBlockState()
方法重新获取当前方块的状态,存储到newState
对象中,由于方块状态更新的异步性,这里必须使用新获取的对象,而不是参数中提供的state
;
BlockState newState = world.getBlockState(pos);
方法传递方块位置对象pos
;
3.获取ACTIVATED
属性的值,判断当前方块的状态;
if (newState.get(ACTIVATED)) {spawnFirework(world, pos);world.scheduleBlockTick(pos, this, 20);
}
如果方块状态为已激活,则调用spawnFirework(world, pos)
方法,向空中发射一个烟花,然后调用scheduleBlockTick()
方法再次计划周期性更新行为,其中依次传递方块位置对象pos
、当前方块对象和执行间隔为20游戏刻;
当scheduleBlockTick()
方法被调用时,类中重写的scheduledTick()
方法就会被调用;
4.在onUse()
方法中调用scheduleBlockTick()
,使方块被使用时添加计划性更新行为;
@Override
protected ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, BlockHitResult hit) {world.setBlockState(pos, state.with(ACTIVATED, !state.get(ACTIVATED)));world.playSound(player, pos, SoundEvents.BLOCK_COMPARATOR_CLICK, SoundCategory.BLOCKS, 1.0F, 1.0F);world.scheduleBlockTick(pos, this, 20);return ActionResult.SUCCESS;
}
现在,当方块处于激活状态时,方块正上方会每隔20游戏刻(1秒)渲染一个烟花并发射升空,直到方块处于未激活状态。
再次启动游戏测试,方块功能已全部实现。
本章小结
本章详细阐述了创建带有自定义方块状态的方块的详细步骤,同时也提供了烟花渲染的标准代码。本章篇幅长,难度较大,是方块状态的进阶级教程。感谢各位的阅读,有兴趣可以订阅此专栏!