本篇教程的视频

本篇教程的源代码

GitHub地址:TutorialMod-1.20.1-Forge-Crop

查看CropBlock类

这一期教程我们来写一个单方块的作物,类似于原版的那些小麦,而下一期教程我们来写一个多方块的作物

所以在此之前,我们不妨先看看源代码是怎么写的(为了减少阅读量,这里截取的代码已简化,建议自行结合源代码来查看)

CropBlock类是所有种植在耕地上的作物的基类,小麦是直接使用这个方块类的,而马铃薯、甜菜根、胡萝卜等作物也都是继承这个类的

一些参数

1
2
3
public static final int MAX_AGE = 7;
public static final IntegerProperty AGE = BlockStateProperties.AGE_7;
private static final VoxelShape[] SHAPE_BY_AGE = ...
  • MAX_AGE:作物最大生长阶段
  • AGE:作物生长阶段的方块状态属性
  • SHAPE_BY_AGE:作物各个生长阶段的碰撞箱(外轮廓线)

构造函数

这里取构造函数中的一条语句

1
this.registerDefaultState(this.stateDefinition.any().setValue(this.getAgeProperty(), Integer.valueOf(0)));

这个就是设置方块最初的方块状态,将AGE属性设置为0

这个方法我们将在之后的方块状态小系列中频繁使用

getShape 方法

1
2
3
public VoxelShape getShape(BlockState pState, BlockGetter pLevel, BlockPos pPos, CollisionContext pContext) {
return SHAPE_BY_AGE[this.getAge(pState)];
}

这是根据方块的AGE属性来返回对应的碰撞箱

mayPlaceOn 方法

1
2
3
protected boolean mayPlaceOn(BlockState pState, BlockGetter pLevel, BlockPos pPos) {
return pState.is(Blocks.FARMLAND);
}

这个方法用于判断方块是否可以放置在指定位置上

那么耕地作物一般是种植在耕地上的,所以这里返回pState.is(Blocks.FARMLAND)

isRandomlyTicking & randomTick 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean isRandomlyTicking(BlockState pState) {
return !this.isMaxAge(pState);
}

public void randomTick(BlockState pState, ServerLevel pLevel, BlockPos pPos, RandomSource pRandom) {
if (!pLevel.isAreaLoaded(pPos, 1)) return; // Forge: prevent loading unloaded chunks when checking neighbor's light
if (pLevel.getRawBrightness(pPos, 0) >= 9) {
int i = this.getAge(pState);
if (i < this.getMaxAge()) {
float f = getGrowthSpeed(this, pLevel, pPos);
...
}
}
}

isRandomlyTicking是判断方块是否需要随机刻,因为作物这种方块的生长依赖于随机刻,而当作物成熟后,就不需要随机刻了

randomTick是具体的随机刻逻辑,当然,这里的部分逻辑已经经过了Forge的特化处理(可对比Fabric那边的源代码)

注意这里的getGrowthSpeed方法,具体的代码我就不放了,这是一个用于获取作物生长速度的函数,其运算机制可参见作物机制 - 中文 Minecraft Wiki,这里我就不解释了

growCrops 方法

1
2
3
4
public void growCrops(Level pLevel, BlockPos pPos, BlockState pState) {
int i = this.getAge(pState) + this.getBonemealAgeIncrease(pLevel);
...
}

这是骨粉催熟作物时执行的逻辑

这里我们再说一点,我们可以观察到CropBlock实现了BonemealableBlock这个接口,而实现了这个接口的方块,就可以被骨粉催熟(或者执行一些其他的逻辑,比如骨粉右键草方块可以生成一堆花花草草)

getBonemealAgeIncrease方法就是获取骨粉催熟作物时,作物的Age属性增加量

canSurvive 方法

1
2
3
public boolean canSurvive(BlockState pState, LevelReader pLevel, BlockPos pPos) {
return (pLevel.getRawBrightness(pPos, 0) >= 8 || pLevel.canSeeSky(pPos)) && super.canSurvive(pState, pLevel, pPos);
}

这个方法是判断方块是否可以存活的,当光照等级不够时,方块将无法存活

entityInside 方法

1
2
3
public void entityInside(BlockState pState, Level pLevel, BlockPos pPos, Entity pEntity) {
...
}

这个方法是当实体进入作物方块的范围内时,会执行的逻辑

参照原版的一些逻辑,比如劫掠兽经过是会破坏作物,甜浆果丛会使实体减速或受伤

getBaseSeedId 方法

1
2
3
protected ItemLike getBaseSeedId() {
return Items.WHEAT_SEEDS;
}

这个方法用于获取作物的种子,那么CropBlock默认返回是就是小麦种子

getCloneItemStack 方法

1
2
3
public ItemStack getCloneItemStack(BlockGetter pLevel, BlockPos pPos, BlockState pState) {
return new ItemStack(this.getBaseSeedId());
}

这是当我们游戏的十字准星对准方块并点击鼠标中键时,会执行的逻辑

那么对于作物方块来说,就是返回作物的种子

isValidBonemealTarget & isBonemealSuccess & performBonemeal 方法

1
2
3
4
5
6
7
8
9
10
11
public boolean isValidBonemealTarget(LevelReader pLevel, BlockPos pPos, BlockState pState, boolean pIsClient) {
return !this.isMaxAge(pState);
}

public boolean isBonemealSuccess(Level pLevel, RandomSource pRandom, BlockPos pPos, BlockState pState) {
return true;
}

public void performBonemeal(ServerLevel pLevel, RandomSource pRandom, BlockPos pPos, BlockState pState) {
this.growCrops(pLevel, pPos, pState);
}

这三者是实现BonemealableBlock接口时需要重写的方法

分别对应的是是否可以进行骨粉催熟是否成功催熟,以及执行骨粉催熟的逻辑

createBlockStateDefinition 方法

1
2
3
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> pBuilder) {
pBuilder.add(AGE);
}

这个方法是为方块添加方块状态属性,这个方法我们在未来的方块状态小系列中,也会频繁使用

作物方块类

那么到这里为止,我们已基本了解了作物方块有哪些逻辑,接下来我们救来实现自己的作物方块

这里我们新建一个StrawberryCrop类,并让它继承CropBlock类

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
public class StrawberryCrop extends CropBlock {
public static final int MAX_AGE = 5;
public static final IntegerProperty AGE = BlockStateProperties.AGE_5;

public StrawberryCrop(Properties pProperties) {
super(pProperties);
}

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

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

@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> pBuilder) {
pBuilder.add(AGE);
}

@Override
protected ItemLike getBaseSeedId() {
return ModItems.STRAWBERRY_SEEDS.get();
}

@Override
protected boolean mayPlaceOn(BlockState pState, BlockGetter pLevel, BlockPos pPos) {
return super.mayPlaceOn(pState, pLevel, pPos) || pState.is(BlockTags.DIRT);
}
}

同时我们重写一些参数,我们将最大的生长阶段定义为5,并定义一个属性为AGE,并添加到方块状态中

这里的种子我们还没注册,等会去注册

最后我们还重写了mayPlaceOn方法,让它不仅可以种在耕地上,还可以直接种在草方块、泥土方块上

注册方块

在注册作物的种子之前,我们先注册方块

1
2
public static final RegistryObject<StrawberryCrop> STRAWBERRY_CROP =
BLOCKS.register("strawberry_crop", () -> new StrawberryCrop(BlockBehaviour.Properties.copy(Blocks.WHEAT)));

这里我们注册了一个名为strawberry_crop的作物方块,并继承了原版小麦方块的属性

注意我们这里直接使用BLOCKS.register方法注册,没有使用我们之前封装的一起注册方块和方块物品的方法,因为方块物品需要单独注册

注册种子物品

1
2
public static final RegistryObject<Item> STRAWBERRY_SEEDS = ITEMS.register("strawberry_seeds",
() -> new ItemNameBlockItem(ModBlocks.STRAWBERRY_CROP.get(), new Item.Properties()));

这里的种子物品我们使用ItemNameBlockItem类,并传入作物方块和物品属性

不要忘了将其添加到物品栏

1
pOutput.accept(ModItems.STRAWBERRY_SEEDS.get());

数据文件

同样我们还是使用数据生成来写数据文件

战利品列表

1
2
3
4
LootItemCondition.Builder builder1 = LootItemBlockStatePropertyCondition.hasBlockStateProperties(ModBlocks.STRAWBERRY_CROP.get())
.setProperties(StatePropertiesPredicate.Builder.properties().hasProperty(StrawberryCrop.AGE, 5));
add(ModBlocks.STRAWBERRY_CROP.get(), createCropDrops(ModBlocks.STRAWBERRY_CROP.get(),
ModItems.STRAWBERRY.get(), ModItems.STRAWBERRY_SEEDS.get(), builder1));

作物的战利品列表有些特殊,当作物没有成熟就被破坏了,只会掉落种子;只有完全成熟的作物才会掉落种子和果实

所以这里我们需要使用LootItemBlockStatePropertyCondition类来判断作物是否已经完全成熟

而后使用createCropDrops方法来生成作物的战利品

方块模型文件

在写方块模型的生成语句之前,我们再创建两个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void crop(CropBlock block, String name, IntegerProperty property) {
Function<BlockState, ConfiguredModel[]> function = state ->
cropStates(state, name, property);

getVariantBuilder(block).forAllStates(function);
}

private ConfiguredModel[] cropStates(BlockState state, String modelName, IntegerProperty property) {
ConfiguredModel[] models = new ConfiguredModel[1];
models[0] = new ConfiguredModel(models().crop(modelName + state.getValue(property),
ResourceLocation.fromNamespaceAndPath(TutorialMod.MOD_ID, "block/" + modelName + state.getValue(property))).renderType("cutout"));

return models;
}

这两个方法本质就是作物方块模型文件的生成语句,因为Forge这边没有提供给我们已经封装好的方法,所以得自己写

最后也附上了renderType的设置,因为作物方块一般是有透明通道的,总不是一个纯粹的面片

然后我们就可以调用这个方法了

1
crop(ModBlocks.STRAWBERRY_CROP.get(), "strawberry_crop_stage", StrawberryCrop.AGE);

这里传入的参数为作物方块,作物名称,作物的生长阶段属性

语言文件

1
add(ModItems.STRAWBERRY_SEEDS.get(), "Strawberry Seeds");

物品模型文件

1
basicItem(ModItems.STRAWBERRY_SEEDS.get());

种子的模型还是一般的物品

贴图文件

作物的方块贴图也有一些特殊,你需要准备作物各个生长阶段的贴图

比如我们这里写的作物,你需要准备6张贴图,strawberry_crop_stage_0.png为未成熟作物,strawberry_crop_stage_1.png为第一阶段,以此类推

物品的种子贴图也记得放到对应的文件夹中

之后我们就可以启动游戏进行测试了