本篇教程的视频

(待发布)

本篇教程的源代码

(待发布)

本篇教程目标

初步完成一个真正意义上的方块实体

介绍

那么从本篇教程开始,我们就进入了方块实体的小系列

在这个小系列中,我们会讲解包括矿机精炼炉灌装机,这三个案例分别对应无输入单输出单输入单输出多输入单输出,也是最基本的方块实体了

未来还会根据终末地工业模组的开发继续追加教程(比如流体工业)

每个案例实际上还包括它的 GUI 和 自定义配方类型 ,所以这部分的内容确实很多,大家慢慢消化

三个案例之后,我们再来讲点其他的,比如物流电网,不过还是终末地工业的案例,毕竟只有我自己写的我才最熟悉,那么具体的就看后续的教程吧

虽然我很想讲一些传统工业模组中的方块实体,但我确实不太熟悉,终末地模组开发的时候我是直接按照我之前学的还有我自己的想法写的,不过,整体思路应该不会差很多

另外,因为终末地工业模组的轻量级工业定位,也确实适合拿来做教程(太过复杂的我可能可以写出来,但可能写不好教程,因为会很乱)

目标分析

本篇的案例是矿机,它是一个无输入单输出的方块实体,玩家把它放在矿石上面,它就可以开采矿物(在这篇教程中,我们就先只开采钻石矿)

除此之外,我们还要给它配置GUI,这样玩家就有一个面板来拿取开采出来的矿物

在后面的教程中,我们还将配置自定义配方REI 适配等,不过这期教程就简单一点好了

注意!由于本篇教程直接接续前面的 GeckoLib 教程,方块实体的创建和注册这一块将会直接略过,如果你跳过了前面的教程,也可以回过头去看看创建方块实体和注册方块实体相关内容

方块实体类 BlockEntity

那么接下来我们先来完善方块实体的逻辑,这部分的内容是最核心的,请大家仔细阅读

实现 ExtendedScreenHandlerFactory 接口

因为我们要为方块实体配置GUI,而ExtendedScreenHandlerFactory接口是Fabric为我们封装好的一个接口,可以在打开GUI时传输其他数据,就不用我们额外得去写网络包了(当然后续复杂的网络通讯还是得我们自己写)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class PortableOriginiumRigBlockEntity extends BlockEntity implements GeoBlockEntity, ExtendedScreenHandlerFactory {
...

@Override
public void writeScreenOpeningData(ServerPlayerEntity player, PacketByteBuf buf) {
buf.writeBlockPos(this.pos);
}

@Override
public Text getDisplayName() {
return Text.translatable("blockEntity.portable_originium_rig");
}

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

另外还需要重写三个方法:writeScreenOpeningDatagetDisplayNamecreateMenu

getDisplayName方法是获取显示在GUI上的名字,这里我们用Text.translatable方法来创建,后面用语言文件来翻译

createMenu方法用于获取GUIScreenHandler,这里我们暂时返回null,因为我们还没写

writeScreenOpeningData方法这里我们写了一个用于传递方块实体位置的语句,因为在前传中我也讲过,方块实体是由 World(按区块/坐标)管理的,World使用BlockPos作为查找键,简单来说就是用来找到方块实体的

添加 PropertyDelegate

那么有人可能会问了,除了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
public final PropertyDelegate propertyDelegate;
public int progress = 0;
public int maxProgress = 60;

public PortableOriginiumRigBlockEntity(BlockPos pos, BlockState state) {
...
this.propertyDelegate = new PropertyDelegate() {
@Override
public int get(int index) {
return switch (index) {
case 0 -> PortableOriginiumRigBlockEntity.this.progress;
case 1 -> PortableOriginiumRigBlockEntity.this.maxProgress;
default -> 0;
};
}

@Override
public void set(int index, int value) {

}

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

我们创建了一个PropertyDelegate对象,并在构造函数中完成初始化,这个对象可以获取和设置方块实体中的数据,这个变量是向GUI传递数据的,如果我们需要显示箭头这种动态的数据,那么我们就需要使用这个对象

PropertyDelegate对象是一个接口,我们需要实现getset方法,这里我们分别返回progressmaxProgress

注意你有多少个数据传递,在size()方法返回就是几个,不要少写,不然可能就无法正确传递

总结一下 网络包 和 PropertyDelegate:

总的来说,一般情况下,我们需要把BlockPos通过writeScreenOpeningData发给客户端以便双方(客户端和服务端)能引用同一世界对象,与此同时,也可以拿到方块实体的PropertyDelegate数据了

然后我们把需要持续显示的数值(例如进度)放在PropertyDelegate里由ScreenHandler自动同步

但是如果未来有复杂或大体量的数据我们还需要使用BlockEntityNBT同步或自定义网络包来处理

添加 输出槽

那么既然我们的方块实体要输出物品,那么我们就需要添加一个输出槽

不过这里我们将使用FabricTransfer API来编写相关逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected final SimpleInventory outputInv = new SimpleInventory(1) {
@Override
public void markDirty() {
super.markDirty();
PortableOriginiumRigBlockEntity.this.markDirty();
}

@Override
public boolean isValid(int slot, ItemStack stack) {
return false;
}
};

protected final InventoryStorage outputStorage = InventoryStorage.of(outputInv, null);

一个是SimpleInventory类型的输出槽,这是用于方块实体内部处理逻辑的

另一个是InventoryStorage,来自Transfer API,虽然会有警告,但不影响,这是用于暴露给外部的接口,也就是供其他可以传输物品的方块使用,比如漏斗这种(机械动力的漏斗也是)

关于这个Transfer API,简单来说它就是Fabric版的Capability,但很遗憾,因为一些众所周知的原因,它的生态没有像ForgeNeoForge原生的Capability那么强,至今还是实验性的功能(不影响使用)

1
2
3
public SimpleInventory getOutputInv() {
return this.outputInv;
}

这里我们再写一个方法,让GUI能获取outputInv

我们还需要写个方法,用于外部获取outputStorage

1
2
3
4
@Nullable
public Storage<ItemVariant> getStorage() {
return outputStorage;
}

这个方法会在之后介绍

添加 tick 逻辑

在此之前我们再添加一个变量,这个变量将用于控制我们模型的动画

1
protected boolean isWorking;

那么接下来就是最重要的tick逻辑了,这是让我们的方块实体真正能够工作的关键方法,后面还要到方块类里面去调用这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void tick(World world, BlockPos pos,  BlockState state, PortableOriginiumRigBlockEntity be) {
if (world.isClient()) return;

boolean canProcess = be.hasCorrectRecipe(world);

if (canProcess) {
be.incrementProgress();

if (be.hasProgressingFinished()) {
be.craftItem(world);
be.resetProgress();
}
} else {
be.resetProgress();
}

if (be.isWorking != canProcess) {
be.isWorking = canProcess;
world.updateListeners(pos, state, state, 3);
}

be.markDirty();
}

现在这个方法中,有很多需要我们自己写的方法,不过看这些方法名,大家应该知道它有什么作用,后面我们一点点完善这些方法

这里我们将各部分功能拆开来,也是一种模块化的思想,这样也方便复用一些代码

简单解释一下这里的逻辑:

  1. 首先判断当前世界是否是服务端,因为运算由服务端完成,客户端不需要参与;
  2. 判断是否有有效的配方(在本篇中我们还是硬编码,后面再加上自定义的配方类型),把这个布尔值同步给isWorking,用于在外部控制动画;
  3. 如果可用,则开始工作,并累加progress;否则重置progress
  4. progress达到最大值时,产出物品,并重置progress
  5. markDirty方法用于触发数据同步,保存关键数据。

接下来我们就来完善各个方法

hasCorrectRecipe

1
2
3
4
5
6
7
8
9
10
private boolean hasCorrectRecipe(World world) {
BlockState belowState = world.getBlockState(pos.down());
return belowState.isOf(Blocks.DIAMOND_ORE) && canOutputAccept(Items.DIAMOND.getDefaultStack());
}

private boolean canOutputAccept(ItemStack result) {
ItemStack out = outputInv.getStack(0);
return (out.isEmpty() || out.getItem() == result.getItem())
&& out.getCount() + result.getCount() <= out.getMaxCount();
}

这里的配方判断我们还是先使用硬编码,因为我们尚未编写自定义的配方,这个到下期教程再来修改

另外添加的canOutputAccept方法检测输出槽是否可以接受物品,因为如果输出物品和槽中已有物品不一致,那么就阻塞了

hasProgressingFinished

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

这里我们判断进度是否已经完成,显然,当progress累加到大于maxProgress的时候,说明已经完成

craftItem

1
2
3
4
private void craftItem(World world) {
ItemStack out = outputInv.getStack(0);
outputInv.setStack(0, new ItemStack(Items.DIAMOND, out.getCount() + 1));
}

因为还是硬编码,所以这里直接添加钻石

resetProgress

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

重置进度就更简单了,将progress赋值为 0 即可

修改动画逻辑

1
2
3
4
5
6
7
@Override
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
controllers.add(new AnimationController<>(this, "controller", 0,
state -> this.isWorking()
? state.setAndContinue(RawAnimation.begin().thenLoop("working"))
: state.setAndContinue(RawAnimation.begin().thenLoop("idle"))));
}

改写一下之前的动画逻辑,当工作时,播放working动画,否则播放idle动画

当然大家如果跳过了之前GeckoLib的教程,这里可以省略

添加 getItems

1
2
3
4
5
public DefaultedList<ItemStack> getItems() {
DefaultedList<ItemStack> inv = DefaultedList.ofSize(1, ItemStack.EMPTY);
inv.set(0, outputInv.getStack(0));
return inv;
}

这个方法我们会在方块类中使用,以便方块被破坏时,能够掉落其存储的物品

添加数据持久化 & 数据同步

数据持久化也是一个重要的部分,因为在游戏运行时,方块实体的数据会跑在内存上,当你退出了世界,内存上的数据不会被保存到本地磁盘中,此时方块实体的一些数据,比如储存的物品、加工进度等,都会丢失

而如果需要保存这些数据,就需要用到数据持久化

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
@Override
protected void writeNbt(NbtCompound nbt) {
super.writeNbt(nbt);
NbtCompound outputTag = new NbtCompound();
Inventories.writeNbt(outputTag, outputInv.stacks);
nbt.put("output", outputTag);
nbt.putInt("progress", this.progress);
nbt.putBoolean("isWorking", this.isWorking);
}

@Override
public void readNbt(NbtCompound nbt) {
super.readNbt(nbt);
if (nbt.contains("output")) {
Inventories.readNbt(nbt.getCompound("output"), outputInv.stacks);
}
this.progress = nbt.getInt("progress");
this.isWorking = nbt.getBoolean("isWorking");
}

@Override
public NbtCompound toInitialChunkDataNbt() {
return this.createNbt();
}

@Override
public @Nullable Packet<ClientPlayPacketListener> toUpdatePacket() {
return BlockEntityUpdateS2CPacket.create(this);
}

数据同步数据持久化需要用到四个方法

writeNbt用于退出世界时保存数据;readNbt用于打开世界时加载数据

toInitialChunkDataNbttoUpdatePacket用于数据同步,关系到客户端与服务端的数据同步(这个和我们自定义的网络包不同,游戏会自动同步一些必要的数据)

注册 Storage

为了让其他传输物品的方块,比如机械动力的漏斗,我们还需要注册我们暴露的Storage

这里我们首先创建一个ModStorages

1
2
3
public class ModStorages {

}

然后在里面写上:

1
2
3
public static void register() {
ItemStorage.SIDED.registerForBlockEntity((be, dir) -> be.getStorage(), ModBlockEntities.PORTABLE_ORIGINIUM_RIG);
}

这里我们注册了ItemStorage,获取PortableOriginiumRigBlockEntityStorage并返回

这样,我们的方块就可以用像机械动力的漏斗这样的方块来输出物品了

未来我们还会限定Storage的方向,用于限制输入输出方向,毕竟正常来说,一个口不能又输入又输出吧,那不得乱套了

对了,不要忘记在主类中调用这个类

1
ModStorages.register()

屏幕处理程序类 ScreenHandler

创建类

接下来就是GUI的部分,首先我们创建一个PortableOriginiumRigScreenHandler类,继承自ScreenHandler

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

public PortableOriginiumRigScreenHandler(@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;
}
}

新增变量 & 改写构造函数

首先我们来新增一些变量,改写构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public final SimpleInventory outputInv;
public final PropertyDelegate propertyDelegate;
public final PortableOriginiumRigBlockEntity entity;

public PortableOriginiumRigScreenHandler(int syncId, PlayerInventory playerInventory, PacketByteBuf buf) {
this(syncId, playerInventory, Objects.requireNonNull(getClientEntity(playerInventory, buf)),
new ArrayPropertyDelegate(2));
}

public PortableOriginiumRigScreenHandler(int syncId, PlayerInventory playerInventory, PortableOriginiumRigBlockEntity entity, PropertyDelegate propertyDelegate) {
super(, syncId);
checkSize(playerInventory, 1);
this.outputInv = entity.getOutputInv();
this.propertyDelegate = propertyDelegate;
this.entity = entity;
}

@Environment(EnvType.CLIENT)
private static PortableOriginiumRigBlockEntity getClientEntity(PlayerInventory playerInventory, PacketByteBuf buf) {
BlockPos pos = buf.readBlockPos();
BlockEntity be = playerInventory.player.getWorld().getBlockEntity(pos);
return be instanceof PortableOriginiumRigBlockEntity e ? e : null;
}

这里我们新增outputInvpropertyDelegateentity三个成员变量,其实这些都是获取方块实体相关内容的

第一个构造方法是供注册方法调用的,第二个构造方法是供方块实体调用的,其中第二个的super函数没有写完,这个在之后完成Screen注册后再来添加

注意ArrayPropertyDelegate的参数要和上面我们在方块实体中初始化PropertyDelegate的个数一致

getBlockEntity是获取方块实体的方法

创建物品栏

接下来我们来创建方块实体的物品栏、玩家背包物品栏和快捷栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public PortableOriginiumRigScreenHandler(int syncId, PlayerInventory playerInventory, PortableOriginiumRigBlockEntity entity, PropertyDelegate propertyDelegate) {
...
this.addSlot(new Slot(outputInv, 0, 104, 37));
addPlayerInventory(playerInventory);
addPlayerHotbar(playerInventory);

addProperties(propertyDelegate);
}

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

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

我们方块实体的输出槽位置位于GUI图片上的坐标是104,37,注意,这是左上角的坐标

Minecraft 的图片坐标原点位于左上角,向下是Y轴正方向,向右是X轴正方向,这个在处理位置有关的逻辑时需要注意

addPlayerInventoryaddPlayerHotBar方法分别添加了玩家背包物品栏和快捷栏,其实都是从原版的类中拿来的方法

另外再添加一个addProperties方法,让ScreenHandler自动同步相关的属性

重写 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 invSlot) {
ItemStack newStack = ItemStack.EMPTY;
Slot slot = this.slots.get(invSlot);
if (slot != null && slot.hasStack()) {
ItemStack originalStack = slot.getStack();
newStack = originalStack.copy();
if (invSlot < this.outputInv.size()) {
if (!this.insertItem(originalStack, this.outputInv.size(), this.slots.size(), true)) {
return ItemStack.EMPTY;
}
} else if (!this.insertItem(originalStack, 0, this.outputInv.size(), false)) {
return ItemStack.EMPTY;
}

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

这个方法其实就是我们按住Shift时,会执行的方法,就是将我们的物品在方块实体物品栏与玩家物品栏之间进行快速移动

没有特殊需求的情况下,直接搬原版的方法就好了

重写 canUse

1
2
3
4
5
6
@Override
public boolean canUse(PlayerEntity player) {
return this.entity != null
&& this.entity.getWorld() != null
&& this.entity.getPos().isWithinDistance(player.getBlockPos(), 8);
}

这是玩家能否使用这个方块实体的方法,一般情况下,这个方法会检查玩家是否在方块实体的附近,如果玩家不在附近,则玩家将无法使用方块实体

另外的方法

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

public int getScaledProgress() {
int progress = this.propertyDelegate.get(0);
int maxProgress = this.propertyDelegate.get(1);
int progressArrowSize = 26;

return maxProgress != 0 && progress != 0 ? progress * progressArrowSize / maxProgress : 0;
}

接下来我们还需要写两个方法,不过这两个方法将会被我们的Screen类调用,他们将用于渲染加工箭头

这个progressArrowSizeGUI上箭头的总长度(高度),按照当前的progressmaxProgress的比例计算当前需要渲染的箭头长度(高度)

注册屏幕处理程序类

我们要创建一个ModScreens

1
2
3
public class ModScreens {、

}

然后在这里我们注册屏幕处理程序

1
2
3
4
5
6
7
public static final ScreenHandlerType<PortableOriginiumRigScreenHandler> PORTABLE_ORIGINIUM_RIG_SCREEN =
Registry.register(Registries.SCREEN_HANDLER, new Identifier(TutorialModRe.MOD_ID, "portable_originium_rig_screen"),
new ExtendedScreenHandlerType<>(PortableOriginiumRigScreenHandler::new));

public static void register() {

}

这里我们就直接调用Registry.register()方法,你也可以自己封装一个方法

同样的,register方法是给主类调用的

1
ModScreens.register();

随后我们就可以修复ScreenHandler里面的构造函数了

1
super(ModScreens.PORTABLE_ORIGINIUM_RIG_SCREEN, syncId);

方块实体类调用

现在我们写完了ScreenHandler,方块实体就可以调用了

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

屏幕类 Screen

屏幕处理程序类主要用于处理像相关的逻辑,而屏幕类则主要用于进行渲染

简单来说,ScreenHandler是规定GUI要怎么画,物品栏的位置在哪里、箭头怎么渲染;而Screen是规定GUI要画什么,用什么贴图、什么颜色

创建屏幕类

首先我们创建一个PortableOriginiumRigScreen类,继承HandledScreen,泛型填入PortableOriginiumRigScreenHandler

1
2
3
4
5
6
7
8
9
10
public class PortableOriginiumRigScreen extends HandledScreen<PortableOriginiumRigScreenHandler> {
public PortableOriginiumRigScreen(PortableOriginiumRigScreenHandler handler, PlayerInventory inventory, Text title) {
super(handler, inventory, title);
}

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

}
}

指定贴图

接下来我们要指定需要绘制的GUI对应的位置

1
public static final Identifier TEXTURE = new Identifier(TutorialModRe.MOD_ID, "textures/gui/portable_originium_rig.png");

记得将对应的贴图放置在assets/modid/textures/gui

新建变量

我们新建一个对应方块实体的变量,虽然现在还用不着,但后面讲到开关时,我们会用到(所以这里可以跳过)

1
2
3
4
5
6
public final PortableOriginiumRigBlockEntity entity;

public PortableOriginiumRigScreen(PortableOriginiumRigScreenHandler handler, PlayerInventory inventory, Text title) {
super(handler, inventory, title);
this.entity = handler.entity;
}

重写 drawBackground

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void drawBackground(DrawContext context, float delta, int mouseX, int mouseY) {
RenderSystem.setShader(GameRenderer::getPositionTexProgram);
RenderSystem.setShaderColor(1f, 1f, 1f, 1f);
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);
}

private void renderProgressArrow(DrawContext context, int x, int y) {
if (handler.isCrafting()){
context.drawTexture(TEXTURE, x + 68, y + 41, 176, 0, handler.getScaledProgress(), 8);
}
}

虽然这个方法名字的字面意思是渲染背景,但实际上还包括了整个GUI的绘制,包括背景、图标、进度条等等

renderProgressArrow方法绘制了进度条,相关的数据就是从ScreenHandler中获取

注意renderProgressArrowdrawTexture方法中x + 68y + 41指的是原来箭头(灰色的)的起始位置是相比于GUI原点偏移68,41(个像素位置)

而后面的176, 0则代表替换箭头(白色的)的起始位置,再后面的两个参数则代表替换箭头的高度(height)和宽度(width

其实绘制箭头、能量条这种,它就是在原来的GUI上再绘制一部分,有点像油画,不过下面是什么,直接覆盖原来的GUI

重写 render

我们还要重写一个render方法

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

主要用来渲染背景、渲染鼠标的悬浮文本,这个算是常规操作

客户端类注册屏幕

最后,我们需要在客户端类的onInitializeClient方法中注册这个屏幕

1
HandledScreens.register(ModScreens.PORTABLE_ORIGINIUM_RIG_SCREEN, PortableOriginiumRigScreen::new);

不然我们打开GUI时会崩溃

修改方块类

最后的最后,我们还要改写方块类,添加tick相关方法,右键使用的方法,以及被破坏是,掉落里面的物品的方法

重写 getTicker

1
2
3
4
@Override
public @Nullable <T extends BlockEntity> BlockEntityTicker<T> getTicker(World world, BlockState state, BlockEntityType<T> type) {
return checkType(type, ModBlockEntities.PORTABLE_ORIGINIUM_RIG, PortableOriginiumRigBlockEntity::tick);
}

这个方法是用来获取方块实体的tick方法,以供这个方块实体随游戏刻运行

重写 onStateReplaced

1
2
3
4
5
6
7
8
9
10
11
@Override
public 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 PortableOriginiumRigBlockEntity be) {
ItemScatterer.spawn(world, pos, be.getItems());
world.updateComparators(pos, this);
}
super.onStateReplaced(state, world, pos, newState, moved);
}
}

这个方法就会在方块被破坏时调用,如果方块实体里面还有东西,就会把东西掉落

重写 onUse

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

这个方法是我们右键方块时会调用的,一般来说,右键方块就和打开GUI绑定了

数据文件

大部分数据文件我们已经在上一期教程中生成了,这里还需要补充一条语言文件

1
translationBuilder.add("blockEntity.portable_originium_rig", "Portable Originium Rig");

这个是显示在GUI上的名字

然后我们就可以跑数据生成,再运行游戏了