方块对Minecraft世界来说很重要.他们构成了所有的地形,结构还有机器.事实是如果你对于制作模组感兴趣,你就会想去添加一些方块.本页面会告诉你如何你创造方块,以及一些你可以用它来做的事。
用一个方块来统治他们
在开始之前,需要理解的是,在游戏中每种方块只存在一个.世界是由那一个方块的千万个在不同位置的引用组成的.换句话说就是同一个方块显示了很多次.
因此一种方块只能被实例一次,也就是在注册的时候.一旦方块被注册,你就可以在需要的情况下使用被注册的方块.
与大部分其他的注册表不同的是,方块可以使用DeferredRegister
的一种特殊版本,称作DeferredRegister.Blocks
. DeferredRegister.Blocks
表现的基本与DeferredRegister<Block>
相同,但是也有一些小的不同之处:
他们是从
DeferredRegister.createBlocks("yourmodid")
而不是常用的DeferredRegister.create(...)
方法中创建出来的。#register
返回一个DeferredBlock<T extends Block>
,它继承了DeferredHolder<Block, T>
.T
是我们注册的方块的class的分类.有一些助手方法可以注册方块,详见下文.
现在,我们来注册自己的方块:
//BLOCKS 的Class是 DeferredRegister.Blocks
public static final DeferredBlock<Block> MY_BLOCK = BLOCKS.register("my_block", registryName -> new Block(...));
在注册方块之后,所有对新的my_block
的引用都应该使用这个常量.例如,如果你想确认在给定位置的方块是不是my_block
,这个功能的代码看起来就像这样:
level.getBlockState(position) // 返回给定level(世界)指定位置的方块状态
.is(MyBlockRegistrationClass.MY_BLOCK);
这种方式有一个便利性,就是可以使用block1 == block2
这样的语法而不是使用Java的equals
方法(equals
依然有用,但是既然能直接通过引用来比较的话这就有点没有意义).
📛不要在注册外调用
new Block()
!只要你这么做了,很多东西会坏掉:
方块必须在注册表没有被冻结的时候创建.NeoForge 会为你解冻注册表并且在之后重新冻结,所以注册时就是你创建方块的时机.
如果你试着在注册表都再次被冻结时创建并/或者注册一个方块,游戏会崩溃并抛出一个
null
方块,这会非常让人摸不到头脑.如果你仍然设法获取了一个架空的方块实例,游戏在同步和保存的时候不会识别它,并且会把它替换成空气.
创建方块
就像上文讨论过的,我们通过创建我们的DeferredRegister.Blocks
开始:
public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks("yourmodid");
基础方块
对于不需要特殊功能的基础方块(想想圆石,木板之类的),可以直接使用Block
类.要这样做的话,在注册的时候,使用BlockBehaviour.Properties
参数来实例化Block
。这个BlockBehaviour.Properties
参数可以通过使用BlockBehaviour.Properties#of
来创建,并且可以通过调用它的方法来实现自定义.比较重要的方法有这些:
setId
- 设置方块的资源键.每个方块都必须设置这个;不然就会抛出一个异常.
destroyTime
- 决定了方块需要多长时间被破坏.石头的破坏时间是1.5,泥土是0.5,黑曜石是50,基岩是-1(不可破坏).
explosionResistance
- 决定了方块的爆炸抗性.石头有着6.0的爆炸抗性,泥土是0.5,黑曜石是1,200,基岩是3,600,000.
sound
- 设置方块在被敲击、破坏、放置时发出的声音.默认值是
SoundType.STONE
.详细请查阅声音页面.
lightLevel
- 设置方块发出的光.接受一个以BlockState
作为参数返回值在0到15之间的函数.例如,萤石是
state -> 15
, 火把是state -> 14
.
friction
- 设置方块的摩擦力(平滑度).默认值是0.6.冰块是0.98.
所以,一个简单的实现看起来就像这样:
//BLOCKS is a DeferredRegister.Blocks
public static final DeferredBlock<Block> MY_BETTER_BLOCK = BLOCKS.register(
"my_better_block",
registryName -> new Block(BlockBehaviour.Properties.of()
.setId(ResourceKey.create(Registries.BLOCK, registryName))
.destroyTime(2.0f)
.explosionResistance(10.0f)
.sound(SoundType.GRAVEL)
.lightLevel(state -> 7)
));
更多文档请查阅BlockBehaviour.Properties
的源码。更多例子或者Minecraft使用的值,看一看Block
类.
ℹ️要理解的是,世界中的一个方块与物品栏中的一个方块不是一回事.物品栏中看起来像一个方块的东西其实是一个
BlockItem
,一种特殊的在使用时放置一个方块的物品.这也意味着像创造模式的物品栏标签或者最大堆叠数量是由对应的BlockItem
处理的.一个
BlockItem
必须方块分别注册.这是因为一个方块不一定需要一个物品,例如这个方块是不能被捡起的那种(就像火焰这种情况).
更多功能
直接使用Block
类只允许你创建非常基础的方块.如果你想添加别的功能,像是玩家互动或者不同的碰撞箱,需要自定义一个继承Block
的类.Block
类有许多可以被重写以达到不同效果的方法;需要更多相关信息的话就看看Block
,BlockBehaviour
以及 IBlockExtension
这些类.也看看下面的使用方块小节来了解一些最常用的方块用法.
如果你想制作拥有不同变种的方块(就像拥有下半,上半,双层的台阶方块),你应该用方块状态.最后,如果你想要一个存储额外数据的方块(想想箱子存储他的库存),应该使用方块实体.我能给出的经验法则是,如果你的需求的状态是有限且量比较小的(=最多100种状态),用方块状态(blockstate),如果你需要无限或者几乎无限的状态,用方块实体。
方块种类
方块种类是用来序列化并反序列化一种方块的映射编码器(MapCodec
).这个MapCodec
是从BlockBehaviour#codec
里面设置的并且注册到了方块种类注册表里面。目前它唯一的作用就是在方块列表报告生成后,Block
类的每一个子类都应该创建一个方块种类.例如FlowerBlock#CODEC
代表着大部分花的方块种类,而它的子类WitherRoseBlock
有着另一个方块种类.
如果方块子类只接受BlockBehaviour.Properties
,那就可以用BlockBehaviour#simpleCodec
来创建MapCodec
.
// 对于一些方块子类
public class SimpleBlock extends Block {
public SimpleBlock(BlockBehavior.Properties properties) {
// ...
}
@Override
public MapCodec<SimpleBlock> codec() {
return SIMPLE_CODEC.get();
}
}
// 在一些注册类中
public static final DeferredRegister<MapCodec<? extends Block>> REGISTRAR = DeferredRegister.create(BuiltInRegistries.BLOCK_TYPE, "yourmodid");
public static final Supplier<MapCodec<SimpleBlock>> SIMPLE_CODEC = REGISTRAR.register(
"simple",
() -> BlockBehaviour.simpleCodec(SimpleBlock::new)
);
如果方块子类包含更多参数,那么应该使用RecordCodecBuilder#mapCodec
来创建MapCodec
,传入BlockBehaviour#propertiesCodec
来作为BlockBehaviour.Properties
的参数.
// 对于一些方块子类
public class ComplexBlock extends Block {
public ComplexBlock(int value, BlockBehavior.Properties properties) {
// ...
}
@Override
public MapCodec<ComplexBlock> codec() {
return COMPLEX_CODEC.get();
}
public int getValue() {
return this.value;
}
}
// 一些注册类中
public static final DeferredRegister<MapCodec<? extends Block>> REGISTRAR = DeferredRegister.create(BuiltInRegistries.BLOCK_TYPE, "yourmodid");
public static final Supplier<MapCodec<ComplexBlock>> COMPLEX_CODEC = REGISTRAR.register(
"simple",
() -> RecordCodecBuilder.mapCodec(instance ->
instance.group(
Codec.INT.fieldOf("value").forGetter(ComplexBlock::getValue),
BlockBehaviour.propertiesCodec() // 代表BlockBehavior.Properties参数
).apply(instance, ComplexBlock::new)
)
);
ℹ️虽然在当下方块种类基本没用到,因为Mojang逐渐走向以编码器为中心的结构,方块种类在未来会变得越来越来越重要,.
DeferredRegister.Blocks
助手
我们已经在上文讨论了如何创建DeferredRegister.Blocks
,既然它返回DeferredBlock
.那现在就让我们看看特殊的DeferredRegister
还提供了哪些功能.我们从#registerBlock
开始:
public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks("yourmodid");
public static final DeferredBlock<Block> EXAMPLE_BLOCK = BLOCKS.register(
"example_block", registryName -> new Block(
BlockBehaviour.Properties.of()
// 方块上必须设置ID
.setId(ResourceKey.create(Registries.BLOCK, registryName))
)
);
// 与上面一样,除了方块属性是提前构造的.
// setId 也在属性物体中内部的调用了.
public static final DeferredBlock<Block> EXAMPLE_BLOCK = BLOCKS.registerBlock(
"example_block",
Block::new, // 属性会被传入的工厂.
BlockBehaviour.Properties.of() // 要使用的属性.
);
如果你想使用Block::new
,你完全可以将工厂参数留空:
public static final DeferredBlock<Block> EXAMPLE_BLOCK = BLOCKS.registerSimpleBlock(
"example_block",
BlockBehaviour.Properties.of() // 要使用的属性.
);
这和之前的例子是一样的效果,但是更短一点.当然,如果你想用Block
类的子类而不是Block
类本身,你还是要用之前的方式.
资源
如果你在注册完方块之后把他放置到世界中,你会发现它缺少了像材质之类的东西.这是因为材质不像其他东西,是由Minecraft的资源系统处理的.要把材质应用到方块上,你必须提供一个模型和一个将方块和材质与形状联系起来的方块状态文件.看看链接里面的文章了解更多.
使用方块
方块很少直接被拿来做什么事.实际上,Minecraft中大约只有两种最常见的行为 - 获取特定位置的方块和设置方块到特定位置 - 使用方块状态,而不是方块.通常设计的方式是给方块定义行为,但是拥有行为本身就要通过方块状态.因此,BlockState
通常作为参数传递给Block
中的方法.更多关于方块状态用法以及如何从方块上获取方块状态,查阅使用方块状态.
在一些情况下,不同的时间段会使用多种Block
累的方法.之后的小节列出了最常见的与方块相关的管线.抛开特殊情况,所有的方法无论实在物理侧调用还是逻辑侧调用都应该返回同样的结果.
放置方块
方块放置逻辑是从BlockItem#useOn
(或者一些子类的实现中,例如在用于睡莲的PlaceOnWaterBlockItem
里面)中调用的.更多关于游戏如何做到的信息,查阅互动管线。实践中,这意味着只要一个BlockItem
右键了(例如一个圆石物品),这个行为就会调用.
一些前提条件已经检查过了,例如你没有处于旁观模式,所有方块需要的特性占位都开启了或者目标位置没有位于世界边界之外.如果以上这些检查有一项没通过,管线就直接结束了.
BlockBehaviour#canBeReplaced
方法会在尝试放置方块的位置上 已存在 的方块上调用。如果这个方法返回false
,放置流程就会终止。比较常见的情况是,像高草或雪层这样的方块会让这个方法返回true
。Block#getStateForPlacement
被调用.这里会根据上下文(包括像是位置,旋转以及方块被放置在的面这样的信息)返回不同的方块状态.对于能够以不同方向放置的方块来说是很有用的方法.BlockBehaviour#canSurvive
会包含着上一步的方块状态被调用.如果他返回了false
,管线停止.方块状态通过一个
Level#setBlock
调用设置在level中.在那个
Level#setBlock
调用中,BlockBehaviour#onPlace
被调用了.
Block#setPlacedBy
被调用了.
破坏方块
破坏方块会更复杂一点,因为它需要时间.这个过程可以被粗略的分成三个阶段: "启动", "挖掘" 以及 "实际破坏".
当鼠标左键点击时就进入了"启动"阶段.
现在,鼠标左键需要被按住来进入"挖掘"阶段.这个阶段的方法每个tick都会调用.
如果"持续"阶段没有被打断(通过松开鼠标左键)并且方块被破坏了,就进入了"实际破坏"阶段.
或者对于喜欢伪代码的人:
leftClick();
initiatingStage();
while (leftClickIsBeingHeld()) {
miningStage();
if (blockIsBroken()) {
actuallyBreakingStage();
break;
}
}
接下来的小节进一步将这些阶段细化为实际的方法调用.
"启动"阶段
只在客户端:
InputEvent.InteractionKeyMappingTriggered
随着鼠标左键与主手启动.如果事件被取消,管线结束.一些前提条件被检查,例如玩家未处于旁观模式,玩家主手中物品堆(
ItemStack
)所需的所有特性标志是否都已启用,或者方块是不是在世界边界之外等等。如果其中任何一项检查失败,放置流程就会终止。PlayerInteractEvent.LeftClickBlock
启动.如果事件被取消,管线结束.值得注意的是当客户端上事件被取消时,不会有数据包发送到服务器,因此服务端上不会有对应的逻辑运行。
然而,服务端上取消这个时间仍然会导致客户端代码继续执行,最终会导致不同步!
Block#attack
被调用.
"挖掘"阶段
PlayerInteractEvent.LeftClickBlock
启动.如果事件被取消,管线会移动到"完成"阶段.要注意的是当客户端上事件被取消时,不会有数据包发送到服务器,因此服务端上不会有对应的逻辑运行。
然而,服务端上取消这个时间仍然会导致客户端代码继续执行,最终会导致不同步!
Block#getDestroyProgress
被调用并加入内部破坏进程计数器.Block#getDestroyProgress
返回0到1之间的浮点数,代表每个tick破坏进度计数器应该增加到多少.
过程覆盖(裂纹材质)据此更新.
如果破怪进程大于1.0(例如:已完成,例如:方块应该被破坏),退出"挖掘"阶段并进入"实际破坏"阶段.
"实际破坏"阶段
Item#canAttackBlock
调用.如果他返回false
(决定方块是否应该被破坏),管线移动到"完成"阶段.如果目标方块是
GameMasterBlock
的一个实例的话就会调用Player#canUseGameMasterBlocks
.这决定了玩家是否有能力破坏只有创造模式才能破坏的方块.如果返回了false
,管线移动到"完成"阶段.仅服务器端: 调用
Player#blockActionRestricted
方法。这个方法用来判断当前玩家是否 不能 破坏这个方块。如果返回true
,整个破坏流程就直接跳到“完成”阶段了。仅服务器端: 触发
BlockEvent.BreakEvent
事件。如果这个事件被取消了,或者getExpToDrop
方法返回 -1,破坏流程也会跳到“完成”阶段。这个事件最开始的取消状态是由前面三个方法(指前文提到的其他检查)决定的。仅服务器端: 触发
PlayerEvent.HarvestCheck
事件。如果canHarvest
方法返回false
,或者传递给破坏事件的BlockState
是null
,那么这个事件的初始经验值将会是 0。仅服务器端: 如果
PlayerEvent.HarvestCheck#canHarvest
返回true
,则调用IBlockExtension#getExpDrop
方法。这个方法的返回值会传递给BlockEvent.BreakEvent#getExpToDrop
,供后续流程使用。仅服务器端: 调用
IBlockExtension#canHarvestBlock
方法。这个方法决定方块是否可以被“收获”,也就是破坏后掉落物品。调用
IBlockExtension#onDestroyedByPlayer
方法。如果这个方法返回false
,破坏流程也会跳到“完成”阶段。在这个IBlockExtension#onDestroyedByPlayer
方法的调用过程中,会发生以下事情:调用
Block#playerWillDestroy
方法。通过调用
Level#setBlock
方法,将方块从世界中移除,并将其替换为空气方块(Blocks.AIR.defaultBlockState()
)。在
Level#setBlock
方法的调用过程中,会调用Block#onRemove
方法。调用
Block#destroy
方法。
仅服务器端: 如果之前调用
IBlockExtension#canHarvestBlock
返回了true
,则调用Block#playerDestroy
方法。仅服务器端: 调用
Block#dropResources
方法。这个方法决定方块被挖掘时掉落什么物品。仅服务器端: 触发
BlockDropsEvent
事件。如果这个事件被取消了,方块破坏时就不会掉落任何东西。否则,BlockDropsEvent#getDrops
中的每个ItemEntity
都会被添加到当前的世界中。仅服务器端: 如果之前的
IBlockExtension#getExpDrop
方法返回的值大于 0,则调用Block#popExperience
方法,并传入该值,以掉落经验。
刻(Ticking)
刻是一项每1/20秒或50毫秒(1tick)更新一次的游戏机制.方块提供了以不同方式调用的各种各样的刻方法。
服务器刻与刻规划
BlockBehaviour#tick
会在两种情况下被调用: 通过默认的随机刻(见下文)或者是规划刻. 规划刻可以通过 Level#scheduleTick(BlockPos, Block, int)
来创建, int
表示延迟时长. 它被用于原版的各个地方, 例如, 大型滴水莲的倾斜机制很大一部分依靠于这个游戏刻机制. 各种红石元件也大量使用这个游戏刻系统.
客户端的刻
Block#animateTick
只有在客户端的每一帧会被调用. 这里就是仅客户端的逻辑生效的地方, 例如火把的粒子效果生成.
天气刻
天气的刻是由Block#handlePrecipitation
处理的并且与常规的刻相互独立. 它只在服务端被调用, 在1/16的概率以某种形式下雨. 比如下雨下雪时会慢慢蓄水的大锅,用的就是这个东西。
随机刻
随机刻系统与常规刻相独立运行. 想让方块根据游戏里的时间间隔自动触发一些事件,就需要开启它的“随机刻”功能。这个功能藏在方块的BlockBehaviour.Properties
里,通过调用BlockBehaviour.Properties#randomTicks()
方法来启用。这会让方块变成随机刻机制的一部分.
每个游戏刻,每个区块会随机抽取几个方块(数量由 randomTickSpeed
决定,默认为 3)。如果这些方块开启了随机刻,就会执行它们的 BlockBehaviour#randomTick
方法。
Minecraft 里很多东西的运作都靠“随机刻”,像植物长大、冰雪融化、铜生锈这些都是。