本篇教程的视频
本篇教程的源代码
GitHub地址:
自定义方块类
本期教程我们来写一个可连接方块——沙发
源代码的话我们这里就先不看了,因为我们现在写的并不是像栅栏这样的方块
未来的另外一期的可连接方块我们再来讲栅栏那样的方块
SofaBlock
接下来我们先创建一个SofaBlock类,继承自Block类,并重写它的方法
1 2 3 4 5
| public class SofaBlock extends Block { public SofaBlock(Properties properties) { super(properties); } }
|
连接状态枚举类
原版有的连接状态也就只有栅栏方块有,但并不是我们需要的
因为沙发总不能像栅栏那样东西南北4个面都可以连接吧,在这一期教程中,我们暂且定义这个沙发只能左右连接
那么现在,我们就得自己来创建一个方块状态的属性,首先是创建一个枚举类,定义各个状态
在SofaBlock类中创建一个嵌套的枚举类Type,实现StringIdentifiable接口
1 2 3 4 5 6 7 8
| public enum Type implements StringRepresentable { ;
@Override public String getSerializedName() { return ""; } }
|
这里我们再定义一个String类型的变量,用于表示连接状态的名字
1
| private final String name;
|
同时也要创建一个构造函数,用于初始化这个变量
1 2 3
| Type(String name) { this.name = name; }
|
后面的重写方法我们也要改一下
1 2 3 4
| @Override public String getSerializedName() { return this.name; }
|
最后我们就可以来定义各个不同的状态了
1 2 3 4
| SINGLE("single"), LEFT("left"), RIGHT("right"), MIDDLE("middle");
|
这里我们一共定义了4个状态,分别是单个沙发、左沙发、右沙发、中间沙发
那么同样的,你的方块模型就得准备4个,当然建模上的事情我们这里就不多讲了
定义方块状态
写完这个Type类之后,我们接下来就要声明这个类,给我们的方块赋予方块状态了
1 2
| public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; public static final EnumProperty<Type> TYPE = EnumProperty.create("type", Type.class);
|
这里我们将FACING引入,再定义一共EnumProperty类型的变量,泛型是我们写的方块状态枚举类
随后使用EnumProperty.create方法来创建这个变量,第一个参数是名字,第二个参数是枚举类
方块状态初始化
我们在构造函数中来初始化这些方块状态
1 2 3 4
| public SofaBlock(Properties properties) { super(properties); this.registerDefaultState(this.defaultBlockState().setValue(FACING, Direction.NORTH).setValue(TYPE, Type.SINGLE)); }
|
写法上和我们之前写的差不多,用with方法来设置默认状态
重写方法
接下来我们重写几个方法
常规方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Override protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> pBuilder) { pBuilder.add(FACING, TYPE); }
@Override public BlockState rotate(BlockState pState, Rotation pRot) { return pState.setValue(FACING, pRot.rotate(pState.getValue(FACING))); }
@Override public BlockState mirror(BlockState pState, Mirror pMirror) { return pState.rotate(pMirror.getRotation(pState.getValue(FACING))); }
@Override public @Nullable BlockState getStateForPlacement(BlockPlaceContext pContext) { return this.defaultBlockState().setValue(FACING, pContext.getHorizontalDirection().getOpposite()); }
|
这4个方法我们之前都遇到过了,这里就不再赘述
那么随后我们就要再重写一些方法用于改变方块的状态,也就是实现方块能够连接起来的方法
updateShape
这里我们要重写updateShape方法,这个名字显然易见,就是当周围方块发生改变时,会调用这个方法
1 2 3 4
| @Override public BlockState updateShape(BlockState pState, Direction pDirection, BlockState pNeighborState, LevelAccessor pLevel, BlockPos pPos, BlockPos pNeighborPos) { return this.getRelatedBlockState(pState, pLevel, pPos, pState.getValue(FACING)); }
|
这里我们调用了getRelatedBlockState方法,这个方法是我们自定义的方法,用于获取相关的方块状态
1 2 3 4 5 6 7 8 9 10 11 12
| private BlockState getRelatedBlockState(BlockState state, LevelAccessor level, BlockPos pos, Direction direction) { boolean left = isRelatedInDirection(level, pos, direction, true); boolean right = isRelatedInDirection(level, pos, direction, false); if (left && right) { return state.setValue(TYPE, Type.MIDDLE); } else if (left) { return state.setValue(TYPE, Type.RIGHT); } else if (right) { return state.setValue(TYPE, Type.LEFT); } return state.setValue(TYPE, Type.SINGLE); }
|
这个方法就是用来返回不同的方块状态,判断左右是否同样是沙发方块,再来决定是否连接
isRelatedInDirection方法也是我们自定义的方法
1 2 3 4 5 6 7 8 9 10 11 12 13
| private boolean isRelatedInDirection(LevelAccessor level, BlockPos pos, Direction direction, boolean counterClockwise) { Direction rotate = counterClockwise ? direction.getCounterClockWise() : direction.getClockWise(); return this.isRelatedBlock(level, pos, rotate, direction); }
private boolean isRelatedBlock(LevelAccessor level, BlockPos pos, Direction rotate, Direction direction) { BlockState state = level.getBlockState(pos.relative(rotate)); if (state.getBlock() == this) { Direction direction1 = state.getValue(FACING); return direction1.equals(direction); } return false; }
|
其实是还有两个方法的,当时想放一起然后后面又忘记了
isRelatedInDirection方法就进行了一步方向旋转的操作,这个旋转是根据最后的布尔值来决定是逆时针旋转还是顺时针旋转
getCounterClockWise()是逆时针旋转,getClockWise()是顺时针旋转
那么问题又来了,这转的是什么?Direction,也就是方位
要理解这个方法也很简单,现在请你站起来,然后你逆时针转动90°,是不是就面向你原来左边的这个方位了,同理,顺时针转,就面向右边的
当然,因为这里的方法判断是对于玩家来说的,所以上面是反着写的
isRelatedBlock方法就是判断这个方块是否是沙发方块,并且方向是否一致
正常的沙发都是面向同一个方向的吧,总不能一个朝前一个朝后然后还能接起来吧?
当然,你也可以去实现像楼梯那样的
那么现在,我们的方块就已经写好了,接下去就是注册方块
注册方块
注册方块
那么接下来我们就来注册方块
1 2 3
| public static final DeferredBlock<SofaBlock> SOFA_BLOCK = registerBlock("sofa_block", () -> new SofaBlock(Block.Properties.of().strength(3.0F, 3.0F).noOcclusion()));
|
那么它实例化的是我们前面写的SofaBlock,并且设置了一些属性
添加物品栏
不要忘记将物品添加到物品栏
1
| output.accept(ModBlocks.SOFA_BLOCK);
|
数据文件
语言文件
1
| add(ModBlocks.SOFA_BLOCK.get(), "Sofa");
|
模型文件
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
| sofa(ModBlocks.SOFA_BLOCK, "sofa");
private <T extends Block> void sofa(DeferredBlock<T> block, String name) { ModelFile modelSingle = models().getExistingFile(modLoc("block/" + name)); ModelFile modelLeft = models().getExistingFile(modLoc("block/" + name + "_left")); ModelFile modelRight = models().getExistingFile(modLoc("block/" + name + "_right")); ModelFile modelMiddle = models().getExistingFile(modLoc("block/" + name + "_middle"));
getVariantBuilder(block.get()).forAllStates(state -> { SofaBlock.Type type = state.getValue(SofaBlock.TYPE); Direction facing = state.getValue(SofaBlock.FACING);
ModelFile t; switch (type) { case LEFT -> t = modelLeft; case MIDDLE -> t = modelMiddle; case RIGHT -> t = modelRight; default -> t = modelSingle; };
int yRot; switch (facing) { case EAST -> yRot = 90; case SOUTH -> yRot = 180; case WEST -> yRot = 270; default -> yRot = 0; };
return ConfiguredModel.builder() .modelFile(t) .rotationY(yRot) .build(); }); simpleBlockItem(block.get(), modelSingle); }
|
由于我们这里引入了一个自己定义的Type变量,所以没有原版的数据生成类可以调用了,但是我们可以自己写一个
这一串方法看着复杂,但很多都是重复的,其实质就是罗列出每一个状态对应的方块状态是什么
分别根据TYPE和FACING来返回不同的模型文件,并且根据FACING来旋转模型
当你有很多这样的方块要添加的时候,就可以这么做了
另外的模型文件就是Blockbench制作的了
测试
那么跑好数据生成之后,我们就可以进入游戏进行测试了