- Поддерживаемые версии
- 1.20
Вступление
В данном руководстве мы рассмотрим базовое создание NPC с использованием библиотеки PacketEvents.
Код был написан на Java 17 с использованием PE 2.3.0 и протестирован на сервере 1.20.4.
Как создать простого NPC?
Код был написан на Java 17 с использованием PE 2.3.0 и протестирован на сервере 1.20.4.
Как создать простого NPC?
Благо разработчики PacketEvents уже позаботились об этом, предоставив класс NPC, в котором реализована значительная часть необходимой логики.
Создадим обработчик команд в котором будем триггерить спавн NPC
Создадим обработчик команд в котором будем триггерить спавн NPC
Java:
public record NPCData(NPC npc, int entityId, String command, World world) {}
Java:
public class NPCCommand implements CommandExecutor, TabCompleter {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (args.length == 0 || !(sender instanceof Player player)) {
return false;
}
switch (args[0].toLowerCase()) {
case "create":
handleCreate(player, args);
break;
case "destroy":
if (args.length > 1) {
try {
int entityId = Integer.parseInt(args[1]);
handleDestroy(player, entityId);
} catch (NumberFormatException e) {
player.sendMessage("Укажите валидный entityID.");
}
} else {
player.sendMessage("Укажите entityID.");
}
break;
default:
return true;
}
return true;
}
private void handleCreate(Player player, String[] args) {
int entityId = SpigotReflectionUtil.generateEntityId();
UUID entityUUID = UUID.randomUUID();
NPC npc = createNpc(player, entityUUID, entityId);
String Command = (args.length > 1 && !args[1].isEmpty()) ? args[1] : "paper version"; // Устанавливаем дефолтную команду, если игрок не предоставил свою
playerNPCs.computeIfAbsent(player.getUniqueId(), k -> new ArrayList<>()).add(new NPCData(npc, entityId, Command, player.getWorld()));
// Отобразим NPC всем игрокам на сервере
for (Player p : Bukkit.getOnlinePlayers()) {
com.github.retrooper.packetevents.protocol.player.User user = PacketEvents.getAPI().getPlayerManager().getUser(p);
npc.spawn(user.getChannel());
}
player.sendMessage("Создан NPC с EntityID: " + entityId + ".");
}
/**
* Данный метод установит все необходимые свойства нашему NPC
*/
private static @NotNull NPC createNpc(Player player, UUID entityUUID, int entityId) {
NPC npc = new NPC(
new UserProfile(entityUUID, player.getName() + "L"), // Фейковый профиль
entityId,
GameMode.SURVIVAL, // Игровой режим NPC
null, // Имя в табе
NamedTextColor.WHITE, // Цвет ника
null, // Префикс
null // Суффикс
);
npc.setLocation(new Location(
player.getLocation().getX(), player.getLocation().getY(), player.getLocation().getZ(),
player.getLocation().getYaw(), player.getLocation().getPitch()
));
return npc; }
Удаление NPC
С созданием разобрались, а как удалить?
Для этого добавим в наш класс NPCCommand еще два метода, handleDestroy
который будет обрабатывать удаление NPC по его EntityID и onTabComplete для обработки табкомплита наших команд
Для этого добавим в наш класс NPCCommand еще два метода, handleDestroy
который будет обрабатывать удаление NPC по его EntityID и onTabComplete для обработки табкомплита наших команд
Java:
public class NPCCommand implements CommandExecutor, TabCompleter {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (args.length == 0 || !(sender instanceof Player player)) {
return false;
}
switch (args[0].toLowerCase()) {
case "create":
handleCreate(player, args);
break;
case "destroy":
if (args.length > 1) {
try {
int entityId = Integer.parseInt(args[1]);
handleDestroy(player, entityId);
} catch (NumberFormatException e) {
player.sendMessage("Укажите валидный entityID.");
}
} else {
player.sendMessage("Укажите entityID.");
}
break;
default:
return true;
}
return true;
}
private void handleCreate(Player player, String[] args) {
int entityId = SpigotReflectionUtil.generateEntityId();
UUID entityUUID = UUID.randomUUID();
NPC npc = createNpc(player, entityUUID, entityId);
String Command = (args.length > 1 && !args[1].isEmpty()) ? args[1] : "paper version"; // Устанавливаем дефолтную команду, если игрок не предоставил свою
playerNPCs.computeIfAbsent(player.getUniqueId(), k -> new ArrayList<>()).add(new NPCData(npc, entityId, Command, player.getWorld()));
// Отобразим NPC всем игрокам на сервере
for (Player p : Bukkit.getOnlinePlayers()) {
com.github.retrooper.packetevents.protocol.player.User user = PacketEvents.getAPI().getPlayerManager().getUser(p);
npc.spawn(user.getChannel());
}
player.sendMessage("Создан NPC с EntityID: " + entityId + ".");
}
/**
* Данный метод установит все необходимые свойства нашему NPC
*/
private static @NotNull NPC createNpc(Player player, UUID entityUUID, int entityId) {
NPC npc = new NPC(
new UserProfile(entityUUID, player.getName() + "L"), // Фейковый профиль
entityId,
GameMode.SURVIVAL, // Игровой режим NPC
null, // Имя в табе
NamedTextColor.WHITE, // Цвет ника
null, // Префикс
null // Суффикс
);
npc.setLocation(new Location(
player.getLocation().getX(), player.getLocation().getY(), player.getLocation().getZ(),
player.getLocation().getYaw(), player.getLocation().getPitch()
));
return npc;
}
private void handleDestroy(Player player, int entityId) {
UUID playerUUID = player.getUniqueId();
List<NPCData> npcs = playerNPCs.get(playerUUID);
if (npcs == null) {
return;
}
Iterator<NPCData> iterator = npcs.iterator();
while (iterator.hasNext()) {
NPCData npcData = iterator.next();
if (npcData.entityId() == entityId) {
com.github.retrooper.packetevents.protocol.player.User user = PacketEvents.getAPI().getPlayerManager().getUser(player);
npcData.npc().despawn(user.getChannel());
iterator.remove();
player.sendMessage("NPC с EntityID: " + entityId + " уничтожено :}");
return;
}
}
player.sendMessage("NPC с EntityID " + entityId + " не найдено :{");
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, Command command, @NotNull String alias, String[] args) {
if (command.getName().equalsIgnoreCase("npc")) {
if (args.length == 1) {
return Arrays.asList("create", "destroy");
} else if (args.length == 2 && args[0].equalsIgnoreCase("destroy") && sender instanceof Player player) {
UUID playerUUID = player.getUniqueId();
List<NPCData> npcs = playerNPCs.get(playerUUID);
if (npcs != null) {
// Инициализируем массив для хранения всех NPC EntityID игрока
List<String> entityIds = new ArrayList<>();
for (NPCData npcData : npcs) {
entityIds.add(String.valueOf(npcData.entityId()));
}
return entityIds;
}
}
}
return new ArrayList<>();
}
}
Обработка нажатий
Для этого просто создадим пакетный слушатель в котором будем отслеживать получение пакета INTERACT_ENTITY
Java:
public class PacketInteractEntity extends PacketListenerAbstract {
/**
* Вызывается когда клиент отправляет INTERACT_ENTITY пакет
*/
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) {
WrapperPlayClientInteractEntity packet = new WrapperPlayClientInteractEntity(event);
Player player = (Player) SpigotReflectionUtil.getCraftPlayer((Player) event.getPlayer()); // Получаем Bukkit Player`а
int clickedEntityId = packet.getEntityId(); // Получаем EntityID сущности на которую кликнул игрок
for (List<NPCData> npcList : playerNPCs.values()) {
for (NPCData npcData : npcList) {
if (npcData.entityId() == clickedEntityId) { // Проверяем, кликнул ли игрок на нужную сущность
Bukkit.getScheduler().runTask(
NPCExample.instance,
() -> player.performCommand(npcData.command())); // С помощью планировщика Bukkit запускаем задачу в основном потоке через тик
return;
}
}
}
}
}
}
Поворот тела и головы NPC, отображение NPC для новых игроков
Для этого создадим новый класс, который будет слушать PlayerMove и PlayerJoin ивенты.
В PlayerJoinEvent`е мы будем спавнить NPC для каждого вошедшего игрока, а в PlayerMoveEvent`е мы будем отправлять игроку пакеты о том, как тело и голова NPC должны поворачиваться и перемещаться в зависимости от движения игрока.
В PlayerJoinEvent`е мы будем спавнить NPC для каждого вошедшего игрока, а в PlayerMoveEvent`е мы будем отправлять игроку пакеты о том, как тело и голова NPC должны поворачиваться и перемещаться в зависимости от движения игрока.
Java:
public class PlayerEventListener implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
for (Map.Entry<UUID, List<NPCData>> entry : playerNPCs.entrySet()) {
List<NPCData> npcList = entry.getValue();
for (NPCData npcData : npcList) {
npcData.npc().spawn(getUser(event.getPlayer()).getChannel()); // Спавним NPC для каждого нового игрока
}
}
}
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
for (Map.Entry<UUID, List<NPCData>> entry : playerNPCs.entrySet()) {
List<NPCData> npcList = entry.getValue();
for (NPCData npcData : npcList) {
if (npcData.world() != event.getPlayer().getWorld()) return;
// Конвертируем местоположение NPC в местоположение Bukkit
org.bukkit.Location convertedNPCLocation = SpigotConversionUtil.toBukkitLocation(npcData.world(), npcData.npc().getLocation());
float calculatedYaw = calculateYaw(event.getPlayer().getLocation(), convertedNPCLocation);
// Создаем пакет для поворота головы
WrapperPlayServerEntityHeadLook HeadLookPacket = new WrapperPlayServerEntityHeadLook(npcData.entityId(), calculatedYaw);
// Создаем пакет для поворота тела
WrapperPlayServerEntityRelativeMoveAndRotation RotationMovePacket = prepareRelativeMoveAndRotationPacket(npcData, convertedNPCLocation, calculatedYaw);
// Отправляем пакеты игроку
PacketEvents.getAPI().getPlayerManager().sendPacket(event.getPlayer(), RotationMovePacket);
PacketEvents.getAPI().getPlayerManager().sendPacket(event.getPlayer(), HeadLookPacket);
}
}
}
private static @NotNull WrapperPlayServerEntityRelativeMoveAndRotation prepareRelativeMoveAndRotationPacket(NPCData npcData, Location convertedNPCLocation, float calculatedYaw) {
double deltaX = (npcData.npc().getLocation().getX() - convertedNPCLocation.getX()) * 32 * 128;
double deltaY = (npcData.npc().getLocation().getY() - convertedNPCLocation.getY()) * 32 * 128;
double deltaZ = (npcData.npc().getLocation().getZ() - convertedNPCLocation.getZ()) * 32 * 128;
return new WrapperPlayServerEntityRelativeMoveAndRotation(
npcData.entityId(),
(short) deltaX,
(short) deltaY,
(short) deltaZ,
calculatedYaw, npcData.npc().getLocation().getPitch(), true);
}
private static User getUser(Player player) {
return PacketEvents.getAPI().getPlayerManager().getUser(player);
}
/**
* Вычисляет yaw в градусах от местоположения игрока к местоположению NPC.
*/
private static float calculateYaw(org.bukkit.Location playerLocation, org.bukkit.Location npcLocation) {
double deltaX = playerLocation.getX() - npcLocation.getX();
double deltaZ = playerLocation.getZ() - npcLocation.getZ();
return (float) Math.toDegrees(Math.atan2(deltaZ, deltaX)) - 90;
}
}
Заключение
Java:
public class NPCExample extends JavaPlugin {
public static final HashMap<UUID, List<NPCData>> playerNPCs = new HashMap<>();
@Override
public void onLoad() {
instance = this;
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(this));
PacketEvents.getAPI().getSettings().reEncodeByDefault(false)
.checkForUpdates(true)
.bStats(true);
PacketEvents.getAPI().load();
}
@Override
public void onEnable() {
PacketEvents.getAPI().init();
PacketEvents.getAPI().getEventManager().registerListener(new PacketInteractEntity());
Objects.requireNonNull(getCommand("npc")).setExecutor(new NPCCommand());
getServer().getPluginManager().registerEvents(new PlayerEventListener(), this);
}
public static NPCExample instance;
}
GitHub - pnby/NPCExample
Contribute to pnby/NPCExample development by creating an account on GitHub.
github.com
Если я допустил какие либо ошибки или неточности - пишите мне, я всё исправлю