Еще с детства я начал покорять бесконечные просторы 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.
Скачать можно последний релиз.
