本篇教程的视频

本篇教程的源代码

Github地址:TutorialMod-Crop-1.21

介绍

作物在Minecraft中是一类特殊的方块,它们可以被种植,自己会生长,最后成熟时可以被玩家收获

游戏中有很多作物,单方块的如小麦胡萝卜马铃薯等,还有一些是多方块的,如甘蔗南瓜西瓜仙人掌等,还有像其他种植在特殊地域的作物,如海带可可豆

本篇教程我们就来实现一个单方块的作物,在下一篇教程中我们会实现一个多方块的作物

查看源代码

那么首先第一步,我们还是来看看游戏的那些作物的源代码,我们以WHEAT(小麦)为例

作物方块注册

1
2
3
4
5
6
7
8
9
10
11
12
public static final Block WHEAT = register(
"wheat",
new CropBlock(
AbstractBlock.Settings.create()
.mapColor(MapColor.DARK_GREEN)
.noCollision()
.ticksRandomly()
.breakInstantly()
.sounds(BlockSoundGroup.CROP)
.pistonBehavior(PistonBehavior.DESTROY)
)
);

这个是在Blocks类中的注册,因为作物本质还是方块,我们可以看到WHEAT实例化了CropBlock

后面是它的一些Settings中设置了一些属性,之前见过的我这里就不再赘述,我们看看新出现的几个

noCollision这是没有碰撞体积,也就是说玩家可以穿过这个方块。
游戏里的大多数作物是没有碰撞体积的,当然也有特殊的,比如仙人掌是有碰撞体积的,并且碰到会受到伤害;甜浆果虽然没有碰撞体积,但实体碰到会减慢其速度,并造成伤害。
这些其实都可以去研究一下

ticksRandomly这个是随机刻,关于随机刻的介绍可以看这里
它是关乎我们作物生长的,wiki上也指出了除了作物生长之外还要响应随机刻的事件,比如说铜方块的氧化、草方块的蔓延等等。
如果说你有一些方块需要进行随机计算的,也可以使用这个方法

breakInstantly这个是瞬间破坏,也就是说玩家用手去破坏这个方块的时候会瞬间破坏,不会有破坏的动画(也就是那裂开的动画)

pistonBehavior这个是活塞行为,这个是用来设置活塞推动这个方块的时候的行为,这里设置的是DESTROY,也就是推动的时候会破坏这个方块

CropBlock类

那么我们再来看看CropBlock这个类

类有点长,我们截取一点,来看看里面的一些方法

1
2
3
4
5
public static final int MAX_AGE = 7;
public static final IntProperty AGE = Properties.AGE_7;
private static final VoxelShape[] AGE_TO_SHAPE = new VoxelShape[]{
...
};

前面这两个是定义作物的生长阶段,MAX_AGE是最大的生长阶段,AGE是一个IntProperty,也就是说它是一个整数类型的属性,这个属性的值是0-7,也就是说作物有8个生长阶段

后面一个是碰撞箱(外轮廓线,也就是光标对准方块后的那些黑色的边线,这个线和碰撞箱的体积是一致的,只是作物没有碰撞体积)

作物的碰撞箱是会随着生长阶段而变化的,这个数组里面存放了8个生长阶段的碰撞箱,下面也有方法返回其碰撞箱

1
2
3
4
public CropBlock(AbstractBlock.Settings settings) {
super(settings);
this.setDefaultState(this.stateManager.getDefaultState().with(this.getAgeProperty(), Integer.valueOf(0)));
}

这个是构造函数,它设置了作物的默认状态,也就是说作物的默认生长阶段AGE是0

1
2
3
4
@Override
protected VoxelShape getOutlineShape(BlockState state, BlockView world, BlockPos pos, ShapeContext context) {
return AGE_TO_SHAPE[this.getAge(state)];
}

这个是获取碰撞箱的方法,它会根据作物的目前的生长阶段返回对应的碰撞箱

1
2
3
protected IntProperty getAgeProperty() {
return AGE;
}

这个方法是返回作物的生长阶段属性,也就是AGE

1
2
3
4
@Override
protected boolean canPlantOnTop(BlockState floor, BlockView world, BlockPos pos) {
return floor.isOf(Blocks.FARMLAND);
}

这个是判断作物是否可以种植在某个方块上,这里是判断是否是种植在耕地上,你也可以重写一下,让我们的作物种植在其他方块上

1
2
3
public int getMaxAge() {
return 7;
}

这个是获取作物的最大生长阶段,也就是MAX_AGE,也可以直接返回数字

1
2
3
public int getAge(BlockState state) {
return (Integer)state.get(this.getAgeProperty());
}

这个是获取作物的生长阶段,调用了上面的getAgeProperty方法,返回了作物的生长阶段

1
2
3
public BlockState withAge(int age) {
return this.getDefaultState().with(this.getAgeProperty(), Integer.valueOf(age));
}

这个是设置方块状态,根据age的值返回一个新的BlockState,这是在后面的一系列生长逻辑中调用的

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

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

这两个方法是判断作物是否成熟,isMature是判断作物的生长阶段是否大于等于最大生长阶段

hasRandomTicks是判断是否还需要随机刻,作物成熟后就不需要随机刻了

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
@Override
protected void randomTick(BlockState state, ServerWorld world, BlockPos pos, Random random) {
...
}

public void applyGrowth(World world, BlockPos pos, BlockState state) {
...
}

protected int getGrowthAmount(World world) {
...
}

protected static float getAvailableMoisture(Block block, BlockView world, BlockPos pos) {
...
}

@Override
protected boolean canPlaceAt(BlockState state, WorldView world, BlockPos pos) {
...
}

protected static boolean hasEnoughLightAt(WorldView world, BlockPos pos) {
...
}

这里的一串都是作物生长的逻辑,randomTick是随机刻的逻辑

applyGrowth是应用生长的逻辑

getGrowthAmount是获取随机值,用于生长的逻辑

getAvailableMoisture是获取作物生长环境的湿度(这玩意还挺离谱的)

canPlaceAt是判断作物是否可以种植

hasEnoughLightAt是判断环境是否有足够的光照

1
2
3
4
5
6
7
@Override
protected 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);
}

这个是碰撞实体的逻辑,这里是判断如果碰到的实体是RavagerEntity(劫掠兽)并且游戏规则允许怪物破坏方块,那么就破坏这个方块

1
2
3
protected ItemConvertible getSeedsItem() {
return Items.WHEAT_SEEDS;
}

这个是获取种子的方法,这里是返回小麦的种子

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

这个是判断作物是否可以施肥,施肥可以促进作物生长,但如果作物已经成熟了施肥也没有用

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

这个是添加属性的方法,这里添加了AGE属性

因为AGE属性是我们的方块状态,我们在构造函数中也设置了其初始方块状态。
你翻过那些方块状态文件也会知道,根据不同的AGE值返回不同的模型

如果说你也写了一些带有不同状态的方块,这玩意一定不能忘记重写

种子物品注册

那么除此之外,我们还得再看看Items中的注册

1
2
public static final Item WHEAT_SEEDS = register("wheat_seeds", new AliasedBlockItem(Blocks.WHEAT, new Item.Settings()));
public static final Item WHEAT = register("wheat", new Item(new Item.Settings()));

我们可以发现,其实Blocks中注册的小麦方块是没有对应的方块物品的,但是有个对应的种子,另外一个WHEAT是对应的作物,这个是用来做食物的

所以我们待会在注册的时候就得注意这个问题

不过既然来了,先看看这个种子怎么注册的

它实例化的是AliasedBlockItem,其参数是方块和物品设置,这个AliasedBlockItem是一个继承自BlockItem的类,它的作用是将方块和物品绑定在一起,也就是说你在游戏中种下这个种子,长出来的作物就是这个方块

当然这里的小麦只是其中一个例子,小麦的收获物和种子是分开的,和甜菜根一样。
但还有像马铃薯、胡萝卜、甜浆果等作物,它们的种子和收获物是同一个,这个也值得去研究研究

注册作物

这里我们就来仿照小麦写一个简单的作物,不过参照上面的CropBlock,小麦它是有8个生长阶段的(0-7),我们自己写的时候可能不需要这么多(也许是贴图不想画这么多…)

所以我们可以自己去写一个自定义的作物类,重写它的生长阶段就好

创建StrawberryCropBlock类

我们前面写过一个Strawberry这个食物,所以以此衍生,我们来创建一个StrawberryCropBlock类,继承CropBlock

1
2
3
4
5
6
public class StrawberryCropBlock extends CropBlock {

public StrawberryCropBlock(Settings settings) {
super(settings);
}
}

然后,按照我们的想法,重新定义其生长阶段

1
2
public static final int MAX_AGE = 5;
public static final IntProperty AGE = Properties.AGE_5;

这里我们定义了最大生长阶段的值为5

AGE的属性可以调用Properties中的AGE_5,这个是一个IntProperty,它的值是0-5,也就是说有6个生长阶段

如果说Properties中没有你要的值,那就仿照里面的语句自己创建呗(下一篇会说)

碰撞箱倒是不用再定义了,因为可以直接调用父类的碰撞箱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public int getMaxAge() {
return MAX_AGE;
}
protected IntProperty getAgeProperty() {
return AGE;
}

@Override
public int getAge(BlockState state) {
return state.get(this.getAgeProperty());
}

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

这里我们重写了getMaxAgegetAgePropertygetAgeappendProperties这几个方法

这些也是上面提到过的一些方法

1
2
3
4
@Override
protected ItemConvertible getSeedsItem() {
return null;
}

这里我们先返回null,因为我们还没有注册种子物品

1
2
3
4
@Override
protected boolean canPlantOnTop(BlockState floor, BlockView world, BlockPos pos) {
return floor.isIn(BlockTags.DIRT) || floor.isOf(Blocks.FARMLAND);
}

这里我们重写了canPlantOnTop方法,让我们的作物也可以种植在DIRT(也就是那些土壤上,比如砂土、土壤、草方块等等)

注册作物方块

那么我们就来注册我们的作物方块

1
2
public static final Block STRAWBERRY_CROP = Registry.register(Registries.BLOCK, Identifier.of(TutorialMod.MOD_ID, "strawberry_crop"),
new StrawberryCropBlock(AbstractBlock.Settings.copy(Blocks.WHEAT)));

这里我们注册了一个STRAWBERRY_CROP,实例化StrawberryCropBlock,我们把WHEAT的属性copy过来

另外,我们这里使用的注册方法就直接用了Registry.register,因为我们不需要注册方块物品,也就不能用我们之前写的方法(毕竟作物方块是种在地上的,总不能把植株作为一种方块物品吧?)

注册种子物品

接下来,我们注册种子物品

1
2
public static final Item STRAWBERRY_SEEDS = registerItems("strawberry_seeds",
new AliasedBlockItem(ModBlocks.STRAWBERRY_CROP, new Item.Settings()));

这就是用之前我们看到的写法来写,实例化AliasedBlockItem,把STRAWBERRY_CROPItem.Settings传进去

另外也记得完善我们重写的getSeedsItem方法

1
2
3
4
@Override
protected ItemConvertible getSeedsItem() {
return ModItems.STRAWBERRY_SEEDS;
}

这样我们使用这个种子的时候,作物就可以被种到地上了

写入物品栏

我们还得将作物的种子写入物品栏

1
entries.add(ModItems.STRAWBERRY_SEEDS);

渲染层设置

最后,我们还得设置渲染层,作物的贴图基本上是带全透明区域的吧,毕竟植株之间是有空隙的,所以我们还得单独设置渲染层

还是两种方法,以下是Mixin的方法,到我们之前写的RenderLayersMixin中写

1
BLOCKS.put(ModBlocks.STRAWBERRY_CROP, RenderLayer.getCutout());

另一种是在TutorialModClient

1
BlockRenderLayerMap.INSTANCE.putBlock(ModBlocks.STRAWBERRY_CROP, RenderLayer.getCutout());

这样透明的区域就不会变成黑色的了

数据文件

语言文件

这里就只要写种子的就好了

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

模型文件

种子的模型文件不需要写,它会根据我们的作物方块中设置的种子进行生成

1
blockStateModelGenerator.registerCrop(ModBlocks.STRAWBERRY_CROP, Properties.AGE_5, 0, 1, 2, 3, 4, 5);

这里我们就注册方块的模型文件即可,作物方块的生成有单独的方法,其最后的可变参数是各个生长阶段的值,有n个就从0写到n(为啥它就不能写个循环遍历嘞?)

战利品列表

作物的战利品列表也是值得讲一下的,因为它在不同状态下返回的掉落物是不一样的

比如说小麦在未成熟之前被破坏了,只会掉落1个种子;但是成熟之后被破坏,会掉落1个小麦和1-4个小麦种子

所以这个东西写起来可能有点复杂,不过我们可以参考原版小麦的战利品列表生成方法

1
2
3
4
LootCondition.Builder builder2 = BlockStatePropertyLootCondition.builder(ModBlocks.STRAWBERRY_CROP)
.properties(StatePredicate.Builder.create().exactMatch(StrawberryCropBlock.AGE, 5));
addDrop(ModBlocks.STRAWBERRY_CROP, cropDrops(
ModBlocks.STRAWBERRY_CROP, ModItems.STRAWBERRY, ModItems.STRAWBERRY_SEEDS, builder2));

这个就是搬小麦的,改了一些参数,这里的builder2是战利品列表的判定情况(这里就是当我们的作物的age值到5时,也就是它成熟了),cropDrops是作物方块生成掉落物的方法

那么折腾完这些东西之后,我们就可以进入我们的游戏种植我们的作物了