agentsclimarketplace

Nms custom entity

Skill MrPippi/MJP-Claude-Skills/Skills/nms/nms-custom-entity

建立自定義 NMS 實體:繼承現有 Mob 類別、自訂 PathfinderGoal、替換 vanilla 實體行為 / Create custom NMS entities with custom PathfinderGoal AIFrom its SKILL.md

Install
npx -y skills add MrPippi/MJP-Claude-Skills --skill nms-custom-entity

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

7.7 KB, ~2.2k tokens by cl100k_base, as published. Nobody here has run it

NMS Custom Entity / NMS 自定義實體

技能名稱 / Skill Name

nms-custom-entity

目的 / Purpose

透過繼承 NMS Mob 類別並覆寫 registerGoals() 加入自訂 PathfinderGoal,實現自定義 AI 行為。適用於客製化 Boss、NPC、守衛等場景。

NMS 版本需求 / NMS Version Requirements

  • Paper 1.21 – 1.21.3
  • Paperweight userdev 1.7.2+
  • 須使用 paper-plugin.yml(確保早於 Bukkit plugin 載入)

觸發條件 / Triggers

  • 「自定義實體」「custom entity」「NMS AI」
  • 「PathfinderGoal」「自訂 mob」「custom mob」
  • 「entity goal」「custom zombie」「replace entity」

輸入參數 / Inputs

參數範例說明
package_namecom.example.entities產出類別所在 package
entity_class_nameCustomZombie自定義實體類名
base_entityZombie繼承的 NMS 基類
entity_idmy_custom_zombie註冊用 ID(namespaced)
goal_class_nameFollowClosestPlayerGoal自訂 goal 類名

輸出產物 / Outputs

  • CustomZombie.java — 繼承 net.minecraft.world.entity.monster.Zombie 的自定義實體
  • FollowClosestPlayerGoal.java — 自訂 PathfinderGoal 範本
  • EntitySpawner.java — 生成工具類
  • EntityListener.java(選)— 攔截 vanilla spawn 替換為自定義實體

Paperweight 建置設定 / Build Setup

參見 Skills/paper-nms/PLATFORM.md

代碼範本 / Code Template

CustomZombie.java

package com.example.entities;

import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.ai.goal.FloatGoal;
import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal;
import net.minecraft.world.entity.ai.goal.MeleeAttackGoal;
import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal;
import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal;
import net.minecraft.world.entity.monster.Zombie;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;

@SuppressWarnings("UnstableApiUsage")
public class CustomZombie extends Zombie {

    public CustomZombie(EntityType<? extends Zombie> type, Level level) {
        super(type, level);
    }

    @Override
    protected void registerGoals() {
        // 游泳(必備)
        this.goalSelector.addGoal(0, new FloatGoal(this));

        // 自訂:鎖定最近的玩家
        this.goalSelector.addGoal(1, new FollowClosestPlayerGoal(this, 1.2D, 32.0D));

        // 近戰攻擊(速度倍率 1.0,追擊時不鬆手)
        this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0D, false));

        // 隨機觀察周圍
        this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
        this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));

        // 目標選擇器:玩家優先
        this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Player.class, true));
    }

    /** 自訂屬性:更高的血量與攻擊。 */
    public static net.minecraft.world.entity.ai.attributes.AttributeSupplier.Builder createAttributes() {
        return Zombie.createAttributes()
            .add(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH, 40.0D)
            .add(net.minecraft.world.entity.ai.attributes.Attributes.ATTACK_DAMAGE, 8.0D)
            .add(net.minecraft.world.entity.ai.attributes.Attributes.MOVEMENT_SPEED, 0.3D);
    }
}

FollowClosestPlayerGoal.java

package com.example.entities;

import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.player.Player;

import java.util.EnumSet;

@SuppressWarnings("UnstableApiUsage")
public class FollowClosestPlayerGoal extends Goal {

    private final Mob mob;
    private final double speed;
    private final double range;
    private Player target;

    public FollowClosestPlayerGoal(Mob mob, double speed, double range) {
        this.mob = mob;
        this.speed = speed;
        this.range = range;
        this.setFlags(EnumSet.of(Flag.MOVE));
    }

    @Override
    public boolean canUse() {
        this.target = this.mob.level().getNearestPlayer(this.mob, this.range);
        return this.target != null && !this.target.isCreative() && !this.target.isSpectator();
    }

    @Override
    public boolean canContinueToUse() {
        return this.target != null
            && this.target.isAlive()
            && this.mob.distanceToSqr(this.target) < this.range * this.range;
    }

    @Override
    public void start() {
        this.mob.getNavigation().moveTo(this.target, this.speed);
    }

    @Override
    public void stop() {
        this.target = null;
        this.mob.getNavigation().stop();
    }

    @Override
    public void tick() {
        if (this.target == null) return;
        this.mob.getLookControl().setLookAt(this.target, 30.0F, 30.0F);
        // 每 10 tick 重新規劃路徑
        if (this.mob.tickCount % 10 == 0) {
            this.mob.getNavigation().moveTo(this.target, this.speed);
        }
    }
}

EntitySpawner.java

package com.example.entities;

import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.MobSpawnType;
import org.bukkit.Location;
import org.bukkit.craftbukkit.v1_21_R1.CraftWorld;

@SuppressWarnings("UnstableApiUsage")
public final class EntitySpawner {

    private EntitySpawner() {}

    /**
     * 在指定位置生成自定義殭屍。必須在主執行緒呼叫。
     */
    public static org.bukkit.entity.Entity spawnCustomZombie(Location loc) {
        ServerLevel level = ((CraftWorld) loc.getWorld()).getHandle();

        CustomZombie zombie = new CustomZombie(EntityType.ZOMBIE, level);
        zombie.moveTo(loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch());

        // 設定自訂名稱(顯示在頭上)
        zombie.setCustomName(net.minecraft.network.chat.Component.literal("§c自訂殭屍"));
        zombie.setCustomNameVisible(true);

        level.addFreshEntity(zombie, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CUSTOM);
        return zombie.getBukkitEntity();
    }
}

推薦目錄結構 / Recommended Directory Structure

src/main/java/com/example/
├── MyNmsPlugin.java
└── entities/
    ├── CustomZombie.java
    ├── FollowClosestPlayerGoal.java
    ├── EntitySpawner.java
    └── EntityListener.java

執行緒安全注意事項 / Thread Safety

  • ⚠️ 實體建立、addFreshEntity()moveTo() 必須在主執行緒
  • ⚠️ PathfinderGoal.tick() 由 NMS 在主執行緒的 tick 迴圈呼叫,勿做耗時操作
  • ⚠️ 存取 mob.level() 時確認 chunk 已載入
  • ✅ 可在 async 預計算路徑資料,但 Navigation.moveTo() 必須在主執行緒呼叫
  • 詳見 Skills/_shared/nms-threading.md

失敗回退 / Fallback

錯誤原因解法
實體不動Goal.canUse() 未回傳 true加 log 檢查 target 是否被過濾掉
Chunk 卸載後實體消失未標記 persistentzombie.setPersistenceRequired()
屬性無效未呼叫 createAttributes() 註冊Paper 自定義實體需透過 EntityType.Builder 註冊(或繼承現有 Type)
客戶端顯示空白實體Entity type 不存在於客戶端繼承 vanilla 類型(如 Zombie)而非建立新 EntityType
UnsupportedOperationExceptionregisterGoalsGoal 被重複加入清空 goalSelector 再重建:goalSelector.removeAllGoals(g -> true)

What ships with it: 1 file

4.4 KB alongside SKILL.md

Keep looking

Skills are one crate of 326,758. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.