本篇教程的视频
本篇教程的源代码
介绍
前面我们写了单方块的作物,除了这种单方块的作物,还有像甘蔗
、仙人掌
这种多方块作物。
这次我们来写一个多方块的作物,参考仙人掌,写一个两个方块高的作物
本次我们也就不涉及源代码的讲解了,其基本逻辑和前面的单方块作物是一样的
作物注册
这里我们先来创建自定义的作物类
创建CornCropBlock类
我们创建一个CornCropBlock
类,继承CropBlock
1 2 3 4 5
| public class CornCropBlock extends CropBlock { public CornCropBlock(Settings settings) { super(settings); } }
|
而后我们来编一些基本的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public static final int FIRST_STAGE_AGE = 7; public static final int SECOND_STAGE_AGE = 1; public static final IntProperty AGE = IntProperty.of("age", 0, 8); private static final VoxelShape[] AGE_TO_SHAPE = new VoxelShape[]{ Block.createCuboidShape(0.0, 0.0, 0.0, 16.0, 2.0, 16.0), Block.createCuboidShape(0.0, 0.0, 0.0, 16.0, 4.0, 16.0), Block.createCuboidShape(0.0, 0.0, 0.0, 16.0, 6.0, 16.0), Block.createCuboidShape(0.0, 0.0, 0.0, 16.0, 8.0, 16.0), Block.createCuboidShape(0.0, 0.0, 0.0, 16.0, 10.0, 16.0), Block.createCuboidShape(0.0, 0.0, 0.0, 16.0, 12.0, 16.0), Block.createCuboidShape(0.0, 0.0, 0.0, 16.0, 14.0, 16.0), Block.createCuboidShape(0.0, 0.0, 0.0, 16.0, 16.0, 16.0), Block.createCuboidShape(0.0, 0.0, 0.0, 16.0, 8.0, 16.0) };
|
这里我们定义了两个阶段的作物,我们这里设置第一格的方块有8
个生长阶段(0-7)
,而第二格的方块有1
个生长阶段(8)
接下来我们定义了作物的AGE
属性,这个参数原版中没有,所以我们需要自己定义
随后我们再定义碰撞箱,因为原来的CropBlock
中只要8个生长阶段,而我们合计有9个生长阶段,所以我们需要重新定义碰撞箱(其实就在最后加一行)
重写方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Override protected VoxelShape getOutlineShape(BlockState state, BlockView world, BlockPos pos, ShapeContext context) { return AGE_TO_SHAPE[state.get(AGE)]; }
@Override public int getMaxAge() { return FIRST_STAGE_AGE + SECOND_STAGE_AGE; }
@Override protected void appendProperties(StateManager.Builder<Block, BlockState> builder) { builder.add(AGE); }
@Override protected IntProperty getAgeProperty() { return AGE; }
|
设定上面的参数之后,我们还需要重写getOutlineShape
、getMaxAge
、appendProperties
、getAgeProperty
这几个方法,这些方法在前面的单方块作物中也提到过
这里的最大生长阶段返回的是两个阶段的生长阶段之和
重写canPlaceAt方法
1 2 3 4 5 6
| @Override protected boolean canPlaceAt(BlockState state, WorldView world, BlockPos pos) { BlockState block = world.getBlockState(pos.down()); return super.canPlaceAt(state, world, pos) || block.isOf(this) && block.get(AGE) == 7; }
|
随后我们再重写canPlaceAt
方法
这里是为了让第二格的作物只能种植在第一格的作物上,而且第二格的作物只能种植在第一格的最后一个生长阶段
也就是说只有第一格的作物长到了7
这个值,第二格的作物才能长。同时,当下面的方块被破坏以后,上面的方块也会被破坏
重写生长逻辑
现在我们还得重写其生长逻辑,因为我们的作物和原来的单方块作物不一样,所以我们需要重新定义生长逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Override public void applyGrowth(World world, BlockPos pos, BlockState state) { int nextAge = this.getAge(state) + this.getGrowthAmount(world); int maxAge = this.getMaxAge(); if (nextAge > maxAge) { nextAge = maxAge; } BlockState upState = world.getBlockState(pos.up()); if (this.getAge(state) == 7 && upState.isOf(Blocks.AIR)) { world.setBlockState(pos.up(), this.withAge(nextAge), Block.NOTIFY_LISTENERS); } else { world.setBlockState(pos, this.withAge(nextAge - 1), Block.NOTIFY_LISTENERS); } }
@Override protected void randomTick(BlockState state, ServerWorld world, BlockPos pos, Random random) { int age = this.getAge(state); float f = getAvailableMoisture(this, world, pos); if (world.getBaseLightLevel(pos, 0) >= 9 && random.nextInt((int)(25.0F / f) + 1) == 0 && age < this.getMaxAge()) { if (age == FIRST_STAGE_AGE) { BlockState upState = world.getBlockState(pos.up()); if (upState.isOf(Blocks.AIR)) { world.setBlockState(pos.up(), this.withAge(age + 1), Block.NOTIFY_LISTENERS); } } else { world.setBlockState(pos, this.withAge(age + 1), Block.NOTIFY_LISTENERS); } } }
|
这里我们重写了applyGrowth
和randomTick
方法
前者是玩家施加肥料时,加速其生长的方法
而后者是随机刻,由游戏的随机刻来决定作物的生长
两者乍一看还挺相似的,只有在一些随机数的取法上有差别
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Override public void applyGrowth(World world, BlockPos pos, BlockState state) { int nextAge = this.getAge(state) + this.getGrowthAmount(world); int maxAge = this.getMaxAge(); if (nextAge > maxAge){ nextAge = maxAge; } BlockState upState = world.getBlockState(pos.up()); if (this.getAge(state) == FIRST_STAGE_AGE && upState.isOf(Blocks.AIR)) { world.setBlockState(pos.up(), this.withAge(nextAge), Block.NOTIFY_LISTENERS); } else { world.setBlockState(pos, this.withAge(nextAge - 1), Block.NOTIFY_LISTENERS); } }
|
这里我们先看一下applyGrowth
方法
maxAge
是我们定义的最大生长阶段,nextAge
是当前生长阶段加上一个随机数(2-5,这是父类中的方法),如果大于最大生长阶段,就设置为最大生长阶段
而后我们判断当前第一格的作物是否处于最后一个生长阶段,并且上面没有阻挡(就判断是否为空气)
如果是,就在第二个方块的位置生成一个新的方块,且方块状态的age
值为8
否则就在当前方块的位置更新方块,且方块状态的age
值为nextAge - 1
(这是为了确保age
的值不大于7,我们可不希望第一格的方块长到第8个阶段吧)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Override protected void randomTick(BlockState state, ServerWorld world, BlockPos pos, Random random) { int age = this.getAge(state); float f = getAvailableMoisture(this, world, pos); if (world.getBaseLightLevel(pos, 0) >= 9 && random.nextInt((int) (25.0F / f) + 1) == 0 && age < this.getMaxAge()) {
if (age == FIRST_STAGE_AGE) { BlockState blockState = world.getBlockState(pos.up()); if (blockState.isOf(Blocks.AIR)) { world.setBlockState(pos.up(), this.withAge(age + 1), Block.NOTIFY_LISTENERS); } } else { world.setBlockState(pos, this.withAge(age + 1), Block.NOTIFY_LISTENERS); } } }
|
而后我们再看randomTick
方法
这里我们先获取当前方块的生长阶段age
,然后获取当前方块的湿度f
(这也是父类中的方法)
接着我们判断当前世界环境的光照是否大于等于9(毕竟作物生长需要光照吧),外带一个神奇的随机判断(25.0F / f) + 1
(也是父类的),并且当前方块的生长阶段是否小于最大生长阶段
下面的方法就是和我们差不多的,只是我们在设置方块状态时,给它的设置得加1(不然作物可不会长)
注册种子物品
我们还得重写它获取种子的方法,和之前一样的
但在此之前,得先注册一个种子
1 2
| public static final Item CORN_SEEDS = registerItems("corn_seeds", new AliasedBlockItem(ModBlocks.CORN_CROP, new Item.Settings()));
|
随后我们再重写getSeedsItem
方法
1 2 3 4
| @Override protected ItemConvertible getSeedsItem() { return ModItems.CORN_SEEDS; }
|
这样我们的作物类就写完了
注册CornCropBlock
1 2
| public static final Block CORN_CROP = Registry.register(Registries.BLOCK, Identifier.of(TutorialMod.MOD_ID, "corn_crop"), new CornCropBlock(AbstractBlock.Settings.copy(Blocks.WHEAT)));
|
我们注册了一个CORN_CROP
,实例化CornCropBlock
,接着把WHEAT
的属性copy过来
注册食物
我们这里只写个种子物品,战利品列表的话还得有个食物
1
| public static final FoodComponent CORN = new FoodComponent.Builder().nutrition(8).saturationModifier(0.4f).build();
|
先注册其食物组件,然后再注册物品时
1
| public static final Item CORN = registerItems("corn", new Item(new Item.Settings().food(ModFoodComponents.CORN)));
|
这样我们写战利品列表的时候就好了
加入物品栏
最后我们的食物和种子还得加入物品栏
1 2
| entries.add(ModItems.CORN_SEEDS); entries.add(ModItems.CORN);
|
渲染层设置
别忘了,如果材质贴图有透明区域,还得设置渲染层
一个是Mixin
的方法,到我们之前写的RenderLayersMixin
中写
1
| BLOCKS.put(ModBlocks.CORN_CROP, RenderLayer.getCutout());
|
另一种是在TutorialModClient
写
1
| BlockRenderLayerMap.INSTANCE.putBlock(ModBlocks.CORN_CROP, RenderLayer.getCutout());
|
这样透明的区域就不会变成黑色的了
数据文件
语言文件
1 2
| translationBuilder.add(ModItems.CORN_SEEDS, "Corn Seeds"); translationBuilder.add(ModItems.CORN, "Corn");
|
模型文件
那么前面也有人问过,材质是十字交叉的作物怎么写?
我们之前的作物是四面围合的,但像甘蔗
、甜浆果
这种是十字交叉的,其实这也好实现,去参考一下源代码就好了
1 2 3 4 5 6
| blockStateModelGenerator.blockStateCollector.accept( VariantsBlockStateSupplier.create(ModBlocks.CORN_CROP) .coordinate(BlockStateVariantMap.create(CornCropBlock.AGE) .register(stage -> BlockStateVariant.create() .put(VariantSettings.MODEL, blockStateModelGenerator.createSubModel( ModBlocks.CORN_CROP, "_stage" + stage, Models.CROSS, TextureMap::cross)))));
|
这里我们写了一长串,但其实就是把源代码搬了过来,然后改成了我们的作物
在最后的Models.CROSS
、TextureMap::cross
也就设置了我们作物最后的模型文件是十字交叉型的
战利品列表
1 2 3 4
| LootCondition.Builder builder3 = BlockStatePropertyLootCondition.builder(ModBlocks.CORN_CROP) .properties(StatePredicate.Builder.create().exactMatch(CornCropBlock.AGE, 8)); addDrop(ModBlocks.CORN_CROP, cropDrops( ModBlocks.CORN_CROP, ModItems.CORN, ModItems.CORN_SEEDS, builder3));
|
这个是和前面一样的,无非自己再写一个builder3
,然后在addDrop
中加上builder3
就好了
这样我们的多方块作物就写完了,跑好数据生成之后,我们就可以进入我们的游戏中观察了