本篇教程的视频

本篇教程的源代码

GitHub地址:TutorialMod-TwoHighCrop-1.20.1

本篇教程目标

  • 理解作物类方块中剩余方法
  • 学会自定义多方块作物

查看源代码

那么这期教程我们先来看看CropBlock类中剩下的一些方法

1
2
3
4
5
6
7
8
public final boolean isMature(BlockState blockState) {
return this.getAge(blockState) >= this.getMaxAge();
}

@Override
public boolean hasRandomTicks(BlockState state) {
return !this.isMature(state);
}

isMature方法用于判断作物是否成熟,当作物达到最大生长阶段之后,即为成熟

结合下面的hasRandomTicks方法,我们可以知道,当作物处于成熟阶段时,作物就没有了随机刻,也就不会再生长

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void randomTick(BlockState state, ServerWorld world, BlockPos pos, Random random) {
if (world.getBaseLightLevel(pos, 0) >= 9) {
int i = this.getAge(state);
if (i < this.getMaxAge()) {
float f = getAvailableMoisture(this, world, pos);
if (random.nextInt((int)(25.0F / f) + 1) == 0) {
world.setBlockState(pos, this.withAge(i + 1), Block.NOTIFY_LISTENERS);
}
}
}
}

randomTick方法用于作物随机刻,当作物处于未成熟阶段时,这里的随机刻方法将决定作物的生长

getBaseLightLevel方法用于获取当前世界下,作物种植的方块位置的基础光照,如果光照条件不足,作物就不会生长

下面的i获取作物当前的生长阶段属性值,如果小于最大生长阶段,那么就进行一系列判断,决定作物是否生长到下一个阶段

这一系列判断的话,咱也没法说出个所以然来,这里的随机判断,具体得问Mojang怎么想出来的

getAvailableMoisture是获取当前方块及周边湿度的方法

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
32
33
34
35
36
37
38
39
40
41
42
43
protected static float getAvailableMoisture(Block block, BlockView world, BlockPos pos) {
float f = 1.0F;
BlockPos blockPos = pos.down();

for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
float g = 0.0F;
BlockState blockState = world.getBlockState(blockPos.add(i, 0, j));
if (blockState.isOf(Blocks.FARMLAND)) {
g = 1.0F;
if ((Integer)blockState.get(FarmlandBlock.MOISTURE) > 0) {
g = 3.0F;
}
}

if (i != 0 || j != 0) {
g /= 4.0F;
}

f += g;
}
}

BlockPos blockPos2 = pos.north();
BlockPos blockPos3 = pos.south();
BlockPos blockPos4 = pos.west();
BlockPos blockPos5 = pos.east();
boolean bl = world.getBlockState(blockPos4).isOf(block) || world.getBlockState(blockPos5).isOf(block);
boolean bl2 = world.getBlockState(blockPos2).isOf(block) || world.getBlockState(blockPos3).isOf(block);
if (bl && bl2) {
f /= 2.0F;
} else {
boolean bl3 = world.getBlockState(blockPos4.north()).isOf(block)
|| world.getBlockState(blockPos5.north()).isOf(block)
|| world.getBlockState(blockPos5.south()).isOf(block)
|| world.getBlockState(blockPos4.south()).isOf(block);
if (bl3) {
f /= 2.0F;
}
}

return f;
}

方法是这么一串,但你要说它有什么依据,那我还真不知道,所以我就不解释了

至于下面的if中的25.0F可能是一格水可以满足的四分之一的大耕地(9×9),也就是一格水向两个方向各延伸4个方块,
围合的区域正好是25个方块(5×5

当然,这不在我们讨论范围内了,你也可以根据现实的情况来自己编写

1
2
3
4
5
6
7
8
9
10
11
12
13
public void applyGrowth(World world, BlockPos pos, BlockState state) {
int i = this.getAge(state) + this.getGrowthAmount(world);
int j = this.getMaxAge();
if (i > j) {
i = j;
}

world.setBlockState(pos, this.withAge(i), Block.NOTIFY_LISTENERS);
}

protected int getGrowthAmount(World world) {
return MathHelper.nextInt(world.random, 2, 5);
}

applyGrowth方法是我们在使用骨粉催熟作物时调用,直接加速作物生长

根据getGrowthAmount方法,我们可以知道,每一次施加骨粉时,作物可以生长2-5个阶段

当然,作物的生长值不能对于最大值,所以要有一个if语句加以判断

1
2
3
4
@Override
public boolean canPlaceAt(BlockState state, WorldView world, BlockPos pos) {
return (world.getBaseLightLevel(pos, 0) >= 8 || world.isSkyVisible(pos)) && super.canPlaceAt(state, world, pos);
}

canPlaceAt方法用于判断作物是否可以种植,这里判断的是作物种植的方块位置的光照是否足够

另外,在它的父类方法中了,我们可以看到它还调用了canPlantOnTop方法

在多方块作物中,比如甘蔗仙人掌这种,它们的这个方法是与像小麦这种单方块作物不一样的

1
2
3
4
5
6
7
8
@Override
public void onEntityCollision(BlockState state, World world, BlockPos pos, Entity entity) {
if (entity instanceof RavagerEntity && world.getGameRules().getBoolean(GameRules.DO_MOB_GRIEFING)) {
world.breakBlock(pos, true, entity);
}

super.onEntityCollision(state, world, pos, entity);
}

onEntityCollision方法用于判断实体是否与作物方块发生碰撞,
Ravager劫掠兽碰到作物方块时,且DO_MOB_GRIEFING(游戏规则的一种,实体是否可以修改世界)为true时,会破坏作物方块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public ItemStack getPickStack(BlockView world, BlockPos pos, BlockState state) {
return new ItemStack(this.getSeedsItem());
}

@Override
public boolean isFertilizable(WorldView world, BlockPos pos, BlockState state, boolean isClient) {
return !this.isMature(state);
}

@Override
public boolean canGrow(World world, Random random, BlockPos pos, BlockState state) {
return true;
}

@Override
public void grow(ServerWorld world, Random random, BlockPos pos, BlockState state) {
this.applyGrowth(world, pos, state);
}

下面就还有一些常规的方法

getPickStack方法在鼠标中间选取方块时调用,这里返回的是作物的种子

isFertilizable方法用于判断作物是否可以被催熟,当作物处于未成熟时,返回true,可以被催熟

canGrow方法用于判断作物是否可以生长,这里直接返回true,表示作物可以生长

grow方法用于加速作物生长,这里直接调用applyGrowth方法,加速作物生长

注册作物

自定义多方块作物类

那么上面看了一堆方法,虽然不是所有方法都用得上,不过还是多了解一点,相信大家对作物的生长有了更深入的了解

那么现在我们就来写自定义的多方块作物

首先我们创建CornCropBlock,继承自CropBlock类,并重写其中的方法

1
2
3
4
5
6
7
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
15
16
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)
};

这里定义一下作物的第一个和第二个生长阶段的最大值、生长阶段属性和各个生长阶段的外轮廓线

这里我们要定义9个生长阶段的作物,但原版的生长阶段属性和外轮廓线是没有的,所以我们都需要自己重新定义

接下来就是重写一些方法

常规方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public VoxelShape getOutlineShape(BlockState state, BlockView world, BlockPos pos, ShapeContext context) {
return AGE_TO_SHAPE[state.get(AGE)];
}

@Override
protected void appendProperties(StateManager.Builder<Block, BlockState> builder) {
builder.add(AGE);
}

@Override
public int getMaxAge() {
return 8;
}

@Override
protected IntProperty getAgeProperty() {
return AGE;
}

@Override
protected ItemConvertible getSeedsItem() {
return ModItems.CORN;
}

这里重写的几个方法都是我们之前写过的,这里就不再赘述

canPlaceAt

这个方法我们得改写改写,因为现在我们是两个方块组合起来的一个多方块作物

而上方那一个方块要满足怎样的条件才能存在,是不是要到下面的方块达到生长最大值了才能长出来?

下面的方块被破坏了,那么上面的方块是不是也不能存在了?

考虑了这些之后,我们就可以来重写了

1
2
3
4
5
6
@Override
public boolean canPlaceAt(BlockState state, WorldView world, BlockPos pos) {
BlockState below = world.getBlockState(pos.down());
return super.canPlaceAt(state, world, pos) ||
below.isOf(this) && below.get(AGE) == FIRST_STAGE_AGE;
}

这里的super留着是给下面的方块种植用的,而||后面的,就是给上面的方块判断的

当下面的方块是当前作物,且下面的方块处于第一个生长阶段的最大值时,上面的方块才能存在

applyGrowth

接下来我们重写applyGrowth这个方法,毕竟现在是多方块作物,逻辑上肯定与一般作物方块有所区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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.isAir()) {
world.setBlockState(pos.up(), this.withAge(nextAge), Block.NOTIFY_LISTENERS);
} else {
world.setBlockState(pos, this.withAge(nextAge - 1), Block.NOTIFY_LISTENERS);
}
}

上面的一块是和原版一样的,我将变量名改了一下,这样就更加清晰明确了

那么下面,要分上下方块来判断了

当当前方块处于第一个生长阶段的最大值时,且上面的方块是空气,那么就长出上面的方块

否则就生长到下面的方块的生长阶段最大值

注意,else中的nextAge - 1并不完全准确,因为这篇教程定义的第二个生长阶段只有1个,所以直接-1

更为合理的应该是减去第二个生长阶段最大值

randomTick

那么它的随机刻方法也是差不多的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public 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.isAir()) {
world.setBlockState(pos.up(), this.withAge(age + 1), Block.NOTIFY_LISTENERS);
}
} else {
world.setBlockState(pos, this.withAge(age + 1), Block.NOTIFY_LISTENERS);
}
}
}

里面的一系列随机方法就按照原版的来,如果你没有特殊的判断方法,那么就按原版的写就好了

注册方块

现在我们就已经写好了一个多方块作物方块,接下去就是注册

1
2
public static final Block CORN_CROP = Registry.register(Registries.BLOCK, new Identifier(TutorialMod.MOD_ID, "corn_crop"),
new CornCropBlock(AbstractBlock.Settings.create().noCollision().ticksRandomly().breakInstantly().pistonBehavior(PistonBehavior.DESTROY)));

注册的话,与我们之前写的差不多

注册种子物品

这次我们直接将成熟的果实作为作物的种子,这个CORN也是我们之前写的食物

1
public static final Item CORN = registerItems("corn", new AliasedBlockItem(ModBlocks.CORN_CROP, new Item.Settings().food(ModFoodComponents.CORN)));

数据生成

战利品列表

那么这一次我们是像胡萝卜马铃薯这样,作物种子是果实的,它的战利品列表稍微有点不一样

不过,一样的还是要先写一个战利品列表的条件判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LootCondition.Builder builder2 =
BlockStatePropertyLootCondition.builder(ModBlocks.CORN_CROP)
.properties(StatePredicate.Builder.create().exactMatch(CornCropBlock.AGE, 8));
addDrop(ModBlocks.CORN_CROP,
applyExplosionDecay(
ModBlocks.CORN_CROP,
LootTable.builder()
.pool(LootPool.builder().with(ItemEntry.builder(ModItems.CORN)))
.pool(
LootPool.builder()
.conditionally(builder2)
.with(ItemEntry.builder(ModItems.CORN)
.apply(ApplyBonusLootFunction.binomialWithBonusCount(Enchantments.FORTUNE, 0.5714286F, 3)))
)
)
);

这一大串呢,就是按照原版的胡萝卜马铃薯的写法来的

不过我后来转念一想,是不是直接按照之前写的作物也可以,只是种子换成作物的果实就好了

最后的那一串小数,其实是获得额外产物的概率,后面的3是额外产物的数量

模型文件

模型文件我们也来稍微改变一下,让它来实现像树苗、甜浆果那样十字交叉的模型文件

而不是像原版作物那样的,四面围合的模型

这个我们按照甜浆果的模型生成方法来看好了

1
2
3
4
5
6
7
8
9
10
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,这个就是交叉型的父模型

同样它的贴图赋予的方式也是cross

而且它这个写法吧,不用我们罗列各个生长阶段了,只要将我们写的生长值属性(CornCropBlock.AGE)加进去就好了,它会自动给我们生成

材质文件

那么同样的,材质文件也是和之前一样的,注意是方块注册名 + _stage + 生长值 + .png

注意两个方块之间的衔接,我实际教程中的贴图并没有很好地接起来

测试

那么跑好数据生成之后,我们就可以进入游戏进行测试了