Еще с детства я начал покорять бесконечные просторы 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.
Скачать можно последний релиз.
Комментарии (7)
radiant-ai
13.07.2022 00:36+1Если чанки не прогружены в конечной точке телепортации, то внезапный телепорт туда вызовет панику небольшую для сервера, на спиготе их желательно заранее прогрузить, а на бумаге использовать teleportAsync.
Частицы так спавнить вполне себе ок, главное не забывать, что с главного потока использовать sleep может быть очень больно (в вашем случае 23 наносекунды - фигня).
Location сериализируется и десериализируется без проблем уже встроенным ридером yaml.
Ну и да, у спигота довольно хорошая поддержка работы с файлами конфигураций. Есть методы для загрузки дефолтного файла из jar, есть методы для установки дефолтных значений и даже работа с комментами.
Ах да, проверка местоположения игрока это очень дешёвая операция, её вполне можно вызывать на событии движения как только игрок чуть ниже минимальной координаты. Ой, ещё забыл, начиная с 1.18 разные миры имеют разную минимальную высоту, которую лучше проверять :)
zubovdev Автор
13.07.2022 00:37Благодарю за объяснение! Для меня вся информация полезна, т.к. только недавно начал познавать данный сектор. Думаю, что я буду дальше развивать этот плагин и в следующих версиях доработаю эти моменты :)
radiant-ai
13.07.2022 06:54Конечно! Чем больше разработчиков, тем больше плагинов и тем довольнее игроки!
Siorinex
13.07.2022 09:31+1Высоту, ниже которой телепорт, проще задать в конфиге: чтобы возврат проходил не когда черти куда улетел, а если в принципе ниже возможной границы, которая может быть разной.
vvvvvv112
Крутой плагин