NeoForge 食物与作物开发完全指南
重点:食物、食物效果、作物方块、生长与骨粉、战利品表、数据驱动。
目录
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 食用调用链
链路概览
-
玩家右键物品,走
Item.use() -
取
FoodProperties:itemstack.getFoodProperties(player) -
可吃则
startUsingItem -
结束使用时调用
Item.finishUsingItem() -
委托到
livingEntity.eat(level, stack, foodProperties) -
若实体是玩家,走覆盖的
Player.eat() -
更新
FoodData.eat(foodProperties) -
触发统计、成就、音效、容器返还
关键结算点
在 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 常见错误
-
只注册物品没配模型 -> 紫黑方块
-
翻译键写错 -> 显示 key 本身
-
忘记加入创造栏 -> 测试找不到
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 平衡设计:食物分层法
建议把食物分三层:
-
基础口粮:主世界日常,性价比稳定。
-
战斗补给:alwaysEdible + 快速食用 + 短时 Buff。
-
代价型强食物:高收益 + 副作用(如中毒、虚弱)。
这样比“所有食物都比牛排强”更健康。
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() 做了三件事:
-
收集所有子提供器生成的表
-
校验(validation)
-
输出稳定 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.java14.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 作物问题排查
现象:能种但不长
检查顺序:
-
方块属性是否有
.randomTicks() -
光照是否满足(>=9)
-
isRandomlyTicking是否正确 -
canSurvive是否误判 -
土壤是否可 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 三层架构法
把农业系统拆三层:
-
行为层:
Item/Block/AI 交互 -
数据层:
loot/datamap/tag -
表现层:模型、贴图、音效、粒子
规则:
-
行为层尽量薄,逻辑参数放数据层。
-
表现层绝不反向依赖行为层。
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 的单格植物”。
实际上,原版已经提供了多种形态范式:
-
单格标准作物(小麦、胡萝卜、土豆)
-
低年龄上限作物(甜菜)
-
成熟后转化为另一方块(火把花)
-
结果型作物(瓜藤 + 果实方块)
如果你掌握这五种范式,农业模组的表达空间会大很多。
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
建议先写“掉落需求表”:
| 状态 | 主产物 | 种子 | 特殊掉落 | 附魔影响 |
|---|---|---|---|---|
| 未成熟 | 0 | 1 | 无 | 无 |
| 成熟 | 1~2 | 1~3 | 5% 副产物 | 时运加种子 |
先有表,再落地 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 掉落平衡的四个旋钮
-
成熟门槛(年龄上限)
-
自然生长速度
-
每次收获平均产量
-
副产物概率
不要只调其中一个。
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 建立测试矩阵
至少覆盖以下维度:
-
年龄:0、半熟、满熟
-
环境:光照足 / 不足
-
交互:手动采收 / 骨粉 / 自然生长
-
工具:普通手 / 带时运工具
-
模式:生存 / 创造 / 联机
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 条过一遍:
-
我是否先定义了玩法目标?
-
我是否画出了状态机?
-
我是否明确了成熟条件与成长条件?
-
我是否区分了自然生长与骨粉生长?
-
我是否定义了未成熟与成熟掉落差异?
-
我是否明确了食物层级定位(主粮/功能/代价)?
-
我是否把可调参数外置?
-
我是否考虑了村民/蜜蜂/堆肥兼容?
-
我是否做了联机侧验证?
-
我是否做了概率分布统计?
-
我是否遵循命名和目录规范?
-
我是否写了最小文档说明?
只要这 12 条稳定执行,你的农业系统会非常稳。
结语:从“会写一个作物”到“会做系统”
真正的进步不是你会不会写:
new FoodProperties.Builder().nutrition(6).saturationModifier(0.6F).build();而是你能否回答:
-
这个食物在玩法生态中处于什么层级?
-
它和种植周期、产量、战斗节奏是否匹配?
-
它是否可配置、可测试、可扩展?
请持续使用本文的方法论循环:
-
先画状态机
-
再映射源码钩子
-
先做最小纵切
-
最后数据驱动扩展
当你按这个节奏推进时,食物和作物就不再是“几个类 + 几个 JSON”, 而会变成一个可迭代、可平衡、可维护的农业系统。
附加阅读建议(同目录)
为继续提升,建议配合阅读:
-
NEOFORGE_ITEM_TUTORIAL.md -
NEOFORGE_RESOURCE_TUTORIAL.md -
NEOFORGE_DATA_STORAGE_TUTORIAL.md
这样你可以把“食物与作物”进一步升级为:
-
可持久化农业数据
-
可同步多人状态
-
可配置成长曲线与生态系统