本篇教程的视频

本篇教程的源代码

Github地址:TutorialMod-Connectable Block-1.21

介绍

我们前面简单介绍了一下方块状态,这次我们将使用方块状态来实现一个可连接的方块。这个方块可以连接到其他相同的方块

但本篇教程并不是像原版的栅栏那样,我们将实现另一种可连接方块的方式,更准确来讲应该是沙发长椅这样的方块,它们也还有我们前面讲的FACING属性

本篇教程也是从我自己目前开发的明日方舟家具模组中拿过来的实例,也是用在模组中的沙发类方块上的

当然,原本的方法是照着Mr.Crayfish的家具模组写的,不过当时写的是照着Forge的方法写的Fabric

方块类

创建SimpleFence类

1
2
3
4
5
6
7
public class SimpleFence extends Block {

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

}

这里我们创建了一个SimpleFence类,继承了Block类,这个类是我们的可连接方块的自定义类

引入FACING属性

我们这里引入了FACING属性,然后重写一些方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static final DirectionProperty FACING = Properties.HORIZONTAL_FACING;

@Nullable
@Override
public BlockState getPlacementState(ItemPlacementContext ctx) {
return this.getDefaultState().with(FACING, ctx.getHorizontalPlayerFacing().getOpposite());
}

@Override
protected BlockState rotate(BlockState state, BlockRotation rotation) {
return state.with(FACING, rotation.rotate(state.get(FACING)));
}

@Override
protected BlockState mirror(BlockState state, BlockMirror mirror) {
return state.rotate(mirror.getRotation(state.get(FACING)));
}

重写getPlacementStaterotatemirror方法,和前一篇教程一样

而后我们就要写可以连接的这个属性了

思考

我们怎么实现可连接呢?

我们可以设置一些字段,作为方块的方块状态

比如Type,下面可设置单体四种状态

然后写一些方法来设置当前方块的Type是什么

再在方块状态文件中用不同的Type来返回不同的方块模型

大体的思路就是这样,其实原版很多方块状态都可以拿来参考

创建Type属性

对于我们设计的Type属性,原版没有相关的内容,这就需要我们自己来写了

这里我们创建了一个Type的枚举类,直接写在我们的方块类中好了,将其作为一个内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum Type implements StringIdentifiable {
SINGLE("single"),
LEFT("left"),
MIDDLE("middle"),
RIGHT("right");

private final String name;

Type(String name) {
this.name = name;
}

@Override
public String asString() {
return name;
}
}

当然,这个Type类需要实现StringIdentifiable接口,并重写asString方法

这个接口是实现用字符串提供实例

这里我们就根据我们上面的设计,写了四种Type,分别是SINGLELEFTMIDDLERIGHT

原版一些没有的属性完全可以通过这种方法来实现,我们前面用的FACING也是一个枚举类

设置Type属性

1
public static final EnumProperty<Type> TYPE = EnumProperty.of("type", Type.class);

我们在方块类中写上我们这里的属性,这里我们用EnumProperty来设置Type属性,因为它是一个枚举类

设置Type属性的默认值

1
this.setDefaultState(this.getStateManager().getDefaultState().with(FACING, Direction.NORTH).with(TYPE, Type.SINGLE));

我们在方块类的构造方法中设置了Type属性的默认值,这里我们设置为SINGLE,也就是单体

当然FACING属性也要设置默认值,这里我们设置为NORTH,也就是北方向

重写appendProperties方法

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

记得重写appendProperties方法,将我们的属性加入到方块状态中,包括FACINGTYPE

重写方法

那么接下来我们就要重写一些方法,还要自己写一些方法,用这些方法来设置当前方块到底是什么Type

比如当我们的方块左右都没有相同的方块时,那它就是单体,Type就是SINGLE

当它左边有相同的方块时,那它就是右边的,Type就是RIGHT

当它右边有相同的方块时,那它就是左边的,Type就是LEFT

当它左右都有相同的方块时,那它就是中间的,Type就是MIDDLE

而我们主要要判断的就是它的左右方块是否为相同的方块

重写getStateForNeighborUpdate方法

1
2
3
4
@Override
protected BlockState getStateForNeighborUpdate(BlockState state, Direction direction, BlockState neighborState, WorldAccess world, BlockPos pos, BlockPos neighborPos) {
return getRelatedBlockState(state, world, pos, state.get(FACING));
}

这里我们重写了getStateForNeighborUpdate方法,这个方法是当方块的邻居方块发生变化时调用的方法

然后下面的getRelatedBlockState方法是我们自己写的

getRelatedBlockState方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private BlockState getRelatedBlockState(BlockState state, WorldAccess world, BlockPos pos, Direction direction) {
boolean left = isRelatedBlock(world, pos, direction.rotateYCounterclockwise(), direction) ||
isRelatedBlock(world, pos, direction.rotateYCounterclockwise(), direction.rotateYCounterclockwise());
boolean right = isRelatedBlock(world, pos, direction.rotateYClockwise(), direction) ||
isRelatedBlock(world, pos, direction.rotateYClockwise(), direction.rotateYClockwise());

if (left && right) {
return state.with(TYPE, Type.MIDDLE);
} else if (left) {
return state.with(TYPE, Type.LEFT);
} else if (right) {
return state.with(TYPE, Type.RIGHT);
} else {
return state.with(TYPE, Type.SINGLE);
}
}

上面的两个leftright是用来判断左右方向是否有相同的方块

isRelatedBlock用于判断当前方块的邻居方块是否为相同的方块,也是我们自定义的方法

然后我们根据左右方向是否有相同的方块来设置当前方块的Type,其逻辑就是上面我们设计的那些

direction.rotateYCounterclockwise()是获取当前方向逆时针旋转90度的方块位置(按其朝向,为当前方块左边位置,按照我们的视角,则为方块的右边,因为我们上面放置方块是取其反向的

direction.rotateYClockwise()是获取当前方向顺时针旋转90度的方块位置(同理,对于方块自身而言,是其右边位置

isRelatedBlock方法

1
2
3
4
5
6
7
8
private boolean isRelatedBlock(WorldAccess world, BlockPos pos, Direction direction, Direction direction1) {
BlockState state = world.getBlockState(pos.offset(direction));
if (state.getBlock() == this) {
Direction blockDirection = state.get(FACING);
return blockDirection.equals(direction1);
}
return false;
}

因为这里要结合FACING,所以还得判断当前方块的方向是否和邻居方块的方向相同

这样之后,我们的方块就完全写好了

下面是完整的代码

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class SimpleFence extends Block {
public static final DirectionProperty FACING = Properties.HORIZONTAL_FACING;
public static final EnumProperty<Type> TYPE = EnumProperty.of("type", Type.class);
public SimpleFence(Settings settings) {
super(settings);
this.setDefaultState(this.getStateManager().getDefaultState().with(FACING, Direction.NORTH).with(TYPE, Type.SINGLE));
}

@Nullable
@Override
public BlockState getPlacementState(ItemPlacementContext ctx) {
return this.getDefaultState().with(FACING, ctx.getHorizontalPlayerFacing().getOpposite());
}

@Override
protected BlockState rotate(BlockState state, BlockRotation rotation) {
return state.with(FACING, rotation.rotate(state.get(FACING)));
}

@Override
protected BlockState mirror(BlockState state, BlockMirror mirror) {
return state.rotate(mirror.getRotation(state.get(FACING)));
}

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

@Override
protected BlockState getStateForNeighborUpdate(BlockState state, Direction direction, BlockState neighborState, WorldAccess world, BlockPos pos, BlockPos neighborPos) {
return getRelatedBlockState(state, world, pos, state.get(FACING));
}

private BlockState getRelatedBlockState(BlockState state, WorldAccess world, BlockPos pos, Direction direction) {
boolean left = isRelatedBlock(world, pos, direction.rotateYCounterclockwise(), direction) ||
isRelatedBlock(world, pos, direction.rotateYCounterclockwise(), direction.rotateYCounterclockwise());
boolean right = isRelatedBlock(world, pos, direction.rotateYClockwise(), direction) ||
isRelatedBlock(world, pos, direction.rotateYClockwise(), direction.rotateYClockwise());

if (left && right) {
return state.with(TYPE, Type.MIDDLE);
} else if (left) {
return state.with(TYPE, Type.LEFT);
} else if (right) {
return state.with(TYPE, Type.RIGHT);
} else {
return state.with(TYPE, Type.SINGLE);
}
}

private boolean isRelatedBlock(WorldAccess world, BlockPos pos, Direction direction, Direction direction1) {
BlockState state = world.getBlockState(pos.offset(direction));
if (state.getBlock() == this) {
Direction blockDirection = state.get(FACING);
return blockDirection.equals(direction1);
}
return false;
}

public enum Type implements StringIdentifiable {
SINGLE("single"),
LEFT("left"),
MIDDLE("middle"),
RIGHT("right");

private final String name;
Type(String name) {
this.name = name;
}
@Override
public String asString() {
return name;
}
}
}

注册方块

随后我们要来注册方块,实例化我们的SimpleFence

1
public static final Block SIMPLE_FENCE = register("simple_fence", new SimpleFence(AbstractBlock.Settings.copy(Blocks.STONE).nonOpaque()));

加入物品栏

不要忘记加入物品栏

1
entries.add(ModBlocks.SIMPLE_FENCE);

数据文件

语言文件

1
translationBuilder.add(ModBlocks.SIMPLE_FENCE, "Simple Fence");

这里我们在语言文件中加入了我们的方块的名字

方块状态文件

这里因为我们引入了自定义的方块状态属性,就不能使用数据生成来写了,不过也许可以?

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
{
"variants": {
"facing=north,type=single": {
"model": "tutorialmod:block/simple_fence"
},
"facing=north,type=left": {
"model": "tutorialmod:block/simple_fence_left"
},
"facing=north,type=right": {
"model": "tutorialmod:block/simple_fence_right"
},
"facing=north,type=middle": {
"model": "tutorialmod:block/simple_fence_middle"
},
"facing=east,type=single": {
"model": "tutorialmod:block/simple_fence",
"y": 90
},
"facing=east,type=left": {
"model": "tutorialmod:block/simple_fence_left",
"y": 90
},
"facing=east,type=right": {
"model": "tutorialmod:block/simple_fence_right",
"y": 90
},
"facing=east,type=middle": {
"model": "tutorialmod:block/simple_fence_middle",
"y": 90
},
"facing=south,type=single": {
"model": "tutorialmod:block/simple_fence",
"y": 180
},
"facing=south,type=left": {
"model": "tutorialmod:block/simple_fence_left",
"y": 180
},
"facing=south,type=right": {
"model": "tutorialmod:block/simple_fence_right",
"y": 180
},
"facing=south,type=middle": {
"model": "tutorialmod:block/simple_fence_middle",
"y": 180
},
"facing=west,type=single": {
"model": "tutorialmod:block/simple_fence",
"y": 270
},
"facing=west,type=left": {
"model": "tutorialmod:block/simple_fence_left",
"y": 270
},
"facing=west,type=right": {
"model": "tutorialmod:block/simple_fence_right",
"y": 270
},
"facing=west,type=middle": {
"model": "tutorialmod:block/simple_fence_middle",
"y": 270
}
}
}

这就是我们的方块状态文件,我们根据FACINGTYPE来返回不同的方块模型

模型文件

我们的模型文件也是拿BlockBench制作的

一共是四个模型

simple_fence

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
44
45
46
47
48
49
50
51
52
53
{
"credit": "Made with Blockbench",
"textures": {
"0": "tutorialmod:block/simple_fence",
"particle": "tutorialmod:block/simple_fence"
},
"elements": [
{
"from": [7, 0, 7],
"to": [9, 16, 9],
"rotation": {"angle": 0, "axis": "y", "origin": [6, 0, 6]},
"faces": {
"north": {"uv": [0, 0, 2, 16], "texture": "#0"},
"east": {"uv": [2, 0, 4, 16], "texture": "#0"},
"south": {"uv": [4, 0, 6, 16], "texture": "#0"},
"west": {"uv": [6, 0, 8, 16], "texture": "#0"},
"up": {"uv": [10, 2, 8, 0], "texture": "#0"},
"down": {"uv": [10, 2, 8, 4], "texture": "#0"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 0],
"scale": [0.375, 0.375, 0.375]
},
"thirdperson_lefthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 0],
"scale": [0.375, 0.375, 0.375]
},
"firstperson_righthand": {
"rotation": [0, 45, 0],
"scale": [0.4, 0.4, 0.4]
},
"firstperson_lefthand": {
"rotation": [0, 225, 0],
"scale": [0.4, 0.4, 0.4]
},
"ground": {
"translation": [0, 3, 0],
"scale": [0.25, 0.25, 0.25]
},
"gui": {
"rotation": [30, 225, 0],
"scale": [0.625, 0.625, 0.625]
},
"fixed": {
"scale": [0.5, 0.5, 0.5]
}
}
}

simple_fence_left

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
{
"credit": "Made with Blockbench",
"textures": {
"0": "tutorialmod:block/simple_fence",
"particle": "tutorialmod:block/simple_fence"
},
"elements": [
{
"from": [7, 0, 7],
"to": [9, 16, 9],
"rotation": {"angle": 0, "axis": "y", "origin": [6, 0, 6]},
"faces": {
"north": {"uv": [0, 0, 2, 16], "texture": "#0"},
"east": {"uv": [2, 0, 4, 16], "texture": "#0"},
"south": {"uv": [4, 0, 6, 16], "texture": "#0"},
"west": {"uv": [6, 0, 8, 16], "texture": "#0"},
"up": {"uv": [10, 2, 8, 0], "texture": "#0"},
"down": {"uv": [10, 2, 8, 4], "texture": "#0"}
}
},
{
"from": [2, 0, 7],
"to": [4, 14, 9],
"rotation": {"angle": 0, "axis": "y", "origin": [2.5, 0, 6]},
"faces": {
"north": {"uv": [0, 0, 2, 16], "texture": "#0"},
"east": {"uv": [2, 0, 4, 16], "texture": "#0"},
"south": {"uv": [4, 0, 6, 16], "texture": "#0"},
"west": {"uv": [6, 0, 8, 16], "texture": "#0"},
"up": {"uv": [10, 2, 8, 0], "texture": "#0"},
"down": {"uv": [10, 2, 8, 4], "texture": "#0"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 0],
"scale": [0.375, 0.375, 0.375]
},
"thirdperson_lefthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 0],
"scale": [0.375, 0.375, 0.375]
},
"firstperson_righthand": {
"rotation": [0, 45, 0],
"scale": [0.4, 0.4, 0.4]
},
"firstperson_lefthand": {
"rotation": [0, 225, 0],
"scale": [0.4, 0.4, 0.4]
},
"ground": {
"translation": [0, 3, 0],
"scale": [0.25, 0.25, 0.25]
},
"gui": {
"rotation": [30, 225, 0],
"scale": [0.625, 0.625, 0.625]
},
"fixed": {
"scale": [0.5, 0.5, 0.5]
}
}
}

simple_fence_right

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
{
"credit": "Made with Blockbench",
"textures": {
"0": "tutorialmod:block/simple_fence",
"particle": "tutorialmod:block/simple_fence"
},
"elements": [
{
"from": [7, 0, 7],
"to": [9, 16, 9],
"rotation": {"angle": 0, "axis": "y", "origin": [6, 0, 6]},
"faces": {
"north": {"uv": [0, 0, 2, 16], "texture": "#0"},
"east": {"uv": [2, 0, 4, 16], "texture": "#0"},
"south": {"uv": [4, 0, 6, 16], "texture": "#0"},
"west": {"uv": [6, 0, 8, 16], "texture": "#0"},
"up": {"uv": [10, 2, 8, 0], "texture": "#0"},
"down": {"uv": [10, 2, 8, 4], "texture": "#0"}
}
},
{
"from": [12, 0, 7],
"to": [14, 14, 9],
"rotation": {"angle": 0, "axis": "y", "origin": [9.5, 0, 6]},
"faces": {
"north": {"uv": [0, 0, 2, 16], "texture": "#0"},
"east": {"uv": [2, 0, 4, 16], "texture": "#0"},
"south": {"uv": [4, 0, 6, 16], "texture": "#0"},
"west": {"uv": [6, 0, 8, 16], "texture": "#0"},
"up": {"uv": [10, 2, 8, 0], "texture": "#0"},
"down": {"uv": [10, 2, 8, 4], "texture": "#0"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 0],
"scale": [0.375, 0.375, 0.375]
},
"thirdperson_lefthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 0],
"scale": [0.375, 0.375, 0.375]
},
"firstperson_righthand": {
"rotation": [0, 45, 0],
"scale": [0.4, 0.4, 0.4]
},
"firstperson_lefthand": {
"rotation": [0, 225, 0],
"scale": [0.4, 0.4, 0.4]
},
"ground": {
"translation": [0, 3, 0],
"scale": [0.25, 0.25, 0.25]
},
"gui": {
"rotation": [30, 225, 0],
"scale": [0.625, 0.625, 0.625]
},
"fixed": {
"scale": [0.5, 0.5, 0.5]
}
}
}

simple_fence_middle

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
{
"credit": "Made with Blockbench",
"textures": {
"0": "tutorialmod:block/simple_fence",
"particle": "tutorialmod:block/simple_fence"
},
"elements": [
{
"from": [7, 0, 7],
"to": [9, 16, 9],
"rotation": {"angle": 0, "axis": "y", "origin": [6, 0, 6]},
"faces": {
"north": {"uv": [0, 0, 2, 16], "texture": "#0"},
"east": {"uv": [2, 0, 4, 16], "texture": "#0"},
"south": {"uv": [4, 0, 6, 16], "texture": "#0"},
"west": {"uv": [6, 0, 8, 16], "texture": "#0"},
"up": {"uv": [10, 2, 8, 0], "texture": "#0"},
"down": {"uv": [10, 2, 8, 4], "texture": "#0"}
}
},
{
"from": [12, 0, 7],
"to": [14, 14, 9],
"rotation": {"angle": 0, "axis": "y", "origin": [9.5, 0, 6]},
"faces": {
"north": {"uv": [0, 0, 2, 16], "texture": "#0"},
"east": {"uv": [2, 0, 4, 16], "texture": "#0"},
"south": {"uv": [4, 0, 6, 16], "texture": "#0"},
"west": {"uv": [6, 0, 8, 16], "texture": "#0"},
"up": {"uv": [10, 2, 8, 0], "texture": "#0"},
"down": {"uv": [10, 2, 8, 4], "texture": "#0"}
}
},
{
"from": [2, 0, 7],
"to": [4, 14, 9],
"rotation": {"angle": 0, "axis": "y", "origin": [2.5, 0, 6]},
"faces": {
"north": {"uv": [0, 0, 2, 16], "texture": "#0"},
"east": {"uv": [2, 0, 4, 16], "texture": "#0"},
"south": {"uv": [4, 0, 6, 16], "texture": "#0"},
"west": {"uv": [6, 0, 8, 16], "texture": "#0"},
"up": {"uv": [10, 2, 8, 0], "texture": "#0"},
"down": {"uv": [10, 2, 8, 4], "texture": "#0"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 0],
"scale": [0.375, 0.375, 0.375]
},
"thirdperson_lefthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 0],
"scale": [0.375, 0.375, 0.375]
},
"firstperson_righthand": {
"rotation": [0, 45, 0],
"scale": [0.4, 0.4, 0.4]
},
"firstperson_lefthand": {
"rotation": [0, 225, 0],
"scale": [0.4, 0.4, 0.4]
},
"ground": {
"translation": [0, 3, 0],
"scale": [0.25, 0.25, 0.25]
},
"gui": {
"rotation": [30, 225, 0],
"scale": [0.625, 0.625, 0.625]
},
"fixed": {
"scale": [0.5, 0.5, 0.5]
}
}
}

不想自己做模型的话就从这里复制吧

另外不要忘了对应的材质文件,而后,我们就可以启动游戏测试了