本篇教程的视频
本篇教程的源代码
本篇教程目标
查看源代码
那么这期教程我们先来看看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
注意两个方块之间的衔接
,我实际教程中的贴图并没有很好地接起来
测试
那么跑好数据生成之后,我们就可以进入游戏进行测试了