本篇教程的视频

本篇教程的源代码

Github地址:TutorialMod-Crop-1.21

介绍

前面我们写了单方块的作物,除了这种单方块的作物,还有像甘蔗仙人掌这种多方块作物。

这次我们来写一个多方块的作物,参考仙人掌,写一个两个方块高的作物

本次我们也就不涉及源代码的讲解了,其基本逻辑和前面的单方块作物是一样的

作物注册

这里我们先来创建自定义的作物类

创建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;
}

设定上面的参数之后,我们还需要重写getOutlineShapegetMaxAgeappendPropertiesgetAgeProperty这几个方法,这些方法在前面的单方块作物中也提到过

这里的最大生长阶段返回的是两个阶段的生长阶段之和

重写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);
}
}
}

这里我们重写了applyGrowthrandomTick方法

前者是玩家施加肥料时,加速其生长的方法

而后者是随机刻,由游戏的随机刻来决定作物的生长

两者乍一看还挺相似的,只有在一些随机数的取法上有差别

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.CROSSTextureMap::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就好了

这样我们的多方块作物就写完了,跑好数据生成之后,我们就可以进入我们的游戏中观察了