简幻欢第三方镜像文档
模组开发教程

NeoForge 食物与作物开发完全指南

重点:食物、食物效果、作物方块、生长与骨粉、战利品表、数据驱动。


目录

  1. 前置说明:你将学到什么

  2. 方法论总览:先建模型,再写代码

  3. 源码地图:食物与作物相关核心类

  4. 食物系统底层原理(必读)

  5. 从 0 写第一个自定义食物

  6. 食物特殊效果设计与实现

  7. 食物进阶:容器返还、动态属性、平衡设计

  8. 作物系统底层原理(必读)

  9. 从 0 写第一个自定义作物方块

  10. 作物生长控制:随机刻、生长速度、骨粉

  11. 作物战利品表:从逻辑到掉落

  12. 数据生成(DataGen)与手写 JSON 的取舍

  13. 相关数据驱动内容:堆肥与耕地标签

  14. 完整实战:食物 + 作物 + 战利品一体化模板

  15. 调试、测试与排错清单

  16. 设计方法论:如何扩展到“农业系统”模组

  17. 附录 A:关键源码索引(建议收藏)

  18. 附录 B:可复用模板代码


1. 前置说明:你将学到什么

这不是“只教你把一个物品注册出来”的速成文。

这份文档会带你掌握三层能力:

  • 实现能力:能做出可食用物品、可生长作物、正确掉落。

  • 理解能力:知道为什么这样做,理解调用链和数据流。

  • 设计能力:能做平衡性、可扩展性、可维护性设计。

你最终要形成的思维:

“我不是在写一个食物 item,我是在设计一个完整的食物-作物系统。”


2. 方法论总览:先建模型,再写代码

2.1 新手常见失败模式

很多人一上来就开始写类,结果:

  • 食物能吃但效果异常。

  • 作物能种但不长。

  • 成熟后掉落不对。

  • 骨粉行为不符合预期。

  • 后期难扩展,改一个地方崩一片。

原因通常不是 Java 不会,而是没有系统模型。

2.2 推荐方法论(四步循环)

第一步:画出“状态机”

例如作物:

  • AGE=0(幼苗)

  • AGE=1..n-1(生长期)

  • AGE=n(成熟)

你必须先定义:

  • 什么条件推进状态(光照、土壤、随机刻)。

  • 什么操作推进状态(骨粉)。

  • 每个状态视觉如何表达(模型 / 方块状态)。

  • 每个状态掉落如何表达(loot condition)。

第二步:映射到源码钩子

把设计映射到具体方法:

  • 食物属性:FoodProperties.Builder

  • 食物入口:Item.use()

  • 食物结算:Player.eat()

  • 作物随机生长:CropBlock.randomTick()

  • 骨粉接口:BonemealableBlock

  • 骨粉入口:BoneMealItem.applyBonemeal()

  • 作物掉落帮助器:createCropDrops()

第三步:先做“最小可用纵切”

先做一个最简闭环:

  • 一个可吃食物(无效果)

  • 一个可种作物(默认生长)

  • 一个成熟掉落

跑通后再加特殊效果,不要反过来。

第四步:数据驱动优先

能用数据配置(如 loot、datamap)就不要硬编码。

这样做的收益:

  • 平衡调整快

  • 热更新方便

  • 后续和整合包兼容更好


3. 源码地图:食物与作物相关核心类

先建立“地图感”,后面你会快很多。

3.1 食物核心

  • 食物数据结构:FoodProperties

  • 玩家饱食系统:FoodData

  • 物品使用入口:Item

  • 生物食用通用逻辑:LivingEntity.eat()

  • 玩家食用覆盖逻辑:Player.eat()

  • 食物数据组件键:DataComponents.FOOD

  • 原版食物参数参考:Foods

3.2 作物核心

  • 基础作物方块:CropBlock

  • 骨粉接口:BonemealableBlock

  • 骨粉实现入口:BoneMealItem

  • 特化作物参考:

    • BeetrootBlock

    • CarrotBlock

    • PotatoBlock

    • TorchflowerCropBlock

    • PitcherCropBlock

    • StemBlock

3.3 战利品与数据生成核心

  • 生成器入口:LootTableProvider

  • 方块 loot 帮助基类:BlockLootSubProvider

  • 原版作物掉落实现:VanillaBlockLoot

3.4 数据驱动相关

  • 堆肥 datamap 定义:NeoForgeDataMaps.COMPOSTABLES

  • 堆肥 datamap 示例:compostables.json

  • 村民耕地标签示例:villager_farmlands.json


4. 食物系统底层原理(必读)

这一章是“会不会踩坑”的分水岭。

4.1 食物不是 isEdible,而是数据组件

在现代版本里,食物本质上是一个 DataComponent:

  • 键:DataComponents.FOOD

  • 值类型:FoodProperties

在 Item.Properties.food() 中,本质就是把 FOOD 组件塞到物品组件表里。

这意味着:

  • 食物定义天然支持序列化(Codec)。

  • 支持网络同步(StreamCodec)。

  • 支持数据驱动思路(理论上可来自数据系统)。

4.2 FoodProperties 字段语义(底层)

查看 FoodProperties``记录类型:

  • nutrition:增加的饥饿值(鸡腿)

  • saturation:增加的饱和度(不是 modifier,是最终值)

  • canAlwaysEat:是否可在饱食时吃

  • eatSeconds:进食耗时秒数

  • usingConvertsTo:吃完后转化得到容器(如碗)

  • effects:可能触发的效果列表(含概率)

注意 FoodProperties.Builder.build():

saturationModifier 最终会经过 FoodConstants.saturationByModifier 计算为 saturation。

4.3 食用调用链

链路概览

  1. 玩家右键物品,走 Item.use()

  2. 取 FoodProperties:itemstack.getFoodProperties(player)

  3. 可吃则 startUsingItem

  4. 结束使用时调用 Item.finishUsingItem()

  5. 委托到 livingEntity.eat(level, stack, foodProperties)

  6. 若实体是玩家,走覆盖的 Player.eat()

  7. 更新 FoodData.eat(foodProperties)

  8. 触发统计、成就、音效、容器返还

关键结算点

在 LivingEntity.eat() 中完成:

  • 播放吃东西声音

  • 应用食物效果(addEatEffect())

  • food.consume(1, this) 消耗堆叠

  • 发送 GameEvent.EAT

在 Player.eat() 中额外完成:

  • 饱食值变化

  • 统计 Stats.ITEM_USED

  • 触发 CriteriaTriggers.CONSUME_ITEM

  • 处理 usingConvertsTo 容器返还逻辑

4.4 FoodData 的饥饿-饱和-消耗机制

看 FoodData.tick():

  • 先处理 exhaustionLevel > 4 的消耗阈值

  • 优先扣饱和度,再扣饥饿值

  • 满饱和可更快回血

  • 饥饿值低时触发饥饿伤害

这决定了食物平衡的核心:

  • 高营养低饱和:短期补鸡腿,持续航行弱

  • 中营养高饱和:探索效率高

  • alwaysEdible + 特效:战斗工具型食物

4.5 效果系统概率执行细节

LivingEntity.addEatEffect() 会遍历 FoodProperties.effects():

  • 每条 effect 做一次随机判定:random < probability

  • 命中则 addEffect(...)

这意味着多个效果是独立概率而非互斥。

4.6 IItemExtension.getFoodProperties:栈敏感扩展点

NeoForge 提供了扩展入口:

  • IItemExtension.getFoodProperties(stack, entity)

  • IItemStackExtension.getFoodProperties(entity)

默认实现就是读 DataComponents.FOOD。

意义:你可以根据 ItemStack 内容(组件/NBT/耐久等)动态返回不同食物属性。


5. 从 0 写第一个自定义食物

本节先做最小可用。

5.1 目标

实现一个“蓝莓派”:

  • 可食用

  • 恢复适中

  • 无特殊效果

5.2 注册代码(示例)

// src/main/java/com/example/farmmod/item/ModItems.java
package com.example.farmmod.item;

import net.minecraft.world.food.FoodProperties;
import net.minecraft.world.item.Item;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.registries.DeferredItem;
import net.neoforged.neoforge.registries.DeferredRegister;

public class ModItems {
    public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems("farmmod");

    public static final DeferredItem<Item> BLUEBERRY_PIE = ITEMS.registerSimpleItem(
        "blueberry_pie",
        new Item.Properties().food(
            new FoodProperties.Builder()
                .nutrition(6)
                .saturationModifier(0.6F)
                .build()
        )
    );

    public static void register(IEventBus modBus) {
        ITEMS.register(modBus);
    }
}

5.3 底层对照(你要知道发生了什么)

  • .food(...) 对应 Item.Properties.food()

  • 右键入口走 Item.use()

  • 食用结算走 Item.finishUsingItem()

5.4 常见错误

  1. 只注册物品没配模型 -> 紫黑方块

  2. 翻译键写错 -> 显示 key 本身

  3. 忘记加入创造栏 -> 测试找不到


6. 食物特殊效果设计与实现

本章是“自定义食物”核心。

6.1 FoodProperties.Builder 常用能力

在 FoodProperties.Builder 中常用方法:

  • .nutrition(int)

  • .saturationModifier(float)

  • .alwaysEdible()

  • .fast()(把 eatSeconds 设为 0.8)

  • .effect(Supplier<MobEffectInstance>, probability)

  • .usingConvertsTo(ItemLike)

6.2 示例:可概率中毒的“可疑果实”

public static final DeferredItem<Item> SUSPICIOUS_BERRY = ITEMS.registerSimpleItem(
    "suspicious_berry",
    new Item.Properties().food(
        new FoodProperties.Builder()
            .nutrition(3)
            .saturationModifier(0.2F)
            .effect(() -> new MobEffectInstance(MobEffects.POISON, 100, 0), 0.35F)
            .effect(() -> new MobEffectInstance(MobEffects.HUNGER, 200, 0), 0.20F)
            .build()
    )
);

方法论要点:

  • 多个 .effect(...) 是叠加独立判定。

  • 不要在一个食物塞太多强效果,否则平衡会崩。

6.3 示例:战斗型“肾上腺糖”

public static final DeferredItem<Item> ADRENALINE_CANDY = ITEMS.registerSimpleItem(
    "adrenaline_candy",
    new Item.Properties().food(
        new FoodProperties.Builder()
            .nutrition(2)
            .saturationModifier(0.1F)
            .alwaysEdible()
            .fast()
            .effect(() -> new MobEffectInstance(MobEffects.MOVEMENT_SPEED, 100, 1), 1.0F)
            .build()
    )
);

这个食物是典型“功能型道具”,不是主粮。

6.4 示例:吃完返还容器

public static final DeferredItem<Item> HERB_STEW = ITEMS.registerSimpleItem(
    "herb_stew",
    new Item.Properties().stacksTo(1).food(
        new FoodProperties.Builder()
            .nutrition(7)
            .saturationModifier(0.6F)
            .usingConvertsTo(Items.BOWL) // 此处的这个数据组件在1.21.2已经被移除,可以通过别的方法实现该功能
            .build()
    )
);

容器返还逻辑发生在 Player.eat()。

6.5 示例:继承 Item 做后处理

有些效果不适合放在 FoodProperties(如发包、复杂世界交互),可重写 finishUsingItem()。

package com.example.farmmod.item;

import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;

public class VitalityFruitItem extends Item {
    public VitalityFruitItem(Properties properties) {
        super(properties);
    }

    @Override
    public ItemStack finishUsingItem(ItemStack stack, Level level, LivingEntity entity) {
        ItemStack result = super.finishUsingItem(stack, level, entity);

        // 只在服务端执行逻辑,避免双端重复
        if (!level.isClientSide && entity instanceof Player player) {
            player.addEffect(new MobEffectInstance(MobEffects.REGENERATION, 60, 0));

            // 可在这里触发更多自定义逻辑:统计、能力系统、任务进度等
            if (level instanceof ServerLevel serverLevel) {
                // serverLevel 侧的世界逻辑
            }
        }

        return result;
    }
}

6.6 何时用 FoodProperties.effect,何时重写 finishUsingItem

场景推荐方式
简单药水效果 + 概率FoodProperties.effect(...)
吃后返还容器usingConvertsTo(...)
与世界交互(生成实体/修改方块)重写finishUsingItem
与你模组能力系统联动重写finishUsingItem
仅数值回复nutrition/saturation

7. 食物进阶:容器返还、动态属性、平衡设计

7.1 动态食物属性(栈敏感)

你可以根据 ItemStack 状态动态返回不同食物参数。

示意代码:

@Override
public FoodProperties getFoodProperties(ItemStack stack, @Nullable LivingEntity entity) {
    // 比如根据自定义组件“新鲜度”调整数值
    int freshness = stack.getOrDefault(MyDataComponents.FRESHNESS, 100);

    if (freshness >= 80) {
        return new FoodProperties.Builder().nutrition(6).saturationModifier(0.7F).build();
    } else if (freshness >= 40) {
        return new FoodProperties.Builder().nutrition(4).saturationModifier(0.4F).build();
    } else {
        return new FoodProperties.Builder()
            .nutrition(2)
            .saturationModifier(0.1F)
            .effect(() -> new MobEffectInstance(MobEffects.HUNGER, 200, 0), 0.5F)
            .build();
    }
}

接口来源见 IItemExtension.getFoodProperties。

7.2 平衡设计:食物分层法

建议把食物分三层:

  1. 基础口粮:主世界日常,性价比稳定。

  2. 战斗补给:alwaysEdible + 快速食用 + 短时 Buff。

  3. 代价型强食物:高收益 + 副作用(如中毒、虚弱)。

这样比“所有食物都比牛排强”更健康。

7.3 可测量平衡指标

  • 单次恢复总值(nutrition + saturation)

  • 获取难度(种植时间、配方成本)

  • 战斗窗口收益(吃完到收益生效所需时间)

  • 连续食用收益递减(是否过强)

7.4 参考原版参数

建议先对照 Foods 再定你自己的参数区间。


8. 作物系统底层原理(必读)

8.1 CropBlock 的核心结构

CropBlock 是大多数农作物的基类。

核心要素:

  • 年龄属性 AGE

  • 最大年龄 MAX_AGE

  • 各年龄碰撞/形状数组 SHAPE_BY_AGE

  • 土壤检查 mayPlaceOn()

  • 随机生长 randomTick()

  • 骨粉实现 performBonemeal()

8.2 生长条件判定

在 randomTick() 中:

  • 光照需 >=9

  • 非成熟才尝试生长

  • 生长概率由 getGrowthSpeed 影响

8.3 getGrowthSpeed 真正决定了农田效率

看 getGrowthSpeed():

  • 统计下方 3x3 土壤贡献

  • 可持续植物 + 肥沃土壤有额外加成

  • 相邻同作物会降速(避免无脑密排)

这是你设计“农场布局玩法”的底层依据。

8.4 生存性判定 canSurvive

canSurvive() 组合了:

  • 土壤可持续性(canSustainPlant,支持 TriState)

  • 光照判定 hasSufficientLight()

8.5 骨粉系统接口模型

CropBlock 实现了 BonemealableBlock:

必须理解三个方法:

  • isValidBonemealTarget:现在能不能施肥

  • isBonemealSuccess:这次施肥是否成功(可做概率)

  • performBonemeal:成功后怎么长

骨粉道具入口在 BoneMealItem.applyBonemeal()。

8.6 为什么你的作物可能“能种但不长”

典型原因:

  • 没有 .randomTicks()

  • isRandomlyTicking 返回错误

  • AGE 属性定义/读取不一致

  • 光照不足

  • canSurvive 条件太严格


9. 从 0 写第一个自定义作物方块

我们做一个“蓝莓作物”,流程完全可复用。

9.1 设计目标

  • 8 阶段生长(0-7)

  • 只能种在耕地

  • 光照足够时随机生长

  • 骨粉可加速

  • 成熟后掉蓝莓和种子

9.2 自定义方块类

package com.example.farmmod.block;

import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.ItemLike;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.CropBlock;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import com.example.farmmod.item.ModItems;

public class BlueberryCropBlock extends CropBlock {
    public static final MapCodec<BlueberryCropBlock> CODEC = simpleCodec(BlueberryCropBlock::new);

    private static final VoxelShape[] SHAPE_BY_AGE = new VoxelShape[] {
        Block.box(0, 0, 0, 16, 2, 16),
        Block.box(0, 0, 0, 16, 3, 16),
        Block.box(0, 0, 0, 16, 5, 16),
        Block.box(0, 0, 0, 16, 7, 16),
        Block.box(0, 0, 0, 16, 9, 16),
        Block.box(0, 0, 0, 16, 11, 16),
        Block.box(0, 0, 0, 16, 13, 16),
        Block.box(0, 0, 0, 16, 15, 16)
    };

    public BlueberryCropBlock(BlockBehaviour.Properties properties) {
        super(properties);
    }

    @Override
    public MapCodec<BlueberryCropBlock> codec() {
        return CODEC;
    }

    @Override
    protected ItemLike getBaseSeedId() {
        return ModItems.BLUEBERRY_SEEDS.get();
    }

    @Override
    protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
        return SHAPE_BY_AGE[this.getAge(state)];
    }
}

9.3 注册方块与方块物品

package com.example.farmmod.block;

import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.material.MapColor;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.registries.DeferredBlock;
import net.neoforged.neoforge.registries.DeferredRegister;

public class ModBlocks {
    public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks("farmmod");

    public static final DeferredBlock<Block> BLUEBERRY_CROP = BLOCKS.register(
        "blueberry_crop",
        () -> new BlueberryCropBlock(
            BlockBehaviour.Properties.of()
                .mapColor(MapColor.PLANT)
                .noCollission()
                .randomTicks()
                .instabreak()
        )
    );

    public static void register(IEventBus modBus) {
        BLOCKS.register(modBus);
    }
}

这里属性风格可参考原版 Blocks.WHEAT。

9.4 作物资源文件(最小集)

你至少需要:

  • blockstates/blueberry_crop.json

  • models/block/blueberry_crop_stage0..7.json

  • textures/block/blueberry_crop_stage0..7.png

  • models/item/blueberry_seeds.json

  • textures/item/blueberry_seeds.png

blockstates 示例:

{
  "variants": {
    "age=0": { "model": "farmmod:block/blueberry_crop_stage0" },
    "age=1": { "model": "farmmod:block/blueberry_crop_stage1" },
    "age=2": { "model": "farmmod:block/blueberry_crop_stage2" },
    "age=3": { "model": "farmmod:block/blueberry_crop_stage3" },
    "age=4": { "model": "farmmod:block/blueberry_crop_stage4" },
    "age=5": { "model": "farmmod:block/blueberry_crop_stage5" },
    "age=6": { "model": "farmmod:block/blueberry_crop_stage6" },
    "age=7": { "model": "farmmod:block/blueberry_crop_stage7" }
  }
}

10. 作物生长控制:随机刻、生长速度、骨粉

10.1 调整自然生长速度

默认速度在 CropBlock.randomTick()。

你可像 BeetrootBlock.randomTick() 一样“抽样”调用父类,降低生长频率。

@Override
protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
    // 仅 1/2 的随机刻进入父逻辑 => 约等价减速
    if (random.nextBoolean()) {
        super.randomTick(state, level, pos, random);
    }
}

10.2 调整骨粉增量

默认骨粉增量逻辑见 getBonemealAgeIncrease()。

@Override
protected int getBonemealAgeIncrease(Level level) {
    // 更保守:每次 +1 或 +2
    return level.random.nextBoolean() ? 1 : 2;
}

10.3 自定义骨粉成功率

@Override
public boolean isBonemealSuccess(Level level, RandomSource random, BlockPos pos, BlockState state) {
    // 70% 成功率
    return random.nextFloat() < 0.7F;
}

10.4 自定义“可施肥目标”

@Override
public boolean isValidBonemealTarget(LevelReader level, BlockPos pos, BlockState state) {
    // 例如:只有白天可施肥(示例)
    return !isMaxAge(state) && level instanceof Level l && l.isDay();
}

10.5 更高级:重写 growCrops

@Override
public void growCrops(Level level, BlockPos pos, BlockState state) {
    int age = getAge(state);

    // 示例:后期成长更慢
    int inc = (age < 4) ? 2 : 1;
    int next = Math.min(getMaxAge(), age + inc);

    level.setBlock(pos, getStateForAge(next), 2);
}

10.6 参考特化实现

  • 甜菜:生长慢 + 骨粉增量低,见 BeetrootBlock

  • 火把花:最终年龄变成其他方块,见 TorchflowerCropBlock.getStateForAge

  • 投手荚:双格结构,见 PitcherCropBlock


11. 作物战利品表:从逻辑到掉落

战利品表是很多新手忽略但最关键的一环。

11.1 原版作物掉落模式

原版在 VanillaBlockLoot 中展示了标准做法:

  • 先定义成熟条件(BlockStatePropertyCondition)

  • 用 createCropDrops()

示例(小麦):

  • 成熟时掉小麦 + 额外种子(受时运)

  • 未成熟时掉种子

11.2 createCropDrops 的真实语义

看 createCropDrops 注释:

  • 条件不满足(未成熟):掉 seedsItem

  • 条件满足(成熟):掉 grownCropItem + 附加种子池

这是你做“统一风格作物”最推荐的入口。

11.3 自定义作物 loot(DataGen 方式)

package com.example.farmmod.data;

import java.util.Set;
import java.util.concurrent.CompletableFuture;
import net.minecraft.advancements.critereon.StatePropertiesPredicate;
import net.minecraft.core.HolderLookup;
import net.minecraft.data.loot.BlockLootSubProvider;
import net.minecraft.world.flag.FeatureFlags;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.storage.loot.predicates.LootItemBlockStatePropertyCondition;
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
import com.example.farmmod.block.ModBlocks;
import com.example.farmmod.block.BlueberryCropBlock;
import com.example.farmmod.item.ModItems;

public class ModBlockLoot extends BlockLootSubProvider {
    public ModBlockLoot(HolderLookup.Provider registries) {
        super(Set.of(), FeatureFlags.REGISTRY.allFlags(), registries);
    }

    @Override
    protected void generate() {
        LootItemCondition.Builder mature = LootItemBlockStatePropertyCondition
            .hasBlockStateProperties(ModBlocks.BLUEBERRY_CROP.get())
            .setProperties(StatePropertiesPredicate.Builder.properties().hasProperty(BlueberryCropBlock.AGE, 7));

        add(
            ModBlocks.BLUEBERRY_CROP.get(),
            createCropDrops(
                ModBlocks.BLUEBERRY_CROP.get(),
                ModItems.BLUEBERRY.get(),
                ModItems.BLUEBERRY_SEEDS.get(),
                mature
            )
        );
    }

    @Override
    protected Iterable<Block> getKnownBlocks() {
        return ModBlocks.BLOCKS.getEntries().stream().map(h -> (Block) h.get()).toList();
    }
}

11.4 手写 JSON 等价实现(示意)

如果不用 DataGen,可手写 data/farmmod/loot_table/blocks/blueberry_crop.json。

重点条件:

  • minecraft:block_state_property

  • properties: { "age": "7" }

但复杂条件手写很容易出错,建议长期项目优先 DataGen。

11.5 进阶:按年龄分层掉落

你可以像 createStemDrops 那样按年龄决定掉落数量。

思路:

  • age<4:只掉 0~1 种子

  • age>=4:掉果实 + 小概率额外果实

  • age=7:保证掉果实,并吃时运


12. 数据生成(DataGen)与手写 JSON 的取舍

12.1 你需要理解 LootTableProvider 在做什么

LootTableProvider.run() 做了三件事:

  1. 收集所有子提供器生成的表

  2. 校验(validation)

  3. 输出稳定 JSON

这意味着:

  • DataGen 不只是“方便”,还是正确性护栏。

12.2 子提供器机制

LootTableProvider.SubProviderEntry 绑定了:

  • provider 工厂

  • LootContextParamSet

方块掉落必须用 BLOCK 参数集。

12.3 原版如何装配

看 VanillaLootTableProvider.create:

把各类型 loot provider 统一打包。

你的模组也建议类似组织,而不是把所有生成塞一个类。

12.4 推荐工程实践

  • data/loot/ModBlockLoot.java

  • data/loot/ModLootTableProvider.java

  • data/recipe/...

  • data/tag/...

按类型拆开,后期维护成本极大下降。


13. 相关数据驱动内容:堆肥与耕地标签

很多农业模组忽视这块,导致“系统割裂”。

13.1 堆肥支持(Compostables)

NeoForge 把原版 ComposterBlock.COMPOSTABLES 映射为 datamap:

  • 定义见 NeoForgeDataMaps.COMPOSTABLES

  • 数据路径说明见同文件注释 NeoForgeDataMaps

示例数据可参考 compostables.json。

建议:你的作物果实和副产物都配置合理堆肥概率。

13.2 村民耕地兼容

村民相关 AI(如耕地工作)会检查可用耕地。

NeoForge 提供了标签示例 villager_farmlands.json。

如果你做自定义耕地方块,应考虑加入相关标签,避免“玩家能种,村民不会种”。

13.3 NeoForgeDataMapsProvider 给你的启示

NeoForgeDataMapsProvider 展示了如何把原版映射转成数据。

可学习点:

  • 用数据构建行为,而非散乱硬编码。

  • 通过 provider 统一管理可调参数。


14. 完整实战:食物 + 作物 + 战利品一体化模板

下面给出一个可直接改造的“完整最小闭环”。

14.1 结构建议

src/main/java/com/example/farmmod/
├── FarmMod.java
├── item/
│   ├── ModItems.java
│   └── VitalityFruitItem.java
├── block/
│   ├── ModBlocks.java
│   └── BlueberryCropBlock.java
└── data/
    ├── loot/
    │   ├── ModBlockLoot.java
    │   └── ModLootTableProvider.java
    └── ModDataGenerators.java

14.2 食物注册(综合)

package com.example.farmmod.item;

import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.food.FoodProperties;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.Items;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.registries.DeferredItem;
import net.neoforged.neoforge.registries.DeferredRegister;

public class ModItems {
    public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems("farmmod");

    public static final DeferredItem<Item> BLUEBERRY = ITEMS.registerSimpleItem(
        "blueberry",
        new Item.Properties().food(
            new FoodProperties.Builder()
                .nutrition(2)
                .saturationModifier(0.2F)
                .build()
        )
    );

    public static final DeferredItem<Item> BLUEBERRY_SEEDS = ITEMS.registerSimpleItem(
        "blueberry_seeds",
        new Item.Properties()
    );

    public static final DeferredItem<Item> BLUEBERRY_JAM = ITEMS.registerSimpleItem(
        "blueberry_jam",
        new Item.Properties().stacksTo(16).food(
            new FoodProperties.Builder()
                .nutrition(5)
                .saturationModifier(0.4F)
                .alwaysEdible()
                .effect(() -> new MobEffectInstance(MobEffects.REGENERATION, 60, 0), 0.6F)
                .usingConvertsTo(Items.GLASS_BOTTLE)
                .build()
        )
    );

    public static final DeferredItem<VitalityFruitItem> VITALITY_FRUIT = ITEMS.registerItem(
        "vitality_fruit",
        VitalityFruitItem::new,
        new Item.Properties().food(
            new FoodProperties.Builder()
                .nutrition(4)
                .saturationModifier(0.3F)
                .build()
        )
    );

    public static void register(IEventBus modBus) {
        ITEMS.register(modBus);
    }
}

14.3 作物方块(综合)

package com.example.farmmod.block;

import com.mojang.serialization.MapCodec;
import com.example.farmmod.item.ModItems;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.ItemLike;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.CropBlock;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;

public class BlueberryCropBlock extends CropBlock {
    public static final MapCodec<BlueberryCropBlock> CODEC = simpleCodec(BlueberryCropBlock::new);

    private static final VoxelShape[] SHAPE_BY_AGE = new VoxelShape[] {
        Block.box(0, 0, 0, 16, 2, 16),
        Block.box(0, 0, 0, 16, 3, 16),
        Block.box(0, 0, 0, 16, 5, 16),
        Block.box(0, 0, 0, 16, 7, 16),
        Block.box(0, 0, 0, 16, 9, 16),
        Block.box(0, 0, 0, 16, 11, 16),
        Block.box(0, 0, 0, 16, 13, 16),
        Block.box(0, 0, 0, 16, 15, 16)
    };

    public BlueberryCropBlock(BlockBehaviour.Properties properties) {
        super(properties);
    }

    @Override
    public MapCodec<BlueberryCropBlock> codec() {
        return CODEC;
    }

    @Override
    protected ItemLike getBaseSeedId() {
        return ModItems.BLUEBERRY_SEEDS.get();
    }

    @Override
    protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
        return SHAPE_BY_AGE[this.getAge(state)];
    }

    @Override
    protected int getBonemealAgeIncrease(Level level) {
        // 稍微保守,避免太快成熟
        return level.random.nextInt(2) + 1;
    }

    @Override
    protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
        // 生长稍慢:约 2/3 的 random tick 才继续默认逻辑
        if (random.nextInt(3) != 0) {
            super.randomTick(state, level, pos, random);
        }
    }
}

14.4 战利品生成(综合)

package com.example.farmmod.data.loot;

import com.example.farmmod.block.BlueberryCropBlock;
import com.example.farmmod.block.ModBlocks;
import com.example.farmmod.item.ModItems;
import java.util.Set;
import net.minecraft.advancements.critereon.StatePropertiesPredicate;
import net.minecraft.core.HolderLookup;
import net.minecraft.data.loot.BlockLootSubProvider;
import net.minecraft.world.flag.FeatureFlags;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.storage.loot.predicates.LootItemBlockStatePropertyCondition;
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;

public class ModBlockLoot extends BlockLootSubProvider {
    public ModBlockLoot(HolderLookup.Provider registries) {
        super(Set.of(), FeatureFlags.REGISTRY.allFlags(), registries);
    }

    @Override
    protected void generate() {
        LootItemCondition.Builder mature = LootItemBlockStatePropertyCondition
            .hasBlockStateProperties(ModBlocks.BLUEBERRY_CROP.get())
            .setProperties(StatePropertiesPredicate.Builder.properties().hasProperty(BlueberryCropBlock.AGE, 7));

        add(
            ModBlocks.BLUEBERRY_CROP.get(),
            createCropDrops(
                ModBlocks.BLUEBERRY_CROP.get(),
                ModItems.BLUEBERRY.get(),
                ModItems.BLUEBERRY_SEEDS.get(),
                mature
            )
        );
    }

    @Override
    protected Iterable<Block> getKnownBlocks() {
        return ModBlocks.BLOCKS.getEntries().stream().map(h -> (Block) h.get()).toList();
    }
}

14.5 这个模板为什么稳

因为它遵循了原版模式:

  • 生长逻辑复用 CropBlock

  • 骨粉逻辑复用 BonemealableBlock

  • 掉落逻辑复用 createCropDrops


15. 调试、测试与排错清单

15.1 食物问题排查

现象:右键无反应

检查:

  • 是否配置了 FoodProperties(DataComponents.FOOD)

  • 是否覆盖了 use() 且忘记 startUsingItem

现象:能吃但不加饱食

检查:

  • 是否错误地绕过了 super.finishUsingItem

  • 是否把逻辑都写在客户端分支

现象:效果不触发

检查:

  • 概率是否太低

  • 是否在服务端执行(addEatEffect 仅服务端)

现象:容器不返还

检查:

  • FoodProperties.usingConvertsTo 是否配置

  • 物品是否创造模式无限材料分支

15.2 作物问题排查

现象:能种但不长

检查顺序:

  1. 方块属性是否有 .randomTicks()

  2. 光照是否满足(>=9)

  3. isRandomlyTicking 是否正确

  4. canSurvive 是否误判

  5. 土壤是否可 sustain plant

现象:骨粉无效

检查:

  • isValidBonemealTarget 返回值

  • isBonemealSuccess 是否总 false

  • performBonemeal 是否真正 setBlock

现象:作物被破坏后掉落异常

检查:

  • loot table 路径是否正确

  • 成熟条件属性名是否与 blockstate 一致

  • 年龄上限是否与代码一致

15.3 战利品表问题排查

现象:总是掉种子不掉果实

检查:

  • hasProperty(AGE, max) 是否写对

  • 自定义 getAgeProperty 后,loot 是否仍用旧属性

现象:DataGen 生成成功但游戏内不生效

检查:

  • 生成目录是否复制到 resources/data/...

  • 文件 namespace/path 是否和注册名一致

15.4 行为一致性测试清单(建议逐条过)

  •  生存模式可种植

  •  无光照不生长

  •  有光照能生长

  •  骨粉可加速

  •  成熟外观正确

  •  成熟掉落正确

  •  未成熟掉落正确

  •  时运附魔影响符合预期

  •  食物可正常食用

  •  特殊效果概率符合预期

  •  吃后容器返还符合预期

  •  创造模式无异常


16. 设计方法论:如何扩展到“农业系统”模组

这章讲“做项目”而不是“做 demo”。

16.1 三层架构法

把农业系统拆三层:

  1. 行为层:Item / Block / AI 交互

  2. 数据层:loot / datamap / tag

  3. 表现层:模型、贴图、音效、粒子

规则:

  • 行为层尽量薄,逻辑参数放数据层。

  • 表现层绝不反向依赖行为层。

16.2 扩展路线建议(按收益排序)

路线 A:单作物变多作物

先复制模板,保证一致性:

  • 类名规范:XxxCropBlock

  • 资源命名规范:xxx_crop_stageN

  • loot 统一 createCropDrops

路线 B:引入季节 / 温度 / 湿度

做法:

  • 在 randomTick 前增加环境判定

  • 判定参数尽量配置化

  • 把“生长倍率”做数据表

路线 C:食物链联动

  • 生作物 -> 加工食物 -> 功能药膳

  • 用不同恢复模型区分“食物”与“药剂型食物”

16.3 平衡性方法论(很重要)

指标化

给每个食物打分:

  • 回复效率分

  • 获取难度分

  • 战斗效用分

  • 风险代价分

总分要落在你定义的“层级区间”内。

反通胀

若你不断加新食物,容易“数值通胀”。

应使用:

  • 场景差异化(探险食物、战斗食物、农业食物)

  • 条件收益(在特定生物群系增益)

  • 代价机制(副作用、冷却、稀有材料)

16.4 可维护性规范

推荐规范:

  • 每类作物一个类,不做巨型 if-else

  • 所有 AGE 最大值统一常量

  • Loot 条件构建封装成工具函数

  • 食物参数集中定义表


17. 附录 A:关键源码索引(建议收藏)

17.1 食物相关

  • FoodProperties

  • FoodProperties.Builder

  • FoodData

  • Item.use

  • Item.finishUsingItem

  • Item.Properties.food

  • LivingEntity.eat

  • Player.eat

  • DataComponents.FOOD

  • Foods

  • IItemExtension.getFoodProperties

17.2 作物相关

  • CropBlock

  • CropBlock.randomTick

  • CropBlock.getGrowthSpeed

  • CropBlock.canSurvive

  • BonemealableBlock

  • BoneMealItem.applyBonemeal

  • BeetrootBlock

  • TorchflowerCropBlock

  • PitcherCropBlock

  • StemBlock

17.3 战利品相关

  • LootTableProvider

  • BlockLootSubProvider

  • createCropDrops

  • createStemDrops

  • VanillaBlockLoot``作物段

17.4 数据驱动相关

  • NeoForgeDataMaps.COMPOSTABLES

  • NeoForgeDataMapsProvider.gather

  • compostables.json

  • villager_farmlands.json


18. 附录 B:可复用模板代码

以下模板用于“快速开新作物/新食物”,你可以直接复制后改名。

18.1 食物参数工厂模板

package com.example.farmmod.food;

import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.food.FoodProperties;
import net.minecraft.world.item.Items;

public final class ModFoods {
    private ModFoods() {}

    public static final FoodProperties BASIC_BERRY = new FoodProperties.Builder()
        .nutrition(2)
        .saturationModifier(0.2F)
        .build();

    public static final FoodProperties ENERGY_BAR = new FoodProperties.Builder()
        .nutrition(4)
        .saturationModifier(0.4F)
        .alwaysEdible()
        .fast()
        .effect(() -> new MobEffectInstance(MobEffects.MOVEMENT_SPEED, 80, 0), 1.0F)
        .build();

    public static final FoodProperties HERB_SOUP = new FoodProperties.Builder()
        .nutrition(7)
        .saturationModifier(0.6F)
        .usingConvertsTo(Items.BOWL)
        .build();
}

18.2 通用作物基类模板

package com.example.farmmod.block.crop;

import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.ItemLike;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.CropBlock;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;

public abstract class BaseModCropBlock extends CropBlock {
    public BaseModCropBlock(BlockBehaviour.Properties properties) {
        super(properties);
    }

    @Override
    protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
        return getShapeByAge()[getAge(state)];
    }

    protected abstract VoxelShape[] getShapeByAge();

    @Override
    protected abstract ItemLike getBaseSeedId();

    @Override
    public abstract MapCodec<? extends CropBlock> codec();
}

18.3 Loot 条件构建模板

private LootItemCondition.Builder matureCondition(Block crop, IntegerProperty ageProp, int matureAge) {
    return LootItemBlockStatePropertyCondition
        .hasBlockStateProperties(crop)
        .setProperties(StatePropertiesPredicate.Builder.properties().hasProperty(ageProp, matureAge));
}

18.4 多作物批量注册思路(示意)

public record CropDef(
    String id,
    Supplier<Block> cropBlock,
    Supplier<Item> fruitItem,
    Supplier<Item> seedItem,
    IntegerProperty ageProp,
    int matureAge
) {}

private void addCropLoot(CropDef def) {
    add(
        def.cropBlock().get(),
        createCropDrops(
            def.cropBlock().get(),
            def.fruitItem().get(),
            def.seedItem().get(),
            matureCondition(def.cropBlock().get(), def.ageProp(), def.matureAge())
        )
    );
}

18.5 骨粉粒子类型理解

BonemealableBlock.getType() 决定粒子位置/类型, 实际渲染分支在 BoneMealItem.addGrowthParticles。

如果你做“扩散型植物”(邻近传播),可考虑返回 NEIGHBOR_SPREADER 风格语义。


19. 特殊作物形态深挖:不只是 CropBlock

很多人把“作物”理解成“AGE=0..7 的单格植物”。

实际上,原版已经提供了多种形态范式:

  1. 单格标准作物(小麦、胡萝卜、土豆)

  2. 低年龄上限作物(甜菜)

  3. 成熟后转化为另一方块(火把花)

  4. 结果型作物(瓜藤 + 果实方块)

如果你掌握这五种范式,农业模组的表达空间会大很多。

19.1 标准单格作物范式

对应范式:

  • 年龄属性:AGE_7

  • 成熟条件:age=7

  • 战利品:createCropDrops

你可以直接继承 CropBlock 并最小覆盖:

  • getBaseSeedId

  • getShape

这已经能覆盖 70% 的作物需求。

19.2 低年龄上限范式(甜菜式)

参考特征:

  • 最大年龄 3

  • 生长偏慢

  • 骨粉增量受限

示例模板:

public class HerbCropBlock extends CropBlock {
    public static final MapCodec<HerbCropBlock> CODEC = simpleCodec(HerbCropBlock::new);
    public static final IntegerProperty AGE = BlockStateProperties.AGE_3;

    public HerbCropBlock(Properties properties) {
        super(properties);
    }

    @Override
    public MapCodec<HerbCropBlock> codec() {
        return CODEC;
    }

    @Override
    protected IntegerProperty getAgeProperty() {
        return AGE;
    }

    @Override
    public int getMaxAge() {
        return 3;
    }

    @Override
    protected ItemLike getBaseSeedId() {
        return ModItems.HERB_SEEDS.get();
    }

    @Override
    protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
        // 比默认慢
        if (random.nextInt(3) != 0) {
            super.randomTick(state, level, pos, random);
        }
    }

    @Override
    protected int getBonemealAgeIncrease(Level level) {
        // 控制骨粉收益
        return 1;
    }
}

方法论:

  • 当你希望“高价值作物不应快速量产”时,这个范式非常实用。

19.3 成熟转化范式(火把花式)

这种范式适合“苗 -> 成品植株”表达。

关键在于覆盖 getStateForAge:

@Override
public BlockState getStateForAge(int age) {
    return age >= getMaxAge()
        ? ModBlocks.MATURE_PLANT.get().defaultBlockState()
        : super.getStateForAge(age);
}

用途:

  • 草药幼苗成熟后变成可采收灌木

  • 栽培芽成熟后变成装饰花

注意:

  • 你需要分别处理“作物阶段方块”与“成熟实体方块”的 loot。

19.4 瓜藤结果范式(Stem + Fruit)

这类作物不是“成熟后自己掉果实”,而是:

  • 藤蔓先长到满龄

  • 邻位生成果实方块

  • 自身变附着茎

这个范式适合:

  • 南瓜类

  • 葡萄藤类

  • 藤本外延式作物

如果你希望农场有“占地扩展”与布局策略,这个范式优于简单作物。


20. 战利品设计深入:从“能掉”到“可平衡”

20.1 先定义掉落目标而不是先写 JSON

建议先写“掉落需求表”:

状态主产物种子特殊掉落附魔影响
未成熟01无无
成熟1~21~35% 副产物时运加种子

先有表,再落地 loot。这一步能避免后期大改。

20.2 createCropDrops 的优势与边界

优势:

  • 语义清晰

  • 成熟/未成熟条件封装好

  • 已包含常见 fortune 逻辑

边界:

  • 想做复杂分层掉落(按多个年龄段)时需要自定义池。

20.3 自定义多池掉落模板

下面示例展示成熟后:

  • 必掉蓝莓 1

  • 额外蓝莓(时运影响)

  • 小概率掉“稀有种子”

add(
    ModBlocks.BLUEBERRY_CROP.get(),
    applyExplosionDecay(
        ModBlocks.BLUEBERRY_CROP.get(),
        LootTable.lootTable()
            .withPool(
                LootPool.lootPool()
                    .add(LootItem.lootTableItem(ModItems.BLUEBERRY_SEEDS.get())
                        .otherwise(LootItem.lootTableItem(ModItems.BLUEBERRY.get()))
                        .when(matureCondition(ModBlocks.BLUEBERRY_CROP.get(), BlueberryCropBlock.AGE, 7)))
            )
            .withPool(
                LootPool.lootPool()
                    .when(matureCondition(ModBlocks.BLUEBERRY_CROP.get(), BlueberryCropBlock.AGE, 7))
                    .add(LootItem.lootTableItem(ModItems.BLUEBERRY.get())
                        .apply(ApplyBonusCount.addBonusBinomialDistributionCount(
                            registries.lookupOrThrow(Registries.ENCHANTMENT).getOrThrow(Enchantments.FORTUNE),
                            0.45F,
                            2
                        )))
            )
            .withPool(
                LootPool.lootPool()
                    .when(matureCondition(ModBlocks.BLUEBERRY_CROP.get(), BlueberryCropBlock.AGE, 7))
                    .add(LootItem.lootTableItem(ModItems.RARE_SEED.get())
                        .when(LootItemRandomChanceCondition.randomChance(0.05F)))
            )
    )
);

20.4 结果型作物的掉落分工

对“藤 + 果”作物,建议把掉落分到不同方块:

  • 茎:掉种子(概率按年龄)

  • 果实:掉主产物

这样玩家行为会更符合直觉:

  • 打果实拿果

  • 处理茎拿种

20.5 双格作物的掉落一致性

双格作物常见 bug:上半和下半都掉,导致翻倍。

解决原则:

  • 只让下半掉落

  • 上半仅作视觉/碰撞

可通过 blockstate 条件约束 half=lower。

20.6 掉落平衡的四个旋钮

  1. 成熟门槛(年龄上限)

  2. 自然生长速度

  3. 每次收获平均产量

  4. 副产物概率

不要只调其中一个。


21. 生物与 AI 交互:让你的作物进入世界生态

很多模组作物“可种可吃”,但“世界不认识它”。

21.1 村民耕作兼容思路

农民村民行为会对耕地、作物成熟状态进行判断。

如果你希望村民参与你的作物循环,需要考虑:

  • 你的耕地是否在相关标签内

  • 你的作物是否满足可识别条件

最小兼容建议:

  • 自定义耕地方块加入 villager_farmlands 语义标签体系

  • 作物行为与 CropBlock 语义保持一致

21.2 蜜蜂授粉加速兼容思路

蜜蜂能促进部分可生长植物,作物是否可被识别取决于类型与标签语义。

建议:

  • 尽量继承标准作物基类

  • 使用标准年龄属性语义

  • 在数据层补足相关标签

21.3 骨粉事件扩展

BoneMealItem 在应用前会触发事件钩子(NeoForge)。

你可以基于此做:

  • 特定维度禁用骨粉

  • 作物等级系统(等级不足骨粉无效)

  • 骨粉替代品(魔法粉末)

21.4 不要把兼容逻辑硬写在作物类里

推荐分层:

  • 作物类只负责“自身能否长、如何长”

  • 兼容逻辑放事件层 / 数据层

这样后期加新作物不用复制一堆兼容代码。


22. 数据驱动进阶:把农业参数搬到数据层

22.1 为什么要“参数外置”

当你作物超过 5 种,硬编码会迅速失控。

把以下参数外置:

  • 生长倍率

  • 骨粉倍率

  • 产量分布

  • 堆肥概率

收益:

  • 热修复平衡快

  • 整合包作者可覆盖

  • 兼容层可做按包配置

22.2 食物参数表模式(示例)

可定义一个“食物平衡表”并在注册时读取:

public record FoodBalance(int nutrition, float saturation, boolean alwaysEdible, float eatSeconds) {}

public final class FoodBalanceTable {
    public static final Map<String, FoodBalance> TABLE = Map.of(
        "blueberry", new FoodBalance(2, 0.2F, false, 1.6F),
        "blueberry_jam", new FoodBalance(5, 0.4F, true, 1.6F),
        "adrenaline_candy", new FoodBalance(2, 0.1F, true, 0.8F)
    );
}

你可以进一步把它做成 datapack JSON + codec 读取。

22.3 作物参数表模式(示例)

public record CropBalance(int maxAge, float growthMultiplier, int bonemealMin, int bonemealMax) {}

public final class CropBalanceTable {
    public static final Map<String, CropBalance> TABLE = Map.of(
        "blueberry", new CropBalance(7, 1.0F, 1, 2),
        "herb", new CropBalance(3, 0.6F, 1, 1),
        "spice", new CropBalance(5, 0.8F, 1, 2)
    );
}

22.4 Datamap 与 Tag 的分工

  • Tag:集合语义(属于/不属于)

  • Datamap:带数值或结构配置

举例:

  • “可堆肥物品集合”可用 tag;

  • “可堆肥且概率是多少”更适合 datamap。

22.5 堆肥系统可拓展实践

你可以为自己模组增加类似“肥料值”数据图:

  • 输入:某些有机物

  • 输出:肥力增益值

并在作物生长时读取周边土壤肥力组件。


23. 测试方法:如何验证食物与作物系统

23.1 手动测试不足以保证稳定

手动测试只能覆盖“你记得测的路径”。

农业系统通常有大量边界:

  • 光照临界值

  • 骨粉概率

  • 掉落分布

  • 联机同步

建议建立“脚本化测试 + 手测验证”双轨。

23.2 建立测试矩阵

至少覆盖以下维度:

  1. 年龄:0、半熟、满熟

  2. 环境:光照足 / 不足

  3. 交互:手动采收 / 骨粉 / 自然生长

  4. 工具:普通手 / 带时运工具

  5. 模式:生存 / 创造 / 联机

23.3 食物测试案例模板

Case: blueberry_jam
- 前置: 饱食值 20, 生命值受损
- 操作: 食用一次
- 断言:
  1) 可在满饱食时食用
  2) 恢复值符合配置
  3) 有概率触发再生效果
  4) 消耗后返还玻璃瓶

23.4 作物测试案例模板

Case: blueberry_crop growth
- 前置: age=0, farmland, skylight>=9
- 操作: 跑随机刻 2000 tick
- 断言:
  1) age 最终应 > 0
  2) 不能超过 maxAge
  3) 满熟后 randomTick 不再增长

23.5 掉落分布测试建议

如果你做概率掉落,建议跑批量统计:

  • 1000 次采收

  • 统计均值与方差

  • 判断是否偏离设计目标

伪代码:

int berries = 0;
int seeds = 0;
for (int i = 0; i < 1000; i++) {
    DropResult r = simulateHarvest(age7, fortune2);
    berries += r.berries();
    seeds += r.seeds();
}
System.out.println("avg berries=" + berries / 1000.0);
System.out.println("avg seeds=" + seeds / 1000.0);

23.6 联机一致性测试

重点看:

  • 服务器主导生长

  • 客户端仅表现

  • 食用后效果同步

  • 战利品掉落由服务端生成

出现“单机正常,联机异常”,通常是你把核心逻辑写在客户端分支。


24. 常见坑位深挖(高级排错)

24.1 AGE 属性不一致

典型问题:

  • 代码用 AGE_3

  • blockstates 仍写 age=0..7

结果:模型映射错乱或直接缺失。

解决:

  • 统一年龄语义来源

  • 生成资源时由同一个常量驱动

24.2 自定义作物忘记 MapCodec

新版本方块大量依赖 codec 体系。

你应保证:

  • CODEC 常量存在

  • codec() 正确返回

否则部分序列化/注册场景可能出问题。

24.3 randomTicks 忘开

即使你写了 randomTick,没有 properties.randomTicks() 也不会自然生长。

24.4 骨粉粒子和实际行为不一致

你可能会看到粒子但没长,或长了没粒子。

排查:

  • isValidBonemealTarget

  • isBonemealSuccess

  • performBonemeal

  • 粒子仅视觉,不代表逻辑成功

24.5 掉落文件路径写错

常见错误:

  • loot_tables 和 loot_table 混用

  • namespace 写错

  • 文件名与 block id 不一致

建议:

  • DataGen 统一输出,不手写关键路径

24.6 super 调用遗漏

重写 finishUsingItem 或 randomTick 时漏掉 super,会丢失基础逻辑。

经验规则:

  • 如果你是“增强”,先 super 后追加。

  • 如果你是“完全重写”,确认你把默认行为完整重建。

24.7 逻辑写在错误侧

强规则:

  • 世界状态变更在服务端

  • 客户端只做 UI / 粒子 / 渲染

24.8 概率体感不对

玩家对概率感知并不线性。

例如 5% 稀有掉落,如果作物采收频率高,玩家会觉得“太常见”。

需要基于“每小时行为次数”校准,而非只看单次概率。

24.9 与其他模组冲突

农业类冲突常见在:

  • 同 ID 覆盖

  • 标签冲突

  • 战利品注入冲突

建议:

  • 不改原版表时尽量“追加”而非“替换”

  • 对外暴露配置让整合包可调


25. 工程化落地:从 1 种作物扩展到 30 种

25.1 命名与目录规范(强烈建议)

block/crop/
  BlueberryCropBlock.java
  HerbCropBlock.java
  SpiceCropBlock.java

item/food/
  ModFoods.java
  food items...

data/loot/
  ModBlockLoot.java

resources/assets/modid/blockstates/crop/
resources/assets/modid/models/block/crop/
resources/assets/modid/textures/block/crop/

按“作物域”组织,而不是按文件类型散落全局。

25.2 批量注册策略

把重复逻辑抽成注册函数:

private static DeferredBlock<Block> registerCrop(String id, Supplier<? extends Block> supplier) {
    return BLOCKS.register(id, supplier);
}

private static DeferredItem<Item> registerSeed(String id) {
    return ITEMS.registerSimpleItem(id + "_seeds", new Item.Properties());
}

25.3 资源生成策略

  • 可写脚本批量生成 blockstates 与 stage 模型

  • 贴图可先占位,后替换

  • 先保证命名与路径一致,再做美术优化

25.4 “先通后优”迭代节奏

阶段 1:功能全通

  • 能种

  • 能长

  • 能收

  • 能吃

阶段 2:平衡校准

  • 调生长速度

  • 调掉落

  • 调食物收益

阶段 3:表现增强

  • 粒子

  • 音效

  • UI 提示

阶段 4:生态兼容

  • 村民

  • 蜜蜂

  • 堆肥/标签

25.5 文档驱动开发

每新增一种作物,填写同一模板:

作物ID:
生长阶段:
生长条件:
骨粉行为:
成熟掉落:
副产物:
食物用途:
堆肥概率:

这会极大提升团队协作效率。


26. FAQ(扩展版)

Q1:我能让食物根据玩家状态动态变化吗?

可以。可通过 getFoodProperties(stack, entity) 返回动态值。

Q2:我能做“吃下去后延迟生效”吗?

可以。finishUsingItem 里记录状态,再用 tick/能力系统延迟执行。

Q3:作物一定要继承 CropBlock 吗?

不一定,但继承可获得大量现成功能。完全自定义适合非常特殊植物。

Q4:为什么我改了 loot JSON 没效果?

通常是路径或命名空间问题,或缓存未刷新。优先用 DataGen 保证正确输出。

Q5:我想让骨粉在不同维度效果不同怎么办?

可以在 isBonemealSuccess / performBonemeal 做维度判定,或监听骨粉事件层统一处理。

Q6:如何避免“一个食物统治全版本”问题?

使用分层设计:主粮、功能、代价型高收益食物,并结合获取难度。

Q7:双格作物值得做吗?

值得。它能显著提升视觉辨识度与成长反馈,但实现复杂度更高。

Q8:我需要一开始就做数据驱动吗?

建议从第一个可用版本就开始。后迁移成本通常高于前期多花的 10% 时间。


27. 最终方法论清单(可打印)

每次开发一个新作物/食物前,按这 12 条过一遍:

  1. 我是否先定义了玩法目标?

  2. 我是否画出了状态机?

  3. 我是否明确了成熟条件与成长条件?

  4. 我是否区分了自然生长与骨粉生长?

  5. 我是否定义了未成熟与成熟掉落差异?

  6. 我是否明确了食物层级定位(主粮/功能/代价)?

  7. 我是否把可调参数外置?

  8. 我是否考虑了村民/蜜蜂/堆肥兼容?

  9. 我是否做了联机侧验证?

  10. 我是否做了概率分布统计?

  11. 我是否遵循命名和目录规范?

  12. 我是否写了最小文档说明?

只要这 12 条稳定执行,你的农业系统会非常稳。


结语:从“会写一个作物”到“会做系统”

真正的进步不是你会不会写:

new FoodProperties.Builder().nutrition(6).saturationModifier(0.6F).build();

而是你能否回答:

  1. 这个食物在玩法生态中处于什么层级?

  2. 它和种植周期、产量、战斗节奏是否匹配?

  3. 它是否可配置、可测试、可扩展?

请持续使用本文的方法论循环:

  • 先画状态机

  • 再映射源码钩子

  • 先做最小纵切

  • 最后数据驱动扩展

当你按这个节奏推进时,食物和作物就不再是“几个类 + 几个 JSON”, 而会变成一个可迭代、可平衡、可维护的农业系统。


附加阅读建议(同目录)

为继续提升,建议配合阅读:

  • NEOFORGE_ITEM_TUTORIAL.md

  • NEOFORGE_RESOURCE_TUTORIAL.md

  • NEOFORGE_DATA_STORAGE_TUTORIAL.md

这样你可以把“食物与作物”进一步升级为:

  • 可持久化农业数据

  • 可同步多人状态

  • 可配置成长曲线与生态系统

On this page

目录1. 前置说明:你将学到什么2. 方法论总览:先建模型,再写代码2.1 新手常见失败模式2.2 推荐方法论(四步循环)第一步:画出“状态机”第二步:映射到源码钩子第三步:先做“最小可用纵切”第四步:数据驱动优先3. 源码地图:食物与作物相关核心类3.1 食物核心3.2 作物核心3.3 战利品与数据生成核心3.4 数据驱动相关4. 食物系统底层原理(必读)4.1 食物不是 isEdible,而是数据组件4.2 FoodProperties 字段语义(底层)4.3 食用调用链链路概览关键结算点4.4 FoodData 的饥饿-饱和-消耗机制4.5 效果系统概率执行细节4.6 IItemExtension.getFoodProperties:栈敏感扩展点5. 从 0 写第一个自定义食物5.1 目标5.2 注册代码(示例)5.3 底层对照(你要知道发生了什么)5.4 常见错误6. 食物特殊效果设计与实现6.1 FoodProperties.Builder 常用能力6.2 示例:可概率中毒的“可疑果实”6.3 示例:战斗型“肾上腺糖”6.4 示例:吃完返还容器6.5 示例:继承 Item 做后处理6.6 何时用 FoodProperties.effect,何时重写 finishUsingItem7. 食物进阶:容器返还、动态属性、平衡设计7.1 动态食物属性(栈敏感)7.2 平衡设计:食物分层法7.3 可测量平衡指标7.4 参考原版参数8. 作物系统底层原理(必读)8.1 CropBlock 的核心结构8.2 生长条件判定8.3 getGrowthSpeed 真正决定了农田效率8.4 生存性判定 canSurvive8.5 骨粉系统接口模型8.6 为什么你的作物可能“能种但不长”9. 从 0 写第一个自定义作物方块9.1 设计目标9.2 自定义方块类9.3 注册方块与方块物品9.4 作物资源文件(最小集)10. 作物生长控制:随机刻、生长速度、骨粉10.1 调整自然生长速度10.2 调整骨粉增量10.3 自定义骨粉成功率10.4 自定义“可施肥目标”10.5 更高级:重写 growCrops10.6 参考特化实现11. 作物战利品表:从逻辑到掉落11.1 原版作物掉落模式11.2 createCropDrops 的真实语义11.3 自定义作物 loot(DataGen 方式)11.4 手写 JSON 等价实现(示意)11.5 进阶:按年龄分层掉落12. 数据生成(DataGen)与手写 JSON 的取舍12.1 你需要理解 LootTableProvider 在做什么12.2 子提供器机制12.3 原版如何装配12.4 推荐工程实践13. 相关数据驱动内容:堆肥与耕地标签13.1 堆肥支持(Compostables)13.2 村民耕地兼容13.3 NeoForgeDataMapsProvider 给你的启示14. 完整实战:食物 + 作物 + 战利品一体化模板14.1 结构建议14.2 食物注册(综合)14.3 作物方块(综合)14.4 战利品生成(综合)14.5 这个模板为什么稳15. 调试、测试与排错清单15.1 食物问题排查现象:右键无反应现象:能吃但不加饱食现象:效果不触发现象:容器不返还15.2 作物问题排查现象:能种但不长现象:骨粉无效现象:作物被破坏后掉落异常15.3 战利品表问题排查现象:总是掉种子不掉果实现象:DataGen 生成成功但游戏内不生效15.4 行为一致性测试清单(建议逐条过)16. 设计方法论:如何扩展到“农业系统”模组16.1 三层架构法16.2 扩展路线建议(按收益排序)路线 A:单作物变多作物路线 B:引入季节 / 温度 / 湿度路线 C:食物链联动16.3 平衡性方法论(很重要)指标化反通胀16.4 可维护性规范17. 附录 A:关键源码索引(建议收藏)17.1 食物相关17.2 作物相关17.3 战利品相关17.4 数据驱动相关18. 附录 B:可复用模板代码18.1 食物参数工厂模板18.2 通用作物基类模板18.3 Loot 条件构建模板18.4 多作物批量注册思路(示意)18.5 骨粉粒子类型理解19. 特殊作物形态深挖:不只是 CropBlock19.1 标准单格作物范式19.2 低年龄上限范式(甜菜式)19.3 成熟转化范式(火把花式)19.4 瓜藤结果范式(Stem + Fruit)20. 战利品设计深入:从“能掉”到“可平衡”20.1 先定义掉落目标而不是先写 JSON20.2 createCropDrops 的优势与边界20.3 自定义多池掉落模板20.4 结果型作物的掉落分工20.5 双格作物的掉落一致性20.6 掉落平衡的四个旋钮21. 生物与 AI 交互:让你的作物进入世界生态21.1 村民耕作兼容思路21.2 蜜蜂授粉加速兼容思路21.3 骨粉事件扩展21.4 不要把兼容逻辑硬写在作物类里22. 数据驱动进阶:把农业参数搬到数据层22.1 为什么要“参数外置”22.2 食物参数表模式(示例)22.3 作物参数表模式(示例)22.4 Datamap 与 Tag 的分工22.5 堆肥系统可拓展实践23. 测试方法:如何验证食物与作物系统23.1 手动测试不足以保证稳定23.2 建立测试矩阵23.3 食物测试案例模板23.4 作物测试案例模板23.5 掉落分布测试建议23.6 联机一致性测试24. 常见坑位深挖(高级排错)24.1 AGE 属性不一致24.2 自定义作物忘记 MapCodec24.3 randomTicks 忘开24.4 骨粉粒子和实际行为不一致24.5 掉落文件路径写错24.6 super 调用遗漏24.7 逻辑写在错误侧24.8 概率体感不对24.9 与其他模组冲突25. 工程化落地:从 1 种作物扩展到 30 种25.1 命名与目录规范(强烈建议)25.2 批量注册策略25.3 资源生成策略25.4 “先通后优”迭代节奏25.5 文档驱动开发26. FAQ(扩展版)Q1:我能让食物根据玩家状态动态变化吗?Q2:我能做“吃下去后延迟生效”吗?Q3:作物一定要继承 CropBlock 吗?Q4:为什么我改了 loot JSON 没效果?Q5:我想让骨粉在不同维度效果不同怎么办?Q6:如何避免“一个食物统治全版本”问题?Q7:双格作物值得做吗?Q8:我需要一开始就做数据驱动吗?27. 最终方法论清单(可打印)结语:从“会写一个作物”到“会做系统”附加阅读建议(同目录)