Minecraft Fabric自定义指南针Mod:钻石指南针动画问题详解
2025-03-18 01:11:39
Minecraft Fabric Mod 自定义指南针动画问题解决
一、问题
我做了一个 Minecraft Fabric Mod,想让一个自定义的指南针指向最近的钻石矿。虽然日志里能看到已经成功计算出了最近钻石矿的方向,但这个钻石指南针并没有像预期那样转起来。这让我挺困惑的,不清楚问题出在哪里。
二、问题原因分析
指南针动画不工作,大概率是因为客户端没有正确获取到更新的角度信息,或者模型没有正确地根据角度进行渲染。Minecraft 的物品动画,特别是指南针,依赖于一个名为 "angle" 的模型谓词(Model Predicate)。这个谓词的值需要在客户端侧进行计算和更新。
主要可能有以下几个方面的问题:
- 模型谓词注册问题:
ModelPredicateProviderRegistry
是否正确注册,且注册的 Identifier 是否和模型文件中的一致? - 角度计算逻辑错误:
calculateAngle
函数计算出的角度是否正确?是否符合 Minecraft 动画系统的要求(0.0 到 1.0 之间)? - 客户端/服务端逻辑混淆: 模型谓词的更新必须在客户端进行。一些计算是否放到了服务端,导致客户端没有接收到正确的数据?
- 资源文件错误 检查
diamond_compass.json
是否被放置在了resources/assets/ferndale/models/item
目录下。 还要注意这个 json 文件的复写 ("overrides") 定义正确.
三、解决方案
针对以上可能的原因,逐一排查和解决:
1. 确认模型谓词注册
首先,我们看下 FerndaleClientMod.java
里的这段代码:
ModelPredicateProviderRegistry.register(FerndaleMod.DIAMOND_COMPASS, new Identifier("angle"),
(stack, world, entity, seed) -> {
if (world == null || entity == null) {
return 0.0f;
}
if (entity instanceof PlayerEntity player) {
float angle = DiamondCompassTracker.calculateAngle(stack, world, player);
System.out.println("[🔧 DEBUG] Compass Angle: " + angle);
return angle;
}
return 0.0f;
});
这段代码看起来没啥大问题, 它在客户端注册了一个名为 "angle" 的模型谓词, 并尝试计算角度。关键在于 new Identifier("angle")
, 这个 Identifier 必须和你模型文件 (diamond_compass.json
) 中使用的 predicate
里的 angle
对应上。 另外, 注意要注册的是 FerndaleMod.DIAMOND_COMPASS
, 而不是其他变量或硬编码字符串.
检查你的资源文件,assets/ferndale/models/item/diamond_compass.json
:
{
"parent": "item/handheld",
"textures": {
"layer0": "ferndale:item/diamond_compass"
},
"overrides": [
{ "predicate": { "angle": 0.000000 }, "model": "item/compass_16" },
{ "predicate": { "angle": 0.062500 }, "model": "item/compass_17" },
{ "predicate": { "angle": 0.125000 }, "model": "item/compass_18" },
{ "predicate": { "angle": 0.187500 }, "model": "item/compass_19" },
{ "predicate": { "angle": 0.250000 }, "model": "item/compass_20" },
{ "predicate": { "angle": 0.312500 }, "model": "item/compass_21" },
{ "predicate": { "angle": 0.375000 }, "model": "item/compass_22" },
{ "predicate": { "angle": 0.437500 }, "model": "item/compass_23" },
{ "predicate": { "angle": 0.500000 }, "model": "item/compass_24" },
{ "predicate": { "angle": 0.562500 }, "model": "item/compass_25" },
{ "predicate": { "angle": 0.625000 }, "model": "item/compass_26" },
{ "predicate": { "angle": 0.687500 }, "model": "item/compass_27" },
{ "predicate": { "angle": 0.750000 }, "model": "item/compass_28" },
{ "predicate": { "angle": 0.812500 }, "model": "item/compass_29" },
{ "predicate": { "angle": 0.875000 }, "model": "item/compass_30" },
{ "predicate": { "angle": 0.937500 }, "model": "item/compass_31" },
{ "predicate": { "angle": 1.000000 }, "model": "item/compass_16" }
]
}
这里定义了当 "angle" 谓词满足什么条件时, 使用哪个模型. 原始代码这里肯定是不对的,缺少一系列针对angle
变化时指向的模型变化文件。你必须要有和原版指南针一样的覆盖(override)列表, 以便模型可以随着 "angle" 的变化而改变。
记住: textures/item
里要有一个名叫 diamond_compass.png
的材质。
2. 检查角度计算
再看看 DiamondCompassTracker.java
里的 calculateAngle
函数:
private static float getCompassAngle(PlayerEntity player, BlockPos target) {
Vec3d playerPos = player.getPos();
Vec3d targetPos = new Vec3d(target.getX(), target.getY(), target.getZ());
double angle = Math.atan2(targetPos.getZ() - playerPos.getZ(), targetPos.getX() - playerPos.getX());
// Normalize angle to be between 0.0 and 1.0 (used by Minecraft's model system)
return (float) ((angle / (2 * Math.PI) + 1) % 1);
}
这个函数先计算玩家和目标方块之间的角度(使用 Math.atan2
),然后将角度标准化到 0.0 到 1.0 之间。 这个标准化非常重要,因为 Minecraft 的模型系统就认这个范围的值!
原始代码是有问题的, 如果(angle / (2 * Math.PI)
的结果恰好是负数, 加一后还是小于零. 再取模 1, 就会得到接近 1 的结果,而不是接近零的结果. 这种情况下指南针就会转过头。 我们可以修改代码,先加一个2 * PI,然后再取模.
private static float getCompassAngle(PlayerEntity player, BlockPos target) {
Vec3d playerPos = player.getPos();
Vec3d targetPos = new Vec3d(target.getX(), target.getY(), target.getZ());
double angle = Math.atan2(targetPos.getZ() - playerPos.getZ(), targetPos.getX() - playerPos.getX());
// Normalize angle to be between 0.0 and 1.0 (used by Minecraft's model system)
return (float) (((angle / (2 * Math.PI)) + 1 + 1) % 1);
}
或者可以导入MathHelper
:
import net.minecraft.util.math.MathHelper;
...
return MathHelper.wrapDegrees((float) Math.toDegrees(angle) / 360.0f);
3. 避免客户端/服务端逻辑混淆
FerndaleClientMod
只在客户端初始化的时候被调用。而角度的计算应该在每一次渲染帧都进行,以保证指南针能实时更新。我们提供的代码把计算放在客户端是对的, 不需要改。
4. 完整代码示例与调整
结合上面的分析,下面是修改后的完整代码:
FerndaleMod.java (无需修改)
package com.ferndale;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents;
import net.minecraft.item.Item;
import net.minecraft.item.ItemGroups;
import net.minecraft.item.ItemStack;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.util.Identifier;
public class FerndaleMod implements ModInitializer {
public static final String MOD_ID = "ferndale";
// Register the Diamond Compass item
public static final Item DIAMOND_COMPASS = new Item(new Item.Settings());
@Override
public void onInitialize() {
System.out.println("[🔧 DEBUG] Ferndale Mod Loaded!");
// Register the Diamond Compass in the game
Registry.register(Registries.ITEM, new Identifier(MOD_ID, "diamond_compass"), DIAMOND_COMPASS);
// Add to the Tools & Utilities Creative Tab
ItemGroupEvents.modifyEntriesEvent(ItemGroups.TOOLS).register(entries -> {
entries.add(new ItemStack(DIAMOND_COMPASS));
});
}
}
DiamondCompassItem.java (无需修改)
package com.ferndale;
import net.minecraft.client.item.TooltipContext;
import net.minecraft.item.CompassItem;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.text.Text;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public class DiamondCompassItem extends CompassItem {
public DiamondCompassItem(Settings settings) {
super(settings);
}
public static void setLodestonePos(ItemStack stack, BlockPos pos) {
NbtCompound nbt = stack.getOrCreateNbt();
nbt.putInt("LodestoneX", pos.getX());
nbt.putInt("LodestoneY", pos.getY());
nbt.putInt("LodestoneZ", pos.getZ());
}
@Override
public void appendTooltip(ItemStack stack, @Nullable World world, List<Text> tooltip, TooltipContext context) {
if (stack.hasNbt() && stack.getNbt() != null) {
NbtCompound nbt = stack.getNbt();
if (nbt.contains("LodestoneX") && nbt.contains("LodestoneY") && nbt.contains("LodestoneZ")) {
tooltip.add(Text.of("Pointing to: " +
nbt.getInt("LodestoneX") + ", " +
nbt.getInt("LodestoneY") + ", " +
nbt.getInt("LodestoneZ")));
} else {
tooltip.add(Text.of("Not pointing to anything."));
}
}
super.appendTooltip(stack, world, tooltip, context);
}
}
ModItems.java (无需修改)
package com.ferndale;
import net.fabricmc.fabric.api.item.v1.FabricItemSettings;
import net.minecraft.item.CompassItem;
import net.minecraft.item.Item;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.util.Identifier;
public class ModItems {
public static final Item DIAMOND_COMPASS = registerItem("diamond_compass",
new CompassItem(new FabricItemSettings().maxCount(1)));
private static Item registerItem(String name, Item item) {
return Registry.register(Registries.ITEM, new Identifier("ferndale", name), item);
}
public static void registerModItems() {
System.out.println("Registering Mod Items for Ferndale...");
}
}
DiamondCompassTracker.java (修改 getCompassAngle
函数)
package com.ferndale;
import net.minecraft.block.Blocks;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World;
public class DiamondCompassTracker {
public static float calculateAngle(ItemStack stack, World world, PlayerEntity player) {
BlockPos nearestDiamond = findNearestDiamond(player, world);
if (nearestDiamond == null) {
return 0.0f;
}
return getCompassAngle(player, nearestDiamond);
}
private static BlockPos findNearestDiamond(PlayerEntity player, World world) {
BlockPos playerPos = player.getBlockPos();
BlockPos nearestDiamond = null;
double nearestDistance = Double.MAX_VALUE;
int radius = 50; // Search range
for (int x = -radius; x <= radius; x++) {
for (int y = -radius; y <= radius; y++) {
for (int z = -radius; z <= radius; z++) {
BlockPos pos = playerPos.add(x, y, z);
if (world.getBlockState(pos).isOf(Blocks.DIAMOND_ORE)) {
double distance = playerPos.getSquaredDistance(pos);
if (distance < nearestDistance) {
nearestDiamond = pos;
nearestDistance = distance;
}
}
}
}
}
return nearestDiamond;
}
private static float getCompassAngle(PlayerEntity player, BlockPos target) {
Vec3d playerPos = player.getPos();
Vec3d targetPos = new Vec3d(target.getX(), target.getY(), target.getZ());
double angle = Math.atan2(targetPos.getZ() - playerPos.getZ(), targetPos.getX() - playerPos.getX());
// Normalize angle to be between 0.0 and 1.0 (used by Minecraft's model system)
return (float) (((angle / (2 * Math.PI)) + 1 + 1) % 1);
}
}
CompassUtils.java (无需修改)
package com.ferndale;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.util.math.MathHelper;
import net.minecraft.world.World;
public class CompassUtils {
public static float getAngle(ItemStack stack, World world, ClientPlayerEntity player) {
if (!stack.hasNbt()) {
return 0.0F;
}
NbtCompound nbt = stack.getNbt();
if (nbt == null) {
return 0.0F;
}
if (!nbt.contains("LodestoneX") || !nbt.contains("LodestoneY") || !nbt.contains("LodestoneZ")) {
return 0.0F;
}
double lodestoneX = nbt.getInt("LodestoneX") + 0.5;
double lodestoneZ = nbt.getInt("LodestoneZ") + 0.5;
double dx = lodestoneX - player.getX();
double dz = lodestoneZ - player.getZ();
float angle = (float) (MathHelper.atan2(dz, dx) * (180.0 / Math.PI)) - player.getYaw();
return MathHelper.wrapDegrees(angle / 360.0F);
}
}
FerndaleClientMod.java (确认注册了正确的物品)
package com.ferndale;
import com.ferndale.FerndaleMod;
import net.fabricmc.api.ClientModInitializer;
import net.minecraft.client.item.ModelPredicateProviderRegistry;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.util.Identifier;
public class FerndaleClientMod implements ClientModInitializer {
@Override
public void onInitializeClient() {
System.out.println("[🔧 DEBUG] Registering Model Predicate for Diamond Compass");
ModelPredicateProviderRegistry.register(FerndaleMod.DIAMOND_COMPASS, new Identifier("angle"),
(stack, world, entity, seed) -> {
if (world == null || entity == null) {
return 0.0f;
}
if (entity instanceof PlayerEntity player) {
float angle = DiamondCompassTracker.calculateAngle(stack, world, player);
System.out.println("[🔧 DEBUG] Compass Angle: " + angle);
return angle;
}
return 0.0f;
});
}
}
resource: diamond_compass.json (重要,需要完整的复写列表)
{
"parent": "item/handheld",
"textures": {
"layer0": "ferndale:item/diamond_compass"
},
"overrides": [
{ "predicate": { "angle": 0.000000 }, "model": "item/compass_16" },
{ "predicate": { "angle": 0.062500 }, "model": "item/compass_17" },
{ "predicate": { "angle": 0.125000 }, "model": "item/compass_18" },
{ "predicate": { "angle": 0.187500 }, "model": "item/compass_19" },
{ "predicate": { "angle": 0.250000 }, "model": "item/compass_20" },
{ "predicate": { "angle": 0.312500 }, "model": "item/compass_21" },
{ "predicate": { "angle": 0.375000 }, "model": "item/compass_22" },
{ "predicate": { "angle": 0.437500 }, "model": "item/compass_23" },
{ "predicate": { "angle": 0.500000 }, "model": "item/compass_24" },
{ "predicate": { "angle": 0.562500 }, "model": "item/compass_25" },
{ "predicate": { "angle": 0.625000 }, "model": "item/compass_26" },
{ "predicate": { "angle": 0.687500 }, "model": "item/compass_27" },
{ "predicate": { "angle": 0.750000 }, "model": "item/compass_28" },
{ "predicate": { "angle": 0.812500 }, "model": "item/compass_29" },
{ "predicate": { "angle": 0.875000 }, "model": "item/compass_30" },
{ "predicate": { "angle": 0.937500 }, "model": "item/compass_31" },
{ "predicate": { "angle": 1.000000 }, "model": "item/compass_16" }
]
}
###5. 添加材质文件
请把对应的png材质文件,命名为diamond_compass.png
放置到 resources/assets/ferndale/textures/item/
文件夹下.
进阶技巧:平滑过渡
现在指南针应该可以工作了,但可能看起来有点“跳”,不够平滑。这是因为我们直接使用了计算出的角度。可以尝试对角度进行插值,让动画更平滑:
可以在FerndaleClientMod
中增加一个变量来记录上一次的角度,然后在新旧角度之间做插值.
package com.ferndale;
import net.fabricmc.api.ClientModInitializer;
import net.minecraft.client.item.ModelPredicateProviderRegistry;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.MathHelper;
public class FerndaleClientMod implements ClientModInitializer {
private float lastAngle = 0.0f; //记录上次角度.
@Override
public void onInitializeClient() {
ModelPredicateProviderRegistry.register(FerndaleMod.DIAMOND_COMPASS, new Identifier("angle"),
(stack, world, entity, seed) -> {
if (world == null || entity == null) {
return 0.0f;
}
if (entity instanceof PlayerEntity player) {
float targetAngle = DiamondCompassTracker.calculateAngle(stack, world, player);
// 使用线性插值平滑过渡 (可以调整 0.1f 这个系数来控制平滑程度)
float smoothedAngle = MathHelper.lerp(0.1f, lastAngle, targetAngle);
lastAngle = smoothedAngle; // 更新记录的角度.
return smoothedAngle;
}
return 0.0f;
});
}
}
MathHelper.lerp
是 Minecraft 提供的一个线性插值函数。
经过这些修改, 你的钻石指南针应该能够正常工作,并且动画效果会更平滑。 如果还有问题, 仔细检查每一步的代码,特别是 Identifier 是否一致、角度计算是否正确。
fabric_mod.json 不需要调整.