本篇教程的视频

本篇教程的源代码

Github地址:TutorialMod-PolishingMachine-1.21

介绍

前面我们简单抄了一个箱子,也就是可以储物的方块实体,这次我们来做一个可以处理物品的方块实体,也就是大部分工业模组中的常见的方块实体

这一次我们将完整实现一个带有GUI的方块实体,包括方块本身(Block)、方块实体(BlockEntity)、屏幕(Screen)、屏幕处理器(ScreenHandler)以及渲染器(Renderer)

而后面的教程将实现与这个方块实体适配的配方类型,而后适配REI

教程的篇幅会很长,请耐心看完

由于方块实体的实现较为复杂,请留意本篇教程中高亮显示的部分,避免遗漏

方块类

创建PolishingMachine类

首先我们创建一个PolishingMachine类,这个类继承自BlockWithEntity,并且实现BlockEntityProvider接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class PolishingMachine extends BlockWithEntity implements BlockEntityProvider {

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

@Override
protected MapCodec<? extends BlockWithEntity> getCodec() {
return null;
}

@Nullable
@Override
public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
return null;
}
}

实现super函数,另外也要重写两个方法,一个是getCodec,这是编解码器;另一个是createBlockEntity,这是创建方块实体的方法

BlockWithEntity这个类在原版中是很多方块实体的父类,我们前面抄的箱子,也是继承这个类的

我们按住Ctrl点击BlockWithEntity跳转到源代码,并在BlockWithEntity高亮显示时,按下Ctrl+H,可以查看它的作用域,即查看其子类

原版的方块实体其实也够我们研究一阵子了,当然,工业模组里的方块实体也是如此

设置编解码器

我们先来实现getCodec方法,这里需要一个编解码器,我们直接用最简单的写法即可

1
public static final MapCodec<PolishingMachine> CODEC = createCodec(PolishingMachine::new);

前面我们写箱子的时候,也是用的同样的语句

而后,我们在getCodec方法中返回这个编解码器

1
2
3
4
@Override
protected MapCodec<? extends BlockWithEntity> getCodec() {
return CODEC;
}

设置碰撞箱

我们此次制作的模型还是拿BlockBench做的,并不是一个完整的方块,所以我们需要自己设置碰撞箱

1
public static final VoxelShape SHAPE = Block.createCuboidShape(0, 0, 0, 16, 10, 16);

这是一个VoxelShapecreateCuboidShape里面的参数是方块起点终点的坐标,完整方块是0,0,016,16,16

我们设置了一个16*10*16的立方体,它的高度只有10个单位,比完整的方块少6个单位

随后重写getOutlineShape方法,返回这个VoxelShape

1
2
3
4
 @Override
protected VoxelShape getOutlineShape(BlockState state, BlockView world, BlockPos pos, ShapeContext context) {
return SHAPE;
}

设置渲染

和前面一样,我们还得设置这个方块的渲染,重写getRenderType方法

1
2
3
4
@Override
protected BlockRenderType getRenderType(BlockState state) {
return BlockRenderType.MODEL;
}

这里我们让其渲染为自身模型即可

设置状态改变

我们接下来重写onStateReplaced方法

1
2
3
4
@Override
protected void onStateReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) {

}

这里我们先空着,因为我们的方块实体还没写

这个方法是当我们的方块被破坏以后,如果说你的方块里面有东西,那它会将里面的东西掉落出来

比如原版的箱子,被破坏掉以后,箱子里面的东西都会被掉落出来

设置使用方法

再一个我们来写使用这个方块时的方法,重写onUse方法

1
2
3
4
@Override
protected ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, BlockHitResult hit) {

}

这个方法我们也见到很多次了吧,因为我们这里实现的是一个有GUI的方块,与玩家交互时,它得弹出一个GUI,我们就需要在这个方法里面设置

当然,因为我们方块实体和屏幕都没有写,所以这里也先空着

设置Ticker

最后我们还得重写一个getTicker方法

1
2
3
4
5
@Nullable
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(World world, BlockState state, BlockEntityType<T> type) {

}

每个游戏刻都会调用这个方法来更新方块实体的状态,该方法在客户端和服务端都会被调用

它可以验证传递的方块实体类型(BlockEntityType)是否是该方块期望的类型,以防止当前方块与方块实体之间冲突而导致的游戏崩溃

这里我们还没有方块实体,所以也先空着

ImplementedInventory

这里我们直接从FabricWiki上搬一个接口下来,ImplementedInventory,这个是用于实现简单的SidedInventory,帮助我们更好地写方块实体物品栏

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
/**
* A simple {@code SidedInventory} implementation with only default methods + an item list getter.
*
* <h2>Reading and writing to tags</h2>
* Use {@link Inventories#writeNbt(NbtCompound, DefaultedList, RegistryWrapper.WrapperLookup)} and {@link Inventories#readNbt(NbtCompound, DefaultedList, RegistryWrapper.WrapperLookup)}
* on {@linkplain #getItems() the item list}.
*
* License: <a href="https://creativecommons.org/publicdomain/zero/1.0/">CC0</a>
* @author Juuz
*/
@FunctionalInterface
public interface ImplementedInventory extends SidedInventory {
// 这个接口的作用是实现一个简单的SidedInventory,只有默认方法和一个获取物品列表的方法,便于我们在BlockEntity中使用
/**
* Gets the item list of this inventory.
* Must return the same instance every time it's called.
*
* @return the item list
*/
DefaultedList<ItemStack> getItems();

/**
* Creates an inventory from the item list.
*
* @param items the item list
* @return a new inventory
*/
static ImplementedInventory of(DefaultedList<ItemStack> items) {
return () -> items;
}

/**
* Creates a new inventory with the size.
*
* @param size the inventory size
* @return a new inventory
*/
static ImplementedInventory ofSize(int size) {
return of(DefaultedList.ofSize(size, ItemStack.EMPTY));
}

// SidedInventory

/**
* Gets the available slots to automation on the side.
*
* <p>The default implementation returns an array of all slots.
*
* @param side the side
* @return the available slots
*/
@Override
default int[] getAvailableSlots(Direction side) {
int[] result = new int[getItems().size()];
for (int i = 0; i < result.length; i++) {
result[i] = i;
}

return result;
}

/**
* Returns true if the stack can be inserted in the slot at the side.
*
* <p>The default implementation returns true.
*
* @param slot the slot
* @param stack the stack
* @param side the side
* @return true if the stack can be inserted
*/
@Override
default boolean canInsert(int slot, ItemStack stack, @Nullable Direction side) {
return true;
}

/**
* Returns true if the stack can be extracted from the slot at the side.
*
* <p>The default implementation returns true.
*
* @param slot the slot
* @param stack the stack
* @param side the side
* @return true if the stack can be extracted
*/
@Override
default boolean canExtract(int slot, ItemStack stack, Direction side) {
return true;
}

// Inventory

/**
* Returns the inventory size.
*
* <p>The default implementation returns the size of {@link #getItems()}.
*
* @return the inventory size
*/
@Override
default int size() {
return getItems().size();
}

/**
* @return true if this inventory has only empty stacks, false otherwise
*/
@Override
default boolean isEmpty() {
for (int i = 0; i < size(); i++) {
ItemStack stack = getStack(i);
if (!stack.isEmpty()) {
return false;
}
}

return true;
}

/**
* Gets the item in the slot.
*
* @param slot the slot
* @return the item in the slot
*/
@Override
default ItemStack getStack(int slot) {
return getItems().get(slot);
}

/**
* Takes a stack of the size from the slot.
*
* <p>(default implementation) If there are less items in the slot than what are requested,
* takes all items in that slot.
*
* @param slot the slot
* @param count the item count
* @return a stack
*/
@Override
default ItemStack removeStack(int slot, int count) {
ItemStack result = Inventories.splitStack(getItems(), slot, count);
if (!result.isEmpty()) {
markDirty();
}

return result;
}

/**
* Removes the current stack in the {@code slot} and returns it.
*
* <p>The default implementation uses {@link Inventories#removeStack(List, int)}
*
* @param slot the slot
* @return the removed stack
*/
@Override
default ItemStack removeStack(int slot) {
return Inventories.removeStack(getItems(), slot);
}

/**
* Replaces the current stack in the {@code slot} with the provided stack.
*
* <p>If the stack is too big for this inventory ({@link Inventory#getMaxCountPerStack()}),
* it gets resized to this inventory's maximum amount.
*
* @param slot the slot
* @param stack the stack
*/
@Override
default void setStack(int slot, ItemStack stack) {
getItems().set(slot, stack);
if (stack.getCount() > getMaxCountPerStack()) {
stack.setCount(getMaxCountPerStack());
}
markDirty();
}

/**
* Clears {@linkplain #getItems() the item list}}.
*/
@Override
default void clear() {
getItems().clear();
}

@Override
default void markDirty() {
// Override if you want behavior.
}

@Override
default boolean canPlayerUse(PlayerEntity player) {
return true;
}
}

本身源代码里面也有注释,可以自己看看

数据类

创建BlockPosPayload类

这个类是一个接口,用于传递BlockPos,用于待会写的网络通信的编解码器

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/*
* This file is part of RebornCore, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 TeamReborn
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package com.besson.tutorialmod.data;

import net.minecraft.block.entity.BlockEntity;
import net.minecraft.block.entity.BlockEntityType;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.registry.Registries;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.math.BlockPos;

import java.util.function.Predicate;

public interface BlockPosPayload {
BlockPos pos();

default boolean isWithinDistance(PlayerEntity player,double distance){
return player.getBlockPos().isWithinDistance(pos(), distance);
}

default boolean canUse(ServerPlayerEntity player, Predicate<ScreenHandler> screenHandlerPredicate) {
ScreenHandler currentScreenHandler = player.currentScreenHandler;

if (currentScreenHandler == null) {
return false;
}
if (!screenHandlerPredicate.test(currentScreenHandler)) {
return false;
}
return currentScreenHandler.canUse(player);
}

default <T extends BlockEntity> T getBlockEntity(BlockEntityType<T> type, PlayerEntity player) {
if (!isWithinDistance(player, 64)) {
throw new IllegalStateException("Player cannot use this block entity as its too far away");
}
BlockEntity blockEntity = getBlockEntity(player);
if (type != blockEntity.getType()) {
throw new IllegalStateException("Block entity is not of the correct type. Expected: " +
Registries.BLOCK_ENTITY_TYPE.getId(type) + " but got: " + Registries.BLOCK_ENTITY_TYPE.getId(blockEntity.getType()));
}
return (T) blockEntity;
}

default <T extends BlockEntity> T getBlockEntity(Class<T> baseClass, PlayerEntity player) {
if (!isWithinDistance(player, 64)) {
throw new IllegalStateException("Player cannot use this block entity as its too far away");
}

BlockEntity blockEntity = getBlockEntity(player);

if (!baseClass.isInstance(blockEntity)) {
throw new IllegalStateException("Block entity is not of the correct class");
}

//noinspection unchecked
return (T) blockEntity;
}

default BlockEntity getBlockEntity(PlayerEntity player) {
if (!isWithinDistance(player, 64)) {
throw new IllegalStateException("Player cannot use this block entity as its too far away");
}

BlockEntity blockEntity = player.getWorld().getBlockEntity(pos());

if (blockEntity == null) {
throw new IllegalStateException("Block entity is null");
}

return blockEntity;
}
}

当然,这里是搬了科技复兴(Tech Reborn)的核心库里面的一个接口,用于传递BlockPos,我们这里也是直接拿来用,遵循其MIT协议

因为我其实对网络通信并不清楚,而1.21又大改一通,这下只能借助强大的开源社区了qwq

创建PolishingMachineData类

这个是待会用到的用于数据传输的类,因为在1.21里面,源代码又改了,现在需要我们自己去实现这个数据类,所以我们需要自己写一个

这个是用于网络通信的数据编解码器

1
2
3
4
public record PolishingMachineData(BlockPos pos) implements BlockPosPayload {
public static final PacketCodec<RegistryByteBuf, PolishingMachineData> CODEC=
PacketCodec.tuple(BlockPos.PACKET_CODEC, PolishingMachineData::pos, PolishingMachineData::new);
}

这个类是一个record类,它的作用是传递BlockPos,实现了BlockPosPayload接口

PacketCodec是一个编解码器,它的作用是将数据编码成ByteBuf,然后传输给客户端或服务端

这里我们传递的是BlockPos,所以我们用BlockPos.PACKET_CODEC来编解码BlockPos

方块实体

创建PolishingMachineBlockEntity类

接下来我们就可以写方块实体了,我们创建一个PolishingMachineBlockEntity类,这个类继承自BlockEntity,并且实现ImplementedInventory接口和ExtendedScreenHandlerFactory<PolishingMachineData>接口

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
public class PolishingMachineBlockEntity extends BlockEntity implements ExtendedScreenHandlerFactory<PolishingMachineData>, ImplementedInventory {

public PolishingMachineBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
super(type, pos, state);
}

@Override
public DefaultedList<ItemStack> getItems() {
return null;
}

@Override
public Text getDisplayName() {
return null;
}

@Nullable
@Override
public ScreenHandler createMenu(int syncId, PlayerInventory playerInventory, PlayerEntity player) {
return null;
}

@Override
public PolishingMachineData getScreenOpeningData(ServerPlayerEntity player) {
return null;
}
}

写好构造函数,实现一些要重写的方法

这里的ExtendedScreenHandlerFactory<PolishingMachineData>接口就是在1.21中改动的地方,现在它要求你自己写一个数据类,用于传递数据

另外,ExtendedScreenHandlerFactory是包括了网络通信的,这样我们就不用自己单独写网络发包了

创建物品槽

我们来创建方块实体中的物品槽,用于存放被处理的物品处理后的物品

1
private final DefaultedList<ItemStack> inventory = DefaultedList.ofSize(2, ItemStack.EMPTY);

这是一个DefaultedList,它的作用是创建一个ItemStack的列表,这里我们创建了一个大小为2的列表,用于存放两个物品,默认为空

1
2
private static final int INPUT_SLOT = 0;
private static final int OUTPUT_SLOT = 1;

接下来设置两个槽的索引一定是从0开始),一个是输入槽,一个是输出槽,名字无所谓,只要你自己能够理解即可

设置加工进度

我们的方块实体是一个加工机,所以我们需要一个加工进度,用于表示物品的加工进度

1
2
3
protected final PropertyDelegate propertyDelegate;
private int progress = 0;
private int maxProgress = 72;

这里我们设置了一个PropertyDelegate,它的作用是用于同步数据,这里我们用于同步加工进度

下面的两个变量是加工进度最大加工进度,这里我们设置了一个72,也就是3.6s

初始化加工进度的同步

我们在构造函数中初始化propertyDelegate,设置加工进度的同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
this.propertyDelegate = new PropertyDelegate() {

@Override
public int get(int index) {
return switch (index) {
case 0 -> PolishingMachineBlockEntity.this.progress;
case 1 -> PolishingMachineBlockEntity.this.maxProgress;
default -> 0;
};
}

@Override
public void set(int index, int value) {
switch (index) {
case 0 -> PolishingMachineBlockEntity.this.progress = value;
case 1 -> PolishingMachineBlockEntity.this.maxProgress = value;
}
}

@Override
public int size() {
return 2;
}
};

这里我们重写了PropertyDelegate的三个方法,get用于获取数据,set用于设置数据,size用于设置数据的数量

重写getItems方法

我们重写getItems方法,返回我们的物品槽列表

1
2
3
4
@Override
public DefaultedList<ItemStack> getItems() {
return this.inventory;
}

重写getDisplayName方法

我们重写getDisplayName方法,返回我们的方块实体显示在GUI上的名字

1
2
3
4
@Override
public Text getDisplayName() {
return Text.translatable("container.polishing_machine");
}

这个待会会拿语言文件翻译

重写getScreenOpeningData方法

我们重写getScreenOpeningData方法,返回我们的数据类

1
2
3
4
@Override
public PolishingMachineData getScreenOpeningData(ServerPlayerEntity player) {
return new PolishingMachineData(pos);
}

重写writeNbtreadNbt方法

这两个方法是用于数据的读写,当我们保存世界的时候,如果方块实体里面有东西,那么我们需要将这些东西保存下来,当我们加载世界的时候,我们需要将这些东西读取出来

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
super.writeNbt(nbt, registryLookup);
Inventories.writeNbt(nbt, this.inventory, false, registryLookup);
nbt.putInt("polishing_machine", progress);
}

@Override
protected void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
super.readNbt(nbt, registryLookup);
Inventories.readNbt(nbt, this.inventory, registryLookup);
progress = nbt.getInt("polishing_machine");
}

这里面的参数写法可以参考源代码的,较比1.20也是有所改动的

重写getMaxCountPerStack方法

这个方法是用于设置物品槽中物品的最大数量

1
2
3
4
@Override
public int getMaxCountPerStack() {
return 64;
}

这里我们设置为64,也就是一个物品槽中最多可以放64个物品

重写tick方法

这个方法是用于更新当前方块实体的状态,每个游戏刻都会调用这个方法,这也是方块实体的特性

正因为有了tick方法,我们才能实现方块实体的加工进度物品处理等功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void tick(World world, BlockPos pos, BlockState state) {
if (world.isClient()) {
return;
}
if (isOutputSlotAvailable()) {
if (hasRecipe()) {
increaseCraftProgress();
markDirty(world, pos, state);

if (hasCraftingFinished()) {
craftItem();
resetProgress();
}
} else {
resetProgress();
}
} else {
resetProgress();
markDirty(world, pos, state);
}
}

这里全是自定义的方法,我们需要自己实现,不过我们来捋一捋这里面的逻辑

  1. 首先判断是否为服务端,因为运算基本上是在服务端进行的
  2. 随后判断输出槽是否有空位,如果有空位,那么我们就可以进行加工,没有空位的话,我们就不进行加工,直接重置加工进度
  3. 接下来判断是否符合配方(虽然上面写的是has,更准确来讲应该是符不符合),如果符合配方,那么我们就增加加工进度,然后保存方块实体的状态
  4. 再接下来判断加工进度是否达到最大值,如果达到最大值,那么我们就加工物品,输出相应的物品,然后重置加工进度,开始下一轮加工

这里的逻辑就大体是这样,其实并不复杂,那么接下来我们就来实现这些方法

实现isOutputSlotAvailable方法

这个方法是用于判断输出槽是否有空位

1
2
3
4
private boolean isOutputSlotAvailable() {
return this.getStack(OUTPUT_SLOT).isEmpty() ||
this.getStack(OUTPUT_SLOT).getCount() <= this.getMaxCountPerStack();
}

这里我们判断输出槽是否为空,或者输出槽的物品数量是否小于最大数量

实现hasRecipe方法

1
2
3
4
5
private boolean hasRecipe() {
ItemStack result = new ItemStack(ModItems.ICE_ETHER);
boolean hasInput = getStack(INPUT_SLOT).getItem() == Items.ICE;
return hasInput && canInsertAmountIntoOutputSlot(result) && canInsertIntoOutputSlot(result.getItem());
}

这个方法是用于判断是否符合配方

这里我们是硬编码的,在学习配方类型之后,我们就可以用json文件来定义我们的配方了

我们的原料是原版的ICE,我们的产物是我们模组的ICE_ETHER

canInsertAmountIntoOutputSlotcanInsertIntoOutputSlot是两个自定义的方法,用于判断是否可以将物品插入到输出槽中(主要是判断输出槽是不是满的)

实现canInsertAmountIntoOutputSlotcanInsertIntoOutputSlot方法

1
2
3
4
5
6
7
8
private boolean canInsertIntoOutputSlot(Item item) {
return this.getStack(OUTPUT_SLOT).isEmpty() ||
this.getStack(OUTPUT_SLOT).getItem() == item;
}

private boolean canInsertAmountIntoOutputSlot(ItemStack result) {
return this.getStack(OUTPUT_SLOT).getCount() + result.getCount() <= this.getMaxCountPerStack();
}

一个是判断输出槽是否为空或输出槽内已有物品是否为同一个

后面一个是判断堆叠数量是否超过上限

实现increaseCraftProgress方法

1
2
3
private void increaseCraftProgress() {
this.progress++;
}

这个方法是用于增加加工进度,自增即可

实现hasCraftingFinished方法

1
2
3
private boolean hasCraftingFinished() {
return this.progress >= this.maxProgress;
}

判断当前加工进度是否达到最大值,如果是,那么就返回true,也就是加工完成

实现craftItem方法

1
2
3
4
5
private void craftItem() {
ItemStack result = new ItemStack(ModItems.ICE_ETHER);
this.setStack(OUTPUT_SLOT, new ItemStack(result.getItem(), getStack(OUTPUT_SLOT).getCount() + result.getCount()));
this.removeStack(INPUT_SLOT, 1);
}

这里我们还是硬编码的,我们将ICE_ETHER放入输出槽中,数量为1

输出时,如果输出槽内有物品,则与输出槽内已有的相同物品合并数量

然后我们将输入槽中的物品减少1(视频教程忘记写了,导致出现了视频最后ICE不消耗的情况,看来我还是适合写bug(bushi))

实现resetProgress方法

1
2
3
private void resetProgress() {
this.progress = 0;
}

直接将加工进度重置为0即可

注册方块实体

当然我们这里还有一个createMenu方法没有重写,这个放到之后写好屏幕屏幕处理再说

那么现在我们来注册我们的方块实体

1
2
public static final BlockEntityType<PolishingMachineBlockEntity> POLISHING_MACHINE_BLOCK_ENTITY = create("polishing_machine_block_entity",
BlockEntityType.Builder.create(PolishingMachineBlockEntity::new, ModBlocks.POLISHING_MACHINE));

注册方法是和我们前面的一样的

记得注册方块

注册方块

1
2
public static final Block POLISHING_MACHINE = register("polishing_machine",
new PolishingMachine(AbstractBlock.Settings.copy(Blocks.STONE)));

方块的注册还是像往常一样

但是我们现在回到ModBlockEntities中,发现它还有报错

这个时候,我们还得改写PolishingMachineBlockEntity的构造函数

1
2
3
4
5
6
public PolishingMachineBlockEntity(BlockPos pos, BlockState state) {

super(ModBlockEntities.POLISHING_MACHINE_BLOCK_ENTITY, pos, state);

...
}

这里我们将原有的type参数去掉,直接调用我们刚刚注册的BlockEntityType,即ModBlockEntities.POLISHING_MACHINE_BLOCK_ENTITY

回到方块类

我们刚刚还剩下几个方法没有重写,现在我们来重写

重写createBlockEntity方法

1
2
3
4
5
@Nullable
@Override
public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
return new PolishingMachineBlockEntity(pos, state);
}

这里我们返回我们刚刚注册的方块实体

重写onStateReplaced方法

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onStateReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) {
if (state.getBlock() != newState.getBlock()) {
BlockEntity blockEntity = world.getBlockEntity(pos);
if (blockEntity instanceof PolishingMachineBlockEntity) {
ItemScatterer.spawn(world, pos, (PolishingMachineBlockEntity) blockEntity);
world.updateComparators(pos, this);
}
super.onStateReplaced(state, world, pos, newState, moved);
}
}

这个方法是用于方块被破坏时,将方块实体内的物品掉落出来

当然这里还有一个更新比较器的方法

重写getTicker方法

1
2
3
4
5
6
@Nullable
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(World world, BlockState state, BlockEntityType<T> type) {
return validateTicker(type, ModBlockEntities.POLISHING_MACHINE_BLOCK_ENTITY,
(world1, pos, state1, blockEntity) -> blockEntity.tick(world1, pos, state1));
}

这里调用的其实就是我们方块实体的tick方法

onUse也得等到我们将屏幕屏幕处理写好之后再写

那么接下来就是写屏幕屏幕处理

创建三个类

事先说明:我们采用的GUI是和原版一样的256×256分辨率的材质,所以有些地方并不需要自己设置,比如每个小格子的大小、GUI的渲染终点等等

如果说你要采用不同分辨率的材质,那就需要自己设置相关的内容

我们需要创建三个类,一个是屏幕,一个是屏幕处理,一个是屏幕处理注册

屏幕是用于显示GUI的,就是它的渲染。GUI的材质是什么,背景怎么渲染等等这些是在这个类中完成的

屏幕处理是用于处理GUI的,我们要在里面告诉电脑那些区域是我们的物品槽,毕竟电脑不是人,我们是看一眼就知道物品槽在这在那,但电脑不知道,所以我们要告诉它

屏幕处理注册是用于注册屏幕处理

创建PolishingMachineScreenHandler类

创建一个PolishingMachineScreenHandler类,这个类继承自ScreenHandler,并且重写quickMovecanUse方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PolishingMachineScreenHandler extends ScreenHandler {

public PolishingMachineScreenHandler(@Nullable ScreenHandlerType<?> type, int syncId) {
super(type, syncId);
}

@Override
public ItemStack quickMove(PlayerEntity player, int slot) {
return null;
}

@Override
public boolean canUse(PlayerEntity player) {
return false;
}
}

先创建着,后面再来填充

创建ModScreenHandlers类

创建一个ModScreenHandlers类,用于注册我们的屏幕处理

1
2
3
public class ModScreenHandlers {

}

当然这里的注册方法什么的待会再说

创建PolishingMachineScreen类

创建一个PolishingMachineScreen类,这个类继承自HandledScreen,泛型是我们刚刚创建的PolishingMachineScreenHandler,并且重写drawBackground方法

1
2
3
4
5
6
7
8
9
10
11
public class PolishingMachineScreen extends HandledScreen<PolishingMachineScreenHandler> {

public PolishingMachineScreen(PolishingMachineScreenHandler handler, PlayerInventory inventory, Text title) {
super(handler, inventory, title);
}

@Override
protected void drawBackground(DrawContext context, float delta, int mouseX, int mouseY) {

}
}

PolishingMachineScreen

我们先来写屏幕

GUI材质

我们来指定GUI的材质

1
private static final Identifier TEXTURE = Identifier.of(TutorialMod.MOD_ID, "textures/gui/polishing_machine_gui.png");

命名空间,后面加上我们材质文件的路径,这里文件的后缀名也得带上

重写drawBackground方法

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void drawBackground(DrawContext context, float delta, int mouseX, int mouseY) {
RenderSystem.setShader(GameRenderer::getPositionTexProgram);
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
RenderSystem.setShaderTexture(0, TEXTURE);
int x = (this.width - this.backgroundWidth) / 2;
int y = (this.height - this.backgroundHeight) / 2;

context.drawTexture(TEXTURE, x, y, 0, 0, backgroundWidth, backgroundHeight);

renderProgressArrow(context, x, y);
}

这里渲染的是GUI的背景,一般的,在游戏中,我们打开GUI时,背景是半透明的黑色

setShader是获取游戏的渲染器

setShaderColor是设置背景的RGBA值,不过我们这里设置成了不透明,也就是Alpha的值为1

setShaderTexture是设置要渲染的材质

下面的xy是渲染GUI的位置,在整个游戏窗口中的位置

drawTexture是渲染GUI,里面的两个0是要渲染的材质文件的起始坐标,也就是从我们的材质文件的左上角开始渲染

下面还有一个renderProgressArrow方法,这个方法是用于渲染加工进度的箭头的

实现renderProgressArrow方法

1
2
3
4
5
private void renderProgressArrow(DrawContext context, int x, int y) {
if (handler.isCrafting() && handler.isRaining()) {
context.drawTexture(TEXTURE, x + 85, y + 30, 176, 0, 8, handler.getScaledProgress());
}
}

不过这里涉及到屏幕处理程序里面的方法,先让它报错着,我们待会再写

一个是判断其是否正在加工,一个是判断当前世界是否正在下雨(当然,你可以自定义)

这里要实现的就是像熔炉那样的加工进度的箭头,也就是它在加工过程中,箭头会变化

下面的drawTexture和上面的是不一样,不要搞混了

x + 85y + 30是我们材质文件里的箭头起始的坐标

1760要替换的箭头的起始坐标,8是宽度,handler.getScaledProgress()是高度(我们的箭头是竖着的),不过这里也是屏幕处理程序的方法

重写render方法

这个我在教程中忘记讲了,不过好像也不影响

1
2
3
4
5
6
@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
renderBackground(context, mouseX, mouseY, delta);
super.render(context, mouseX, mouseY, delta);
drawMouseoverTooltip(context, mouseX, mouseY);
}

这里让它渲染背景显示提示信息

PolishingMachineScreenHandler

接下来我们就来写屏幕处理程序

定义变量

1
2
3
private final Inventory inventory;
private final PropertyDelegate propertyDelegate;
public final PolishingMachineBlockEntity blockEntity;

分别是物品栏同步数据(加工进度)和方块实体

然后我们改写构造函数,初始化这些方法

1
2
3
4
5
6
7
8
9
public PolishingMachineScreenHandler(@Nullable ScreenHandlerType<?> type, int syncId, PlayerInventory playerInventory, PropertyDelegate propertyDelegate, BlockEntity blockEntity) {
super(type, syncId);
checkSize((Inventory) blockEntity, 2);
this.inventory = (Inventory) blockEntity;
inventory.onOpen(playerInventory.player);

this.propertyDelegate = propertyDelegate;
this.blockEntity = (PolishingMachineBlockEntity) blockEntity;
}

这里我们将形参中的Inventory改为PlayerInventoryPolishingMachineBlockEntity改为BlockEntity

里面我们写上checkSize方法,这个是用于检查当前方块实体的物品槽数量是否与预期的一致(在这个例子中,预期的是2

(Inventory) blockEntity赋值给inventory

inventory.onOpen是打开物品栏时调用

还有两个也是常规的赋值

另外我们在这个构造函数中加入其他的方法

1
2
3
4
5
6
7
this.addSlot(new Slot(inventory, 0, 80, 11));
this.addSlot(new Slot(inventory, 1, 80, 59));

addPlayerInventory(playerInventory);
addPlayerHotbar(playerInventory);

addProperties(propertyDelegate);

首先加入我们方块实体的两个槽,第二个参数是索引值,后面两个起始坐标

addPlayerInventoryaddPlayerHotbar是添加玩家的物品栏快捷栏

addProperties是添加同步的数据

实现addPlayerInventoryaddPlayerHotbar方法

这两个其实去源代码搬一下就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
private void addPlayerHotbar(PlayerInventory playerInventory) {
for (int i = 0; i < 9; ++i) {
this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 142));
}
}

private void addPlayerInventory(PlayerInventory playerInventory) {
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 9; ++j) {
this.addSlot(new Slot(playerInventory, j + i * 9 + 9, 8 + j * 18, 84 + i * 18));
}
}
}

其实就是循环遍历,加格子

重写quickMove方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public ItemStack quickMove(PlayerEntity player, int slot) {
ItemStack newStack = ItemStack.EMPTY;
Slot invSlot = this.slots.get(slot);
if (invSlot != null && invSlot.hasStack()) {
ItemStack originalStack = invSlot.getStack();
newStack = originalStack.copy();
if (slot < this.inventory.size()) {
if (!this.insertItem(originalStack, this.inventory.size(), this.slots.size(), true)) {
return ItemStack.EMPTY;
}
} else if (!this.insertItem(originalStack, 0, this.inventory.size(), false)) {
return ItemStack.EMPTY;
}

if (originalStack.isEmpty()) {
invSlot.setStack(ItemStack.EMPTY);
} else {
invSlot.markDirty();
}
}
return newStack;
}

这个是物品在各个物品栏之间的快速移动,也照搬源代码即可

重写canUse方法

1
2
3
4
@Override
public boolean canUse(PlayerEntity player) {
return this.inventory.canPlayerUse(player);
}

这个是用于判断玩家是否可以使用这个物品栏

实现isCraftingisRaining方法

1
2
3
4
5
6
7
public boolean isCrafting() {
return propertyDelegate.get(0) > 0;
}

public boolean isRaining() {
return blockEntity.getWorld().isRaining();
}

这两个方法是用于判断是否正在加工是否正在下雨

实现getScaledProgress方法

1
2
3
4
5
6
public int getScaledProgress() {
int progress = propertyDelegate.get(0);
int maxProgress = propertyDelegate.get(1);
int progressArrowSize = 26;
return maxProgress != 0 && progress != 0 ? progress * progressArrowSize / maxProgress : 0;
}

这个方法是用于计算加工进度的箭头的高度,我们箭头的长度是26

根据当前加工进度最大加工进度的比例,计算出箭头的高度

在此之后,屏幕类中的报错就没有了

ModScreenHandlers

接下来我们来注册我们的屏幕处理

注册屏幕处理

1
2
3
public static final ScreenHandlerType<PolishingMachineScreenHandler> POLISHING_MACHINE_SCREEN_HANDLER =
Registry.register(Registries.SCREEN_HANDLER, Identifier.of(TutorialMod.MOD_ID, "polishing_machine"),
new ExtendedScreenHandlerType<>(PolishingMachineScreenHandler::new, PolishingMachineData.CODEC));

这里我们注册了一个屏幕处理,并且传入了我们的屏幕处理数据编解码器

但是现在它会报错,这个时候我们还得写一个PolishingMachineScreenHandler的构造函数

1
2
3
public PolishingMachineScreenHandler(int syncId, PlayerInventory playerInventory, PolishingMachineData data) {
this(syncId, playerInventory, new ArrayPropertyDelegate(2), playerInventory.player.getWorld().getBlockEntity(data.pos()));
}

这个构造函数就可以被我们的注册方法调用了,这里的报错也就解决了

初始化注册方法

和常规一样,不要忘了初始化注册方法及其主类的调用

1
2
3
public static void registerScreenHandlers() {

}

调用注册方法

1
ModScreenHandlers.registerScreenHandlers();

方块类onUse方法

现在我们回到方块类,来写onUse方法

1
2
3
4
5
6
7
8
9
10
@Override
protected ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, BlockHitResult hit) {
if (!world.isClient()){
NamedScreenHandlerFactory screenHandlerFactory = (PolishingMachineBlockEntity) world.getBlockEntity(pos);
if (screenHandlerFactory != null) {
player.openHandledScreen(screenHandlerFactory);
}
}
return ActionResult.SUCCESS;
}

这个方法是用于打开GUI的,当玩家右键点击方块时,打开GUI

这里我们判断是否为服务端,然后获取方块实体,如果方块实体不为空,那么就打开GUI

方块实体类createMenu方法

接下来我们来写方块实体createMenu方法

1
2
3
4
5
@Nullable
@Override
public ScreenHandler createMenu(int syncId, PlayerInventory playerInventory, PlayerEntity player) {
return new PolishingMachineScreenHandler(syncId, playerInventory, this.propertyDelegate, this);
}

这个方法是用于创建屏幕处理,我们在这里传入了同步数据方块实体

客户端注册屏幕渲染

接下来还有一件事是注册客户端的屏幕渲染,因为渲染一事是在客户端进行的

我们到TutorialModClient中,注册我们的屏幕渲染

1
HandledScreens.register(ModScreenHandlers.POLISHING_MACHINE_SCREEN_HANDLER, PolishingMachineScreen::new);

这里我们注册了我们的屏幕处理屏幕,这样我们的GUI就可以显示出来了

数据文件

最后的最后,收尾工作了

语言文件

1
2
translationBuilder.add(ModBlocks.POLISHING_MACHINE, "Polishing Machine");
translationBuilder.add("container.polishing_machine", "Polishing Machine");

这里我们添加了方块GUI的翻译

模型文件

1
blockStateModelGenerator.registerSimpleState(ModBlocks.POLISHING_MACHINE);

因为是拿BlockBench做的方块模型,所以这里就写最简单的方块状态即可

材质文件

GUI的话就是我们前面写的路径,方块的材质放在对应的文件夹下面即可

最后也感谢大家看完,虽然我也写得半条命没了