Разработка собственного плагина для сервера Minecraft

от автора

Еще с детства я начал покорять бесконечные просторы Minecraft. Естественно о разработке в то время никакой речи не шло. Но с недавних пор загорелся идеей создать о свой проект серверов.

На Java до этого никогда не писал, но есть бекграунд на других языках, поэтому осталось только приспособиться. Соотвественно разработка плагинов, Bukkit и другие библиотеки вижу впервые, но посмотрев несколько туторов, стала понятна примерная концепция.

Ранее писал на таких языках как PHP, JS. В данный момент веду разработку на языке Go. Сильно привык к «гошке» и его синтаксису и в процессе написания плагина часто использовал синтаксис Go для написания логических конструкций.

Мне не сильно хотелось использовать какие-то готовые решения, ведь тогда не будет углубленных знаний, которые я получу в процессе написания кода. Хочется одновременно и поучить Java и написать что-то свое (самое главное).

В этой статье я не буду затрагивать процесс настройки окружения, установки IDE и стороннего софта.

Идея плагина

На серверах часто используются постройки, находящиеся в пустоте, например летающие лобби, острова. Такую модель постройки мы выбрали вместе с моим другом: летающие острова. Одной из проблем таких построек — Игрок может провалиться в пустоту и не выбраться.

Прошерстив Google мне удалось найти парочку подходящих плагинов, которые уже решают это проблему. Но один из них, который оказался поддерживаем разработчиком и самими ядром сервера, предоставлял ограниченный функционал, расширенный можно было приобрести на X евро. Фича, которая мне понравилась в платном плагине — создание анимаций из частиц после телепортации из пустоты.

Мне захотелось самому понять, как это сделать, разработать собственный плагин, а потом с удовольствием им пользоваться, поддерживать, находить баги — мое мелкое детище, как никак.

Создаем сам плагин

Назвал я плагин просто — VoidTeleport.

Первым делом создал класс для управления конфигурацией плагина.

public class Config {     private static File file;     private static FileConfiguration config;      private static final String fileNameConfig = "config.yml";      /**      * Initializes the static Config class.      */     public static void init() {         // Получаем инстанс нашего плагина.         Plugin plugin = Bukkit.getServer().getPluginManager().getPlugin(VoidTeleport.PluginName);         if (plugin == null) {             // На этом моменте что-то пошло не так,            // нужно обработать и залогировать.             Bukkit.getLogger().log(                     Level.WARNING,                     MessageFormat.format("Cannot get plugin {0}", VoidTeleport.PluginName)             );             return;         }          file = new File(plugin.getDataFolder(), fileNameConfig);          // Мы не знаем существует ли файл, поэтому пытаемся создать его.       // Если файл уже есть, то выражение file.createNewFile() вернет false.          try {             if (file.createNewFile()) {                 plugin.getLogger().log(                         Level.INFO,                         MessageFormat.format("New config file with name {0} was created", fileNameConfig)                 );             }         } catch (IOException e) {             plugin.getLogger().log(Level.SEVERE, e.toString());             return;         }          // На данно моменте наш конфиг пустой,        // поэтому подгружаем его из файла.         reload();     }      /**      * Getter      * @return FileConfiguration      */     public static FileConfiguration get() {         return config;     }      public static void reload() {       // Самый простой анмаршаллер YAML из файла.         config = YamlConfiguration.loadConfiguration(file);     }

Отлично! Класс для работы с конфигом уже есть, теперь нужно определиться со структурой файла config.yml. Нужно реализовать поддержку для разных миров, поэтому не придумал ничего проще, как просто указать список нужных миров.

worlds:     # Наименование мира, например spawn, world, world_the_end   - name: spawn   # Координаты для респавна игрока при падении в пустоту     spawnLocation:       x: 0       y: 0       z: 0

Конфиг есть, теперь можно приступить к созданию обработчика событий. Мой выбор пал на событие EntityDamageByBlockEvent. Можно было бы и слушать событие PlayerMoveEvent, но оно случается гораздо чаще, чем триггер на получение урона. Лишняя нагрузка на сервер не нужна, поэтому стал слушать урон.

public class PlayerDamageListener implements Listener {   // Хеш мапа в которой хранится наименования мира и точка телепортации.     private HashMap<String, Location> worlds = new HashMap<>();      @EventHandler     public void onPlayerDamage(EntityDamageByBlockEvent e) {         if (!(e.getEntity() instanceof Player)) {             // Это не игрок.             return;         }          if (e.getCause() != EntityDamageEvent.DamageCause.VOID) {             // Урон не от пустоты.             return;         }          Player player = (Player) e.getEntity();        // Получаем мир, в котором находится Игрок.         World world = player.getWorld();              // Пытаемся найти в хеш мапе значение по наименованию мира.         Location spawnLocation = this.worlds.get(world.getName());         if (spawnLocation == null) {             // К этому миру не действует правило телепорта.             return;         }          // Данный код является костылем, который я быстро сообразил.       // Проблема в том, что мир может быть = null.       // В таком случае устанавливаем мир на тот, в котором находится игрок.         if (spawnLocation.getWorld() == null) {             spawnLocation.setWorld(world);         }          // Добрались до самого главного.       // Отменяем событие, которое наносит урон игроку.         e.setCancelled(true);          // Отменяем сам урон от падения,  // чтобы при телепортации игрок не разбился.         player.setFallDistance(0);          // Телепортируем игрока.         player.teleport(spawnLocation);          // Доабвляем анимацию из частиц при попадании на точку телепортации.         Spiral.spawn(player);     }      @SuppressWarnings("unchecked")     public void updateWorlds(@Nullable ArrayList<HashMap<String, Object>> listWorlds) {         if (listWorlds == null) {           // Ну если null, так null - ничего не делаем.             return;         }          // Очищаем мапу.         this.worlds = new HashMap<>();          for (HashMap<String, Object> world: listWorlds) {             String worldName = (String) world.get("name");             if (Objects.equals(worldName, "")) {                 // Тут хорошо бы залогировать, но просто скипаем.                 continue;             }              Location spawnLocation = Location.deserialize((Map<String, Object>) world.get("spawnLocation"));                        // Т.к. мир у нас не указан, поэтому получаем его.           spawnLocation.setWorld(Bukkit.getWorld(worldName));              // Сохраняем в хеш мапу.             this.worlds.put(worldName, spawnLocation);         }     } }

Тепер разберем вызов эффекта анимации при телепортации Spiral.spawn(player). Назвал класс Spiral, потому что эффект будет в виде спирали.

Т.к. это мой первый плагин, то не стал заморачиваться с Пакетами и ProtocolLib.

Описываем анимацию в отдельном классе Spiral. Я предпочел реализовать спираль под названием Helix — достаточно простая в реализации модель. Пришлось немного вспомнить тригонометрию, но у меня получилось!

public class Spiral {     public static void spawn(@NotNull Player player) {         Location location = player.getLocation();        // Задаем радиут спирали.         double radius = 0.5;          for (double y = 0; y <= 23; y += 0.1) {             double x = radius * Math.cos(y);             double z = radius * Math.sin(y);              Location particleLocation = new Location(location.getWorld(), location.getX(), location.getY(), location.getZ());             player.spawnParticle(Particle.REDSTONE, particleLocation.add(x, y / 10, z), 2, new Particle.DustOptions(Color.AQUA, 1.0F));              try {               // Думаю, что это плохо, но для первого раза сойдет.                 TimeUnit.NANOSECONDS.sleep(1);             } catch (InterruptedException e) {                 Bukkit.getLogger().log(Level.SEVERE, e.toString());             }         }     } }

Почему в коде 23? Это число является ограничением для координаты y. Т.е. по сути спираль будет подниматься вверх на y = 2.3. Как можно заметить, при указании смещения particleLocation.add(x, y / 10, z) y делится на 10. Еще одной причиной стало то, что спираль не успевает несколько раз «обернуть» игрока.

Собираем все вместе

Наконец можем собрать наш код в единой точке и протестировать, что получилось.

public final class VoidTeleport extends JavaPlugin {     public static final String PluginName = "VoidTeleport";      @Override     public void onEnable() {         getLogger().log(Level.INFO, "Plugin enabled!");          // Инициализируем конфиг         Config.init();        // Регистрируем обработчик событий для входщего урона         this.registerDamageEvent();     }      @Override     public void onDisable() {         getLogger().log(Level.INFO, "Plugin disabled!");     }      @SuppressWarnings("unchecked")     private void registerDamageEvent() {       // Инициализируем обработчик         PlayerDamageListener damageListener = new PlayerDamageListener();              // Достаем из конфига нужные значения и обновляем хеш мапу в обработчике         damageListener.updateWorlds((ArrayList<HashMap<String, Object>>) Config.get().get("worlds"));        // Регистрируем новое событие на сервере         getServer().getPluginManager().registerEvents(damageListener, this);     } }

Результат

При заданным настройкам файле конфигурации мы успешно попадаем в указанную точки и наблюдаем просто классную анимацию, как по мне.

И без указания мира в конфиге.

Можно посмотреть код этого плагина в моем репозитории Github.

Скачать можно последний релиз.


ссылка на оригинал статьи https://habr.com/ru/post/676248/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *