世界中的流体
在世界中放置流体时,使用的是 FluidState 而非 Fluid,这与 BlockState 和 Block 的关系相似。可以调用 Level#getFluidState() 查询指定位置的流体状态,通过 Fluid#defaultFluidState() 获取默认状态。
不过,流体状态与方块状态也有差别。最明显的是,不同流体状态并非通过方块状态那样的属性定义来运作,而是由世界根据流体扩散机制计算出具体状态。多数情况下不需要关注精确的 FluidState;需要时,可以查询其中的一些属性,例如 isSource()。
遗憾的是,当前世界中 FluidState 的实现并不完善,而且 NeoForge 无法在保持原版世界兼容性的前提下修复它。几乎所有流体状态逻辑都以某种方式绑定到 BlockState,尽管理论上没有必要。例如,系统没有 Level#setFluidState() 方法;当前实现中,Level#getFluidState() 最终会深入区块存储,归结为 BlockState#getFluidState()。预计 Mojang 最终会重做这一系统,但目前仍需沿用现有实现。
含水方块
含水机制集中体现了当前 FluidState 系统的局限:某些非完整方块,例如台阶,可以同时包含一个水源。当前通过 WATERLOGGED 方块状态属性实现:
// Implementing SimpleWaterloggedBlock automatically enables bucket pickup
// and makes some helper methods available.
public class MyBlock extends Block implements SimpleWaterloggedBlock {
// Add the WATERLOGGED property to our class for easy access.
public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED;
// Set WATERLOGGED to false by default.
public MyBlock(Properties properties) {
super(properties);
registerDefaultState(getStateDefinition().any().setValue(WATERLOGGED, false));
}
// Add WATERLOGGED to the block state definition.
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
super.createBlockStateDefinition(builder);
builder.add(WATERLOGGED);
}
// The important part: Query the WATERLOGGED property when asked for the fluid state.
// The `false` parameter in Fluids.WATER.getSource(false) means "falling" and is set to false
// for all vanilla waterlogging implementations.
@Override
public FluidState getFluidState(BlockState state) {
return state.getValue(WATERLOGGED) ? Fluids.WATER.getSource(false) : super.getFluidState(state);
}
// When placing this block in water, place the waterlogged state.
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
return defaultBlockState().setValue(
WATERLOGGED,
context.getLevel().getFluidState(context.getClickedPos()).is(Fluids.WATER)
);
}
// When this block receives a block update (because e.g. a neighbor changed),
// a tick should be scheduled.
@Override
protected BlockState updateShape(
BlockState state,
LevelReader level,
ScheduledTickAccess ticks,
BlockPos pos,
Direction directionToNeighbour,
BlockPos neighbourPos,
BlockState neighbourState,
RandomSource random
) {
if (state.getValue(WATERLOGGED)) {
ticks.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level));
}
return super.updateShape(state,
level,
ticks,
pos,
directionToNeighbour,
neighbourPos,
neighbourState,
random);
}
}
说明 类似的“含熔岩”或含其他流体机制也很容易实现:创建新的
BooleanProperty,按常规方式添加到方块中,并在属性为真时让Block#getFluidState()返回目标流体。不过需要注意,世界生成、活塞移动等部分逻辑对含水行为进行了硬编码。
流体方块
要将流体放入世界,需要为它创建 LiquidBlock:
// Assuming a DeferredRegister.Blocks named BLOCKS, and assuming the fluid stuff
// is in another class named ModFluids.
public static final DeferredBlock<LiquidBlock> MOLTEN_IRON = BLOCKS.registerBlock(
// The block registry name.
"molten_iron",
// The liquid block factory.
properties -> new LiquidBlock(ModFluids.MOLTEN_IRON.get(), properties),
// The block properties.
() -> BlockBehaviour.Properties.of()
// Standard properties for both vanilla fluids. Strength 100 disables vanilla TNT
// from having effects while allowing modded explosives to still work.
.liquid()
.noLootTable()
.noCollision()
.replaceable()
.pushReaction(PushReaction.DESTROY)
.sound(SoundType.EMPTY)
.strength(100)
// You may define additional properties depending on what your fluid does.
// For example, like before, we make our molten iron fluid glow slightly:
.lightLevel(_ -> 5)
);
然后将该方块加入流体属性:
public static final BaseFlowingFluid.Properties MOLTEN_IRON_PROPERTIES =
new BaseFlowingFluid.Properties(MOLTEN_IRON_TYPE, MOLTEN_IRON, FLOWING_MOLTEN_IRON)
// Set the block, assuming it is located in the `ModBlocks` class.
// Make sure that `ModBlocks` is classloaded before `ModFluids`!
.block(ModBlocks.MOLTEN_IRON);
最后,方块需要一个模型,可以很方便地通过数据生成创建:
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
blockModels.createNonTemplateModelBlock(ModBlocks.MOLTEN_IRON.get());
}
桶
流体通常可以用桶收集。自定义流体桶可以这样添加:
// Assuming a DeferredRegister.Items named ITEMS, and assuming the fluid stuff
// is in another class named ModFluids.
public static final DeferredItem<BucketItem> MOLTEN_IRON_BUCKET = ITEMS.registerItem(
// The registry name.
"molten_iron_bucket",
// The bucket item factory.
properties -> new BucketItem(ModFluids.MOLTEN_IRON.get(), properties),
// The properties supplier. Buckets stack to 1 and return a bucket when used in crafting.
() -> new Item.Properties().stacksTo(1).craftRemainder(Items.BUCKET)
);
然后将桶加入流体属性:
public static final BaseFlowingFluid.Properties MOLTEN_IRON_PROPERTIES =
new BaseFlowingFluid.Properties(MOLTEN_IRON_TYPE, MOLTEN_IRON, FLOWING_MOLTEN_IRON)
.block(ModBlocks.MOLTEN_IRON)
// Set the bucket, assuming it is located in the `ModItems` class.
// Make sure that `ModItems` is classloaded before `ModFluids`!
.bucket(ModItems.MOLTEN_IRON_BUCKET);
接着,建议为桶添加发射器行为,但这不是必需的:
@SubscribeEvent // on the mod event bus
private static void commonSetup(FMLCommonSetupEvent event) {
// `DispenserBlock#registerBehavior` is not thread-safe so we wrap it in a lambda.
event.enqueueWork(() -> DispenserBlock.registerBehavior(ModItems.MOLTEN_IRON_BUCKET, DispenseFluidContainer.getInstance()));
}
建议 如果需要自定义发射器行为,也可以创建自己的
DispenseItemBehavior。具体实现可参考DispenseFluidContainer源代码。
最后只需添加翻译和模型:
// In the language provider
@Override
protected void addTranslations() {
add(ModFluids.MOLTEN_IRON_TYPE.get().getDescriptionId(), "Molten Iron");
addItem(ModItems.MOLTEN_IRON_BUCKET, "Molten Iron Bucket");
}
// In the model provider
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
blockModels.createNonTemplateModelBlock(ModBlocks.MOLTEN_IRON.get());
// We use NeoForge's `DynamicFluidContainerModel`.
itemModels.itemModelOutput.accept(ModItems.MOLTEN_IRON_BUCKET.get(), new DynamicFluidContainerModel.Unbaked(
// The model's textures. The model is rendered in the order of base, fluid, cover (lowest to highest).
new DynamicFluidContainerModel.Textures(
// The particle texture.
Optional.of(new Material(Identifier.withDefaultNamespace("item/bucket"))),
// The base texture.
Optional.of(new Material(Identifier.withDefaultNamespace("item/bucket"))),
// The fluid texture, i.e. the part that actually contains the fluid.
Optional.of(new Material(Identifier.fromNamespaceAndPath("neoforge", "item/mask/bucket_fluid"))),
// The cover texture. This is rendered last and can be a mask (see below).
Optional.empty()
),
// The fluid to use.
ModFluids.MOLTEN_IRON.get(),
// Whether the bucket model should be flipped, commonly used for "gaseous" fluids.
false,
// If true, the cover texture is a mask. If false, the cover texture is rendered normally.
// See below for more info.
true,
// If this is true, if the fluid emits light, the fluid element of the model becomes emissive.
true));
}
桶的遮罩纹理
当布尔值 coverIsMask 为真时,覆盖纹理会改作遮罩。NeoForge 为桶提供了四种默认遮罩纹理。遮罩的每个像素要么完全透明(下图为便于与网站背景区分,以纯黑显示),要么不是完全透明,作用类似模板。查看这些纹理即可理解:
图片来源:NeoForged/NeoForge 26.1.x。
图片来源:NeoForged/NeoForge 26.1.x。
图片来源:NeoForged/NeoForge 26.1.x。
图片来源:NeoForged/NeoForge 26.1.x。
只有遮罩中非完全透明的像素区域会参与渲染,与完全透明区域重叠的像素会被丢弃。
建议 为便于区分两类像素,建议透明像素统一使用透明黑色
0x00000000,不透明像素使用纯白色0xffffffff。
炼药锅
除桶外,流体也常存放在炼药锅中。为此需要单独的炼药锅方块:
public class MoltenIronCauldronBlock extends AbstractCauldronBlock {
// Block codec boilerplate.
private static final MapCodec<MoltenIronCauldronBlock> CODEC =
simpleCodec(MoltenIronCauldronBlock::new);
@Override
protected MapCodec<? extends AbstractCauldronBlock> codec() {
return CODEC;
}
// The cauldron interaction dispatcher and its id. See below for more info.
public static final CauldronInteraction.Dispatcher CAULDRON_INTERACTIONS =
new CauldronInteraction.Dispatcher();
public static final Identifier CAULDRON_INTERACTIONS_ID =
Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "molten_iron_cauldron");
// Pass our `CauldronInteraction.Dispatcher` to super.
public MoltenIronCauldronBlock(Properties properties) {
super(properties, CAULDRON_INTERACTIONS);
}
// We assume that our cauldron can only ever be completely full, i.e. that we don't have "bottles"
// or a similar intermediary unit present.
@Override
public boolean isFull(BlockState state) {
return true;
}
// Vanilla water cauldrons output 1-3 based on the fill level,
// we are always full and therefore output 3.
@Override
protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) {
return 3;
}
// A full cauldron has its visual height at 0.9375 (= 15/16).
@Override
protected double getContentHeight(BlockState state) {
return 0.9375;
}
}
然后使用这个炼药锅类注册方块:
// Assuming a DeferredRegister.Blocks named BLOCKS.
public static final DeferredBlock<MoltenIronCauldronBlock> MOLTEN_IRON_CAULDRON = BLOCKS.registerBlock(
// The registry name.
"molten_iron_cauldron",
// The cauldron constructor reference.
MoltenIronCauldronBlock::new,
// The properties to use. We generally copy the vanilla cauldron.
// Since we gave molten iron a glow, we also apply that to the cauldron.
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CAULDRON).lightLevel(_ -> 5)
);
接着,需要在 RegisterCauldronFluidContentEvent 中关联流体和炼药锅:
@SubscribeEvent // on the mod event bus
private static void registerCauldronFluidContent(RegisterCauldronFluidContentEvent event) {
event.register(
// The cauldron block.
ModBlocks.MOLTEN_IRON_CAULDRON.get(),
// The fluid.
ModFluids.MOLTEN_IRON.get(),
// The amount.
FluidType.BUCKET_VOLUME,
// The "level" block state property. Since we don't have one, we pass null.
null);
}
最后,流体炼药锅也是普通方块,因此需要相应的数据生成配置,包括翻译、方块模型、战利品表和标签:
// In the language provider
@Override
protected void addTranslations() {
add(ModFluids.MOLTEN_IRON_TYPE.get().getDescriptionId(), "Molten Iron");
addItem(ModItems.MOLTEN_IRON_BUCKET, "Molten Iron Bucket");
addBlock(ModBlocks.MOLTEN_IRON_CAULDRON, "Molten Iron Cauldron");
}
// In the model provider
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
blockModels.createNonTemplateModelBlock(ModBlocks.MOLTEN_IRON.get());
itemModels.itemModelOutput.accept(...);
blockModels.blockStateOutput.accept(BlockModelGenerators.createSimpleBlock(
// Our cauldron block.
ModBlocks.MOLTEN_IRON_CAULDRON.get(),
// We use the `CAULDRON_FULL` model template.
BlockModelGenerators.plainVariant(ModelTemplates.CAULDRON_FULL.create(
// Our cauldron block.
ModBlocks.MOLTEN_IRON_CAULDRON.get(),
// The cauldron fluid texture mapping.
TextureMapping.cauldron(TextureMapping.getBlockTexture(ModBlocks.MOLTEN_IRON.get(), "_still")),
blockModels.modelOutput))));
}
// In the block loot sub provider
@Override
protected void generate() {
// Drop an empty cauldron when mined.
dropOther(ModBlocks.MOLTEN_IRON_CAULDRON.get(), Items.CAULDRON);
}
// In the block tags provider
@Override
protected void addTags(HolderLookup.Provider provider) {
tag(BlockTags.CAULDRONS).add(ModBlocks.MOLTEN_IRON_CAULDRON.get());
}
炼药锅交互
现在已有炼药锅,但还无法与它交互,也无法在生存模式中获得。需要注册炼药锅交互。前面的炼药锅类中有一个 CauldronInteraction.Dispatcher,现在会用到它。
炼药锅交互涉及两个事件。首先注册 CauldronInteraction.Dispatcher:
@SubscribeEvent // on the mod event bus
private static void registerCauldronInteractionDispatchers(RegisterCauldronInteractionEvent.Dispatcher event) {
event.register(
// A unique identifier.
MoltenIronCauldronBlock.CAULDRON_INTERACTIONS_ID,
// Our `CauldronInteraction.Dispatcher`.
MoltenIronCauldronBlock.CAULDRON_INTERACTIONS);
}
然后注册实际交互:
@SubscribeEvent
private static void registerCauldronInteractions(RegisterCauldronInteractionEvent.Interaction event) {
// Empty our cauldron when it is right-clicked with an empty bucket.
event.register(
// The id of our cauldron interactions.
MoltenIronCauldronBlock.CAULDRON_INTERACTIONS_ID,
// The item we're right-clicking with.
Items.BUCKET,
// A callback called when right-clicking. Input parameters are the cauldron blockstate,
// the level, the position, the player, the used hand, and the used item stack.
(state, level, pos, player, hand, stack) -> CauldronInteractions.fillBucket(
// Pass along the input parameters.
state, level, pos, player, hand, stack,
// The resulting item stack.
ModItems.MOLTEN_IRON_BUCKET.toStack(),
// A predicate for additional checks if the bucket can be filled.
// We have no additional checks, so we just always return true.
_ -> true,
// The sound event to play when emptying the cauldron.
SoundEvents.BUCKET_FILL_LAVA));
// For compat with vanilla, we need to add handling for when our cauldron is right-clicked
// with water, lava and powder snow buckets. Compat with other mods is handled
// by the bucket fill handler method, see below.
event.register(
MoltenIronCauldronBlock.CAULDRON_INTERACTIONS_ID,
Items.LAVA_BUCKET,
CauldronInteractions::fillLavaInteraction);
event.register(
MoltenIronCauldronBlock.CAULDRON_INTERACTIONS_ID,
Items.WATER_BUCKET,
CauldronInteractions::fillWaterInteraction);
event.register(
MoltenIronCauldronBlock.CAULDRON_INTERACTIONS_ID,
Items.POWDER_SNOW_BUCKET,
CauldronInteractions::fillPowderSnowInteraction);
// When **any** cauldron is right-clicked with our bucket, replace with our cauldron.
// To do so, we use `event#registerToAll()` instead of `event#register()`.
event.registerToAll(ModItems.MOLTEN_IRON_BUCKET.get(),
// A callback called when right-clicking. Input parameters are the cauldron blockstate,
// the level, the position, the player, the used hand, and the used item stack.
(state, level, pos, player, hand, stack) -> CauldronInteractions.emptyBucket(
// Pass along the input parameters, except the state.
level, pos, player, hand, stack,
// The resulting block state.
ModBlocks.MOLTEN_IRON_CAULDRON.get().defaultBlockState(),
// The sound event to play when filling the cauldron.
SoundEvents.BUCKET_EMPTY_LAVA));
}
炼药锅交互不只用于桶。原版还有一些其他配方,大多用于洗去染色物品的颜色,它们采用的机制基本相同。更多信息请查看 CauldronInteractions 类,其中也可以找到原版炼药锅交互分发器。
滴水石锥滴落
利用滴水石锥和炼药锅,可以再生水和熔岩。对 FluidType.Properties 调用 addDripstoneDripping(),即可为 Mod 的炼药锅扩展这项功能:
FluidType.Properties.create()
.addDripstoneDripping(
// Probability per attempt to fill the cauldron. Vanilla values can be found
// at PointedDripstoneBlock.WATER_TRANSFER_PROBABILITY_PER_RANDOM_TICK
// and PointedDripstoneBlock.LAVA_TRANSFER_PROBABILITY_PER_RANDOM_TICK
// and are ~17.5% and 5.9%, respectively. Water is 3 times more likely to drip
// as it fills the cauldron in 3 levels, while lava cauldrons fill all at once.
// With our molten iron fluid, we follow the lava mechanics.
PointedDripstoneBlock.LAVA_TRANSFER_PROBABILITY_PER_RANDOM_TICK,
// The dripping particle to use. May be null, in which case the dimension's
// default dripping particle - lava dripping in the nether, water dripping
// in all others - will be used instead. Let's assume that we made a particle
// at ModParticles.DRIPPING_DRIPSTONE_MOLTEN_IRON, and use it here.
ModParticles.DRIPPING_DRIPSTONE_MOLTEN_IRON.get(),
// The filled cauldron block to turn the empty cauldron into upon dripping.
ModBlocks.MOLTEN_IRON_CAULDRON.get(),
// The sound to play when filling the cauldron. May be null, in which case no sound
// will be played. As with the particle, let's assume that we made a sound event
// at ModSounds.POINTED_DRIPSTONE_DRIP_MOLTEN_IRON_INTO_CAULDRON, and use it here.
ModSounds.POINTED_DRIPSTONE_DRIP_MOLTEN_IRON_INTO_CAULDRON.get()
);
来源: NeoForged/Documentation(26.1)。中文翻译依据提交
4a680cdd4d15。