返回

Minecraft Fabric自定义指南针Mod:钻石指南针动画问题详解

java

Minecraft Fabric Mod 自定义指南针动画问题解决

一、问题

我做了一个 Minecraft Fabric Mod,想让一个自定义的指南针指向最近的钻石矿。虽然日志里能看到已经成功计算出了最近钻石矿的方向,但这个钻石指南针并没有像预期那样转起来。这让我挺困惑的,不清楚问题出在哪里。

二、问题原因分析

指南针动画不工作,大概率是因为客户端没有正确获取到更新的角度信息,或者模型没有正确地根据角度进行渲染。Minecraft 的物品动画,特别是指南针,依赖于一个名为 "angle" 的模型谓词(Model Predicate)。这个谓词的值需要在客户端侧进行计算和更新。

主要可能有以下几个方面的问题:

  1. 模型谓词注册问题: ModelPredicateProviderRegistry 是否正确注册,且注册的 Identifier 是否和模型文件中的一致?
  2. 角度计算逻辑错误: calculateAngle 函数计算出的角度是否正确?是否符合 Minecraft 动画系统的要求(0.0 到 1.0 之间)?
  3. 客户端/服务端逻辑混淆: 模型谓词的更新必须在客户端进行。一些计算是否放到了服务端,导致客户端没有接收到正确的数据?
  4. 资源文件错误 检查 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 不需要调整.