本篇教程的视频

本篇教程的源代码

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方法,这个方法是我们自定义的方法,用于获取相关的方块状态

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方法也是我们自定义的方法

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变量,所以没有原版的数据生成类可以调用了,但是我们可以自己写一个

这一串方法看着复杂,但很多都是重复的,其实质就是罗列出每一个状态对应的方块状态是什么

分别根据TYPEFACING来返回不同的模型文件,并且根据FACING来旋转模型

当你有很多这样的方块要添加的时候,就可以这么做了

另外的模型文件就是Blockbench制作的了

测试

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