Skills/nms/nms-particle-effect/SKILL.md
透過 ClientboundLevelParticlesPacket 實現進階 NMS 粒子效果:客戶端專屬、大量粒子、自定義參數(Paper NMS + Mojang-mapped)/ Advanced NMS particle effects via ClientboundLevelParticlesPacket with per-client and bulk support
npx skillsauth add MrPippi/MPS nms-particle-effectInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
nms-particle-effect
透過 ClientboundLevelParticlesPacket 直接發送 NMS 粒子封包,實現 Bukkit World.spawnParticle() 無法達到的效果:客戶端專屬粒子(只對特定玩家顯示)、超大量粒子、精確的速度/偏移控制、Item/Block 參數粒子。
| 參數 | 範例 | 說明 |
|------|------|------|
| package_name | com.example.effect | 產出類別所在 package |
| class_name | ParticleEffect | 效果工具類名稱 |
| include_shapes | true | 是否產生預設形狀(圓形、線段、螺旋) |
ParticleEffect.java — 粒子封包發送工具ParticleBuilder.java — 粒子封包 Builder 模式封裝ParticleShapes.java(選)— 預設形狀(circle、line、helix)參見 Skills/paper-nms/PLATFORM.md。關鍵依賴:
dependencies {
paperweight.paperDevBundle('1.21.1-R0.1-SNAPSHOT')
}
ParticleEffect.javapackage com.example.effect;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.core.particles.SimpleParticleType;
import net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket;
import net.minecraft.server.level.ServerPlayer;
import org.bukkit.Location;
import org.bukkit.craftbukkit.v1_21_R1.entity.CraftPlayer;
import org.bukkit.entity.Player;
import java.util.Collection;
@SuppressWarnings("UnstableApiUsage")
public final class ParticleEffect {
private ParticleEffect() {}
/**
* 發送粒子封包給單一玩家(客戶端專屬)。
*
* @param particle NMS ParticleOptions(如 ParticleTypes.FLAME)
* @param loc 粒子位置
* @param count 粒子數量
* @param offsetX/Y/Z 隨機偏移範圍
* @param speed 粒子速度(0 = 不移動)
* @param override true = 強制顯示(無視客戶端粒子設定)
*/
public static void send(Player player, ParticleOptions particle, Location loc,
int count, double offsetX, double offsetY, double offsetZ,
double speed, boolean override) {
ServerPlayer nms = ((CraftPlayer) player).getHandle();
ClientboundLevelParticlesPacket packet = new ClientboundLevelParticlesPacket(
particle, override,
loc.getX(), loc.getY(), loc.getZ(),
(float) offsetX, (float) offsetY, (float) offsetZ,
(float) speed, count
);
nms.connection.send(packet);
}
/** 對一組玩家發送同一粒子封包。 */
public static void sendAll(Collection<? extends Player> players, ParticleOptions particle,
Location loc, int count, double offsetX, double offsetY,
double offsetZ, double speed) {
ClientboundLevelParticlesPacket packet = new ClientboundLevelParticlesPacket(
particle, false,
loc.getX(), loc.getY(), loc.getZ(),
(float) offsetX, (float) offsetY, (float) offsetZ,
(float) speed, count
);
for (Player p : players) {
((CraftPlayer) p).getHandle().connection.send(packet);
}
}
/** 常用快捷方法:在指定位置爆炸粒子。 */
public static void explosion(Player player, Location loc) {
send(player, ParticleTypes.EXPLOSION, loc, 1, 0, 0, 0, 0, true);
}
/** 常用快捷方法:心形粒子。 */
public static void hearts(Player player, Location loc, int count) {
send(player, ParticleTypes.HEART, loc, count, 0.5, 0.5, 0.5, 0, false);
}
/** 常用快捷方法:火焰粒子向上噴。 */
public static void flame(Player player, Location loc, int count) {
send(player, ParticleTypes.FLAME, loc, count, 0.1, 0.3, 0.1, 0.05f, false);
}
}
ParticleBuilder.java(Builder 模式)package com.example.effect;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.core.particles.ParticleTypes;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@SuppressWarnings("UnstableApiUsage")
public class ParticleBuilder {
private ParticleOptions particle = ParticleTypes.FLAME;
private Location location;
private int count = 1;
private double offsetX = 0, offsetY = 0, offsetZ = 0;
private double speed = 0;
private boolean override = false;
private final List<Player> receivers = new ArrayList<>();
public ParticleBuilder particle(ParticleOptions particle) {
this.particle = particle;
return this;
}
public ParticleBuilder at(Location loc) {
this.location = loc;
return this;
}
public ParticleBuilder count(int count) {
this.count = count;
return this;
}
public ParticleBuilder offset(double x, double y, double z) {
this.offsetX = x;
this.offsetY = y;
this.offsetZ = z;
return this;
}
public ParticleBuilder speed(double speed) {
this.speed = speed;
return this;
}
public ParticleBuilder override(boolean override) {
this.override = override;
return this;
}
public ParticleBuilder receivers(Collection<? extends Player> players) {
this.receivers.addAll(players);
return this;
}
public ParticleBuilder receiver(Player player) {
this.receivers.add(player);
return this;
}
public void spawn() {
if (location == null) throw new IllegalStateException("Location not set");
ParticleEffect.sendAll(receivers, particle, location, count, offsetX, offsetY, offsetZ, speed);
}
}
ParticleShapes.java(預設形狀)package com.example.effect;
import net.minecraft.core.particles.ParticleOptions;
import org.bukkit.Location;
import org.bukkit.entity.Player;
@SuppressWarnings("UnstableApiUsage")
public final class ParticleShapes {
private ParticleShapes() {}
/** 在指定位置畫一個水平圓(count 點均分)。 */
public static void circle(Player player, Location center, double radius,
int points, ParticleOptions particle) {
for (int i = 0; i < points; i++) {
double angle = 2 * Math.PI * i / points;
Location loc = center.clone().add(
radius * Math.cos(angle), 0, radius * Math.sin(angle));
ParticleEffect.send(player, particle, loc, 1, 0, 0, 0, 0, false);
}
}
/** 從 start 到 end 畫一條粒子線段(density 控制點密度)。 */
public static void line(Player player, Location start, Location end,
double density, ParticleOptions particle) {
double distance = start.distance(end);
int steps = (int) (distance / density);
double dx = (end.getX() - start.getX()) / steps;
double dy = (end.getY() - start.getY()) / steps;
double dz = (end.getZ() - start.getZ()) / steps;
for (int i = 0; i <= steps; i++) {
Location loc = start.clone().add(dx * i, dy * i, dz * i);
ParticleEffect.send(player, particle, loc, 1, 0, 0, 0, 0, false);
}
}
/** 往上螺旋粒子(height 高度、loops 圈數、points 每圈點數)。 */
public static void helix(Player player, Location base, double radius,
double height, int loops, int pointsPerLoop,
ParticleOptions particle) {
int total = loops * pointsPerLoop;
for (int i = 0; i < total; i++) {
double angle = 2 * Math.PI * i / pointsPerLoop;
double y = height * i / total;
Location loc = base.clone().add(
radius * Math.cos(angle), y, radius * Math.sin(angle));
ParticleEffect.send(player, particle, loc, 1, 0, 0, 0, 0, false);
}
}
}
src/main/java/com/example/
├── MyNmsPlugin.java
└── effect/
├── ParticleEffect.java
├── ParticleBuilder.java
└── ParticleShapes.java
ParticleEffect.send() 內部呼叫 connection.send(),可在任何執行緒呼叫ParticleShapes 中的 start.distance(end) 需要兩個 Location 同屬一個世界Skills/_shared/nms-threading.md| 錯誤 | 原因 | 解法 |
|------|------|------|
| 粒子不顯示 | 客戶端粒子設定為「最少」 | 設 override = true 強制顯示 |
| 粒子只顯示在遠端 | offset 過大 | 減小 offsetX/Y/Z |
| Item 粒子不顯示 | 需使用 ItemParticleOption 而非 SimpleParticleType | 改用 new ItemParticleOption(ParticleTypes.ITEM, nmsItemStack) |
| Block 粒子不顯示 | 需使用 BlockParticleOption | 改用 new BlockParticleOption(ParticleTypes.BLOCK, blockState) |
development
透過 NMS Scoreboard/Objective/Team API 操作 sidebar、tablist 顯示名稱與計分板(Paper NMS + Mojang-mapped)/ Operate sidebar, tablist, and scoreboard via NMS Scoreboard/Objective/Team API
research
操作 GameProfile 進行 skin 注入,用於 NPC 外觀設定與假玩家實體(Paper NMS + Mojang-mapped)/ Manipulate GameProfile for skin injection used in NPC appearance and fake player entities
documentation
直接操作 CompoundTag 讀寫物品、實體、方塊實體的 NBT 資料(Paper NMS + Mojang-mapped)/ Read and write NBT data on items, entities, and block entities via CompoundTag
documentation
操作 Minecraft 1.21 DataComponentType 物品組件系統,讀寫 CustomData、MaxStackSize、Enchantments 等組件(Paper NMS + Mojang-mapped)/ Read and write 1.21 DataComponentType item components including CustomData, MaxStackSize, Enchantments