Skip to content

Commit

Permalink
1.21-neoforge update
Browse files Browse the repository at this point in the history
  • Loading branch information
3TUSK committed Jul 20, 2024
1 parent f0db8ee commit 597e37b
Show file tree
Hide file tree
Showing 31 changed files with 1,877 additions and 4 deletions.
13 changes: 13 additions & 0 deletions docs/1.21-neoforge/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
hide_table_of_contents: true
hide_title: true
title: 1.21-neoforge
---

import Index from '../../README.md'

<Index/>

## 阅读准备

这篇指南基于 Minecraft 1.21 版本,并基于 NeoForge 21.0.61,和 Java 21.0.1。这篇指南假设读者拥有一定基于 Java 21 的开发经验,一定量的 Minecraft 游玩经验,和足够通畅的网络环境。这篇指南同时假设读者拥有一台能够正常运行 Minecraft 1.21,并拥有至少 4 GB 空闲内存的计算机。
237 changes: 237 additions & 0 deletions docs/1.21-neoforge/block-entity/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# 方块实体

<!-- markdownlint-disable MD033 -->

import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
import CodeBlock from '@theme/CodeBlock'
import Xiaozhong from '!!raw-loader!./xiaozhong/Xiaozhong.java'

Minecraft 使用名为方块实体(Block Entity)的机制来实现一些一般情况下方块做不到或很难做到的事情,例如:

- 持有更复杂的数据(物品、文本等)
- 实现像熔炉那样持续不断地行为
- 拥有奇妙的渲染特效

## 方块实体类型

Minecraft 中所有的方块实体均属于 `BlockEntity` 类型。我们首先需要创建我们自己的 `BlockEntity` 类。

<Tabs>
<TabItem value="core" label="核心代码">

```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java"
public static final class MyMachineEntity extends BlockEntity {
public MyMachineEntity(BlockEntityType<MyMachineEntity> type, BlockPos worldPosition, BlockState blockState) {
super(type, worldPosition, blockState);
}
}
```

</TabItem>
<TabItem value="full" label="完整代码">
<CodeBlock language="java" metastring="{75-78,98}" showLineNumbers
title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java">{Xiaozhong}</CodeBlock>
</TabItem>
</Tabs>

然后我们需要注册一个 `BlockEntityType` 实例,它代表了「一种特定的方块实体」,指明了创建对应方块实体实例的工厂方法,及哪些方块允许持有这种方块实体。
`BlockEntityType` 由注册表统一管理,我们可以通过 `DeferredRegister` 来注册新的 `BlockEntityType`

使用 `DeferredRegister` 注册 `BlockEntityType`

<Tabs>
<TabItem value="core" label="核心代码">

```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java"
// 注册 BlockEntityType
public static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES = DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, "xiaozhong");
// 注册 xiaozhong:my_machine
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<MyMachienEntity>> MY_MACHINE_BLOCK_ENTITY;
MY_MACHINE_BLOCK_ENTITY = BLOCK_ENTITY_TYPES.register("my_machine",
() -> BlockEntityType.Builder.of(MyMachineEntity::new, MY_MACHINE.get()).build(DSL.remainderType()));
// 添加监听器
BLOCK_ENTITY_TYPES.register(modEventBus);
```

</TabItem>
<TabItem value="full" label="完整代码">
<CodeBlock language="java" metastring="{28,31,36-37,42}" showLineNumbers
title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java">{Xiaozhong}</CodeBlock>
</TabItem>
</Tabs>

注意到 `MyMachineEntity::new` 此时会报错。刚才提到的「方块实体实例的工厂方法」就是这个,然而原版要求「这个工厂方法只接受两个参数:`BlockPos``BlockState`」。我们向 `MyMachineEntity` 添加额外构造器来解决此问题:

<Tabs>
<TabItem value="core" label="核心代码">

```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java"
public MyMachineEntity(BlockPos worldPosition, BlockState blockState) {
this(MY_MACHINE_BLOCK_ENTITY.get(), worldPosition, blockState);
}
```

</TabItem>
<TabItem value="full" label="完整代码">
<CodeBlock language="java" metastring="{79-81}" showLineNumbers
title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java">{Xiaozhong}</CodeBlock>
</TabItem>
</Tabs>

:::info

这个案例也展示了 `DeferredRegister` 的一个优势:可以方便开发者打破这种出现循环依赖的情况。

:::

`BlockEntity` 此时像是一个纯粹的数据容器:我们在我们的 `MyMachineEntity` 中可以随意添加新的成员字段,并视情况决定是否创建对应的访问器。

<Tabs>
<TabItem value="core" label="核心代码">

```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java"
public static final class MyMachineEntity extends BlockEntity {
public MyMachineEntity(BlockEntityType<MyMachineEntity> type, BlockPos worldPosition, BlockState blockState) {
super(type, worldPosition, blockState);
}

public MyMachineEntity(BlockPos worldPosition, BlockState blockState) {
this(MY_MACHINE_BLOCK_ENTITY.get(), worldPosition, blockState);
}

private int count = 0;
}
```

</TabItem>
<TabItem value="full" label="完整代码">
<CodeBlock language="java" metastring="{75-82,98}" showLineNumbers
title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java">{Xiaozhong}</CodeBlock>
</TabItem>
</Tabs>

## 方块类

脱离方块的方块实体是不存在的。能容纳方块实体的方块需实现 `EntityBlock` 接口。实现这一接口需实现 `newBlockEntity` 方法。

Minecraft 原版为我们提供了 `BaseEntityBlock` 类方便我们实现:

<Tabs>
<TabItem value="core" label="核心代码">

```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java"
public static final DeferredRegister<Block> BLOCKS = DeferredRegister.Blocks.createBlocks("xiaozhong");

public static final RegistryObject<Block> MY_MACHINE;
MY_MACHINE = BLOCKS.register("my_machine",
() -> new MyMachine(BlockBehaviour.Properties.ofFullCopy(Blocks.IRON_BLOCK)));

BLOCKS.register(modEventBus);

public static final class MyMachine extends BaseEntityBlock {
public static final MapCodec<MyMachine> CODEC = simpleCodec(MyMachine::new);
public MyMachine(Properties props) {
super(props);
}

@Override
protected MapCodec<? extends BaseEntityBlock> codec() {
return CODEC;
}

@NotNull
@Override
public RenderShape getRenderShape(@NotNull BlockState state) {
// 注意:这一方法在 BaseEntityBlock 中默认返回 INVISIBLE,
// 这代表 Minecraft 会跳过该方块的渲染。
// 为保证正常渲染,我们在此返回 MODEL,代表使用普通方块模型渲染。
return RenderShape.MODEL;
}

@Override
public BlockEntity newBlockEntity(@NotNull BlockPos pos, @NotNull BlockState state) {
// 这里返回我们刚刚定义的方块实体
return new MyMachineEntity(pos, state);
}
}
```

</TabItem>
<TabItem value="full" label="完整代码">
<CodeBlock language="java" metastring="{27,30,34-35,41,45-66,72}" showLineNumbers
title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java">{Xiaozhong}</CodeBlock>
</TabItem>
</Tabs>

## 方块实体刻

默认情况下,方块实体并不具备跟随游戏刻刷新(亦即方块实体刻)的能力,若要获得此能力,方块实体所在的那个方块需要明确声明一个所谓的「Ticker」,亦即 `BlockEntityTicker<?>`。这通过覆盖 `EntityBlock` 的 `getTicker` 方法实现。

此外,你可以根据 `Level` 是在逻辑服务器上还是逻辑客户端上来返回不同的 Ticker。通常我们在客户端不需要特别的逻辑,因此我们会在 `level.isClientSide()` 返回 `true` 时,令 `getTicker` 返回 `null`。

首先在 `MyMachineEntity` 中创建静态方法 `tick`:

<Tabs>
<TabItem value="core" label="核心代码">

```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java"
private int count = 0;

public static void tick(Level level, BlockPos pos, BlockState state, MyMachineEntity entity) {
// 计数。每一个游戏刻中,游戏都会调用一次 tick 方法,所以我们只需要在这里让 count 自增 1 即可。
entity.count += 1;
// 当计数超过 100 时……
if (entity.count > 100) {
// 首先重置计数
entity.count = 0;
// 检查 Level 实例是否存在,以及是不是在服务器上
if (level != null && !level.isClientSide()) {
// 然后寻找半径 5 方块以内的玩家,最后的 false 表示无视创造或观察模式的玩家
var player = level.getNearestPlayer(pos.getX(), pos.getY(), pos.getZ(), 5.0, false);
// 如果找到了这样的玩家……
if (player != null) {
// 向这名玩家的聊天栏发送问候语。
player.sendSystemMessage(Component.translatable("chat.xiaozhong.welcome"));
}
}
}
}
```

</TabItem>
<TabItem value="full" label="完整代码">
<CodeBlock language="java" metastring="{83-96}" showLineNumbers
title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java">{Xiaozhong}</CodeBlock>
</TabItem>
</Tabs>

然后在我们的方块类中,覆写 `getTicker` 方法:

<Tabs>
<TabItem value="core" label="核心代码">

```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java"
@Nullable
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> blockEntityType) {
// BaseEntityBlock 提供了 createTickerHelper 帮助你生成 BlockEntityTicker 的实例,这里直接调用并传入 tick 方法引用即可。
return level.isClientSide() ? null : createTickerHelper(blockEntityType, MY_MACHINE_BLOCK_ENTITY.get(), MyMachineEntity::tick);
}
```

</TabItem>
<TabItem value="full" label="完整代码">
<CodeBlock language="java" metastring="{68-71}" showLineNumbers
title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java">{Xiaozhong}</CodeBlock>
</TabItem>
</Tabs>

以下演示了一个最基础方块实体的实现及的注册流程——没有相应的语言文件,没有模型,什么都没有,甚至没有为该方块注册对应的物品:

<CodeBlock language="java" showLineNumbers
title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java">{Xiaozhong}</CodeBlock>

这个方块实体的效果是,每 100 个游戏刻检查是否有玩家距离该方块实体距离不到 5 方块,若有,随机抽一位向其打招呼。

打开游戏,执行 `/setblock ~ ~-1 ~ xiaozhong:my_machine` 看看效果吧~
98 changes: 98 additions & 0 deletions docs/1.21-neoforge/block-entity/xiaozhong/Xiaozhong.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package org.teacon.xiaozhong;

import com.mojang.datafixers.DSL;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredRegister;

import org.jetbrains.annotations.NotNull;

@Mod("xiaozhong")
public class Xiaozhong {
public static final DeferredRegister<Block> BLOCKS = DeferredRegister.Blocks.createBlocks("xiaozhong");
public static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES = DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, "xiaozhong");

public static final DeferredHolder<Block, MyMachine> MY_MACHINE;
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<MyMachineEntity>> MY_MACHINE_BLOCK_ENTITY;

static {
MY_MACHINE = BLOCKS.register("my_machine",
() -> new MyMachine(BlockBehaviour.Properties.ofFullCopy(Blocks.IRON_BLOCK)));
MY_MACHINE_BLOCK_ENTITY = BLOCK_ENTITY_TYPES.register("my_machine",
() -> BlockEntityType.Builder.of(MyMachineEntity::new, MY_MACHINE.get()).build(DSL.remainderType()));
}

public Xiaozhong(IEventBus modEventBus) {
BLOCKS.register(modEventBus);
BLOCK_ENTITY_TYPES.register(modEventBus);
}

public static final class MyMachine extends BaseEntityBlock {

public static final MapCodec<MyMachine> CODEC = simpleCodec(MyMachine::new);
public MyMachine(Properties props) {
super(props);
}

@Override
protected MapCodec<? extends BaseEntityBlock> codec() {
return CODEC;
}

@NotNull
@Override
public RenderShape getRenderShape(@NotNull BlockState state) {
return RenderShape.MODEL;
}

@Override
public BlockEntity newBlockEntity(@NotNull BlockPos pos, @NotNull BlockState state) {
return new MyMachineEntity(pos, state);
}

@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, @NotNull BlockState state, @NotNull BlockEntityType<T> blockEntityType) {
return level.isClientSide() ? null : createTickerHelper(blockEntityType, MY_MACHINE_BLOCK_ENTITY.get(), MyMachineEntity::tick);
}
}

public static final class MyMachineEntity extends BlockEntity {
public MyMachineEntity(BlockEntityType<MyMachineEntity> type, BlockPos worldPosition, BlockState blockState) {
super(type, worldPosition, blockState);
}

public MyMachineEntity(BlockPos worldPosition, BlockState blockState) {
this(MY_MACHINE_BLOCK_ENTITY.get(), worldPosition, blockState);
}

private int count = 0;

public static void tick(Level level, BlockPos pos, BlockState state, MyMachineEntity entity) {
entity.count += 1;
if (entity.count > 100) {
entity.count = 0;
if (level != null && !level.isClientSide()) {
var player = level.getNearestPlayer(pos.getX(), pos.getY(), pos.getZ(), 5.0, false);
if (player != null) {
player.sendSystemMessage(Component.translatable("chat.xiaozhong.welcome"));
}
}
}
}
}
}
Loading

0 comments on commit 597e37b

Please sign in to comment.