Разработка собственного плагина для сервера 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 ", 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 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 HashMapworlds = 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> listWorlds) < if (listWorlds == null) < // Ну если null, так null - ничего не делаем. return; >// Очищаем мапу. this.worlds = new HashMap<>(); for (HashMap world: listWorlds) < String worldName = (String) world.get("name"); if (Objects.equals(worldName, "")) < // Тут хорошо бы залогировать, но просто скипаем. continue; >Location spawnLocation = Location.deserialize((Map) 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 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>) Config.get().get("worlds")); // Регистрируем новое событие на сервере getServer().getPluginManager().registerEvents(damageListener, this); > >
Результат
При заданным настройкам файле конфигурации мы успешно попадаем в указанную точки и наблюдаем просто классную анимацию, как по мне.
И без указания мира в конфиге.
Можно посмотреть код этого плагина в моем репозитории Github.
Скачать можно последний релиз.
Онлайн курс программирование Майнкрафт

Дети изучают Java, наиболее широко используемый язык программирования в мире. Они узнают: функции, условия, методы, параметры, циклы и многое другое. Проект создание модов майнкрафт — Дети учатся создавать моды, как профессионалы. Разбираются со сложным исходным кодом, редактируют и точно настраивают различные элементы игры, правильно комментируют свой код и исправляют ошибки, когда они появляются.
Онлайн курс программирование Майнкрафт – это не просто развлечение с любимой детской игрой, а настоящее обучение программированию с нуля на языке Java. Игра Майнкрафт – это хорошая мотивация продолжать курс, а кроме того, сама игра написана на языке Java. Еженедельно учащийся получает видео уроки с пошаговыми инструкциями и задания для выполнения. На курсе предусмотрены тесты для определения уровня усвоения материала. Если у ребенка возник вопрос или сложности, можно написать преподавателю, онлайн-поддержка доступна ежедневно. Часто задаваемые вопросы.
Программирование на Java в Minecraft
Курс по продвинутому программированию в Minecraft с использованием одного из самых востребованных языков — Java. Разработка модов происходит в программе MCreator, что позволяет детям в удобном формате совершенствовать навыки программирования.
Пробный урок Подробнее

Изучение языка программирования Java
Глубокое ознакомление с особенностями изменения игры Minecraft
Создание масштабных модификаций
Развитие алгоритмического, логического и творческого мышления
Результат от курса
- Знание основ программирования на языке Java
- Формирование профессионального подхода к разработке
- Выпуск собственных глобальных модификаций
- Навыки командной работы
ГРУППОВОЕ ОЧНОЕ обучение
6400 ₽/зан. 5600 ₽/зан.
2 часа (2 блока по 40 минут)
- Навык самопрезентации
- Повышение мотивации
- Коммуникативные навыки
Купить Купить Купить

ГРУППОВОЕ ОНЛАЙН обучение
2 часа (2 блока по 40 минут)
- Самодисциплина
- Без затрат на дорогу
- Выгодная цена
Купить Купить Купить
ИНДИВИДУАЛЬНОЕ ОЧНОЕ обучение
График занятий составляется индивидуально
- Постоянный контакт с наставником
- Углубленное изучение
- Удобный график
Купить Купить Купить
ИНДИВИДУАЛЬНОЕ ОНЛАЙН обучение
График занятий составляется индивидуально
- Углубленное изучение
- Удобный график
- Без затрат на дорогу
Купить Купить Купить
Налоговый вычет
Жмите «Смотреть» чтобы узнать как получить налоговый вычет на своё обучение или обучение своих детей, подопечных, братьев или сестёр.

Доступное обучение
Мы выделили по одному бюджетному месту в каждом курсе, чтобы у всех детей была возможность получить доступное и качественное образование в интересной для него сфере. Участвуйте в конкурсе, забронируйте место на курс и подарите своему ребенку шанс бесплатно пройти обучение в Школе будущего «CYBERLAB».
Места ограниченны! Регистрируйтесь прямо сейчас!
Java программирование.
Вопрос к разработчикам модов / плагинов.
Есть несколько вопросов.
Какие знания нужны для выполнения данных задач?
Для разработки собственного детища мне нужно сначала получить базовые знания о Java программировании или лучше сразу начать по гайду на McModding?
Сколько вы обучались данному мастерству?
Могучий горгон

1,870 52 357
Свой первый «мод» я сделал без знания java
Не тот велик, кто никогда не падал,
но тот велик, кто падал и вставал
Putnik
GetRekt написал(а):
Вопрос к разработчикам модов / плагинов.
Есть несколько вопросов.
Какие знания нужны для выполнения данных задач?
Для разработки собственного детища мне нужно сначала получить базовые знания о Java программировании или лучше сразу начать по гайду на McModding?
Сколько вы обучались данному мастерству?
Без знания Java(хотя бы поверхностно) соваться смысла нет. И так куча проблем будет, даже без учета особенностей языка.
Liahim
4,035 63 640
WhiteWarrior написал(а):
Свой первый «мод» я сделал без знания java
Аналогично )))
Обучался делая мод.
Спустя год знаю основы java и forge API.
GetRekt
Как мне кажется «первый мод» это было что то типо добавить свой крафт/броню/блок и так далее. Меня интересует нечто большее.
К примеру полная переработка худа, совмещение модов и так далее.
Liahim
4,035 63 640
Вот мой первый мод https://minecraft.curseforge.com/projects/saltymod
Финальная версия получилась через год с нуля и без навыков программирования.
MaximPixel
1,560 86 204
GetRekt написал(а):
Как мне кажется «первый мод» это было что то типо добавить свой крафт/броню/блок и так далее. Меня интересует нечто большее.
К примеру полная переработка худа, совмещение модов и так далее.
По мере добавления своих предметов, блоков и т.д. Ты можешь и выучить Jav’у, но лучше параллельно учить более лёгкий язык, например Python или Javascript. А то Java, сложноватый.
Могучий горгон

1,870 52 357
Liahim написал(а):
WhiteWarrior написал(а):
Свой первый «мод» я сделал без знания java
Аналогично )))
Обучался делая мод.
Спустя год знаю основы java и forge API.
смотрю на свой первый мод.
Спойлер: Спойлер
Mod (modid = «updatecraft», name=»Electro-GalacticUpdate», version = «1.5-alpha», dependencies=»required-after:GalacticraftCore; required-after:GalacticraftMars; required-after:IC2″)
public class BaseUpdateCraft public static String ASSET_PREFIX = «updatecraft»;
@Instance(«updatecraft»)
public static BaseUpdateCraft instance;
@SidedProxy(clientSide = «com.kirill.UpdateCraft.ClientProxy», serverSide = «com.kirill.UpdateCraft.CommonProxy»)
public static CommonProxy proxy;
private static int modGuiIndex = 0;
public static final int GUI_ITEM_INV = modGuiIndex++;
public static boolean enablePlanet = false;
@EventHandler
public void init(FMLInitializationEvent event)
@EventHandler
public void load(FMLInitializationEvent event)* EntityRegistry.addSpawn(MyBestEntity.class, 1000, 100, 200, EnumCreatureType.monster, BiomeGenBase.plains); */
NetworkRegistry.INSTANCE.registerGuiHandler(this, new GuiHandler());
proxy.registerRenderers();
proxy.registerNetworkStuff();
GalaxyList.planet();
public static void registerEntity(Class entityClass, String name, int primaryColor, int secondaryColor)
int entityID = EntityRegistry.findGlobalUniqueEntityId();
long seed = name.hashCode();
EntityRegistry.registerGlobalEntityID(entityClass, name, entityID);
EntityRegistry.registerModEntity(entityClass, name, entityID, instance, 64, 1, true);
EntityList.entityEggs.put(Integer.valueOf(entityID), new EntityList.EntityEggInfo(entityID, primaryColor, secondaryColor));
>
@Mod.EventHandler
public void preInit(FMLPostInitializationEvent e) proxy.registerRenderers();
proxy.registerItemRenderers();>
@Mod.EventHandler
public void init(FMLPostInitializationEvent e) GCCoreUtil.registerGalacticraftNonMobEntity(EntityTier4Rocket.class, «SpaceshipT4», 150, 1, false);
HashMap input = new HashMap();
input.put(1, new ItemStack(ItemsList.CompressedIridium));
input.put(2, new ItemStack(ItemsList.CompressedIridium));
input.put(3, new ItemStack(ItemsList.CompressedIridium));
input.put(4, new ItemStack(ItemsList.CompressedIridium));
input.put(5, new ItemStack(ItemsList.CompressedIridium));
input.put(6, new ItemStack(ItemsList.CompressedIridium));
input.put(7, new ItemStack(ItemsList.CompressedIridium));
input.put(8, new ItemStack(ItemsList.CompressedIridium));
input.put(9, new ItemStack(ItemsList.CompressedIridium));
input.put(10, new ItemStack(ItemsList.CompressedIridium));
input.put(11, new ItemStack(ItemsList.CompressedIridium));
input.put(12, new ItemStack(ItemsList.CompressedIridium));
input.put(13, new ItemStack(ItemsList.CompressedIridium));
input.put(14, new ItemStack(ItemsList.CompressedIridium));
input.put(15, new ItemStack(ItemsList.CompressedIridium));
input.put(16, new ItemStack(ItemsList.CompressedIridium));
input.put(17, new ItemStack(ItemsList.CompressedIridium));
input.put(18, new ItemStack(ItemsList.CompressedIridium));
input.put(19, null);
input.put(20, null);
input.put(21, null);
EGUcoreUtil.addRocketBenchT4Recipe(new ItemStack(ItemsList.rocketTier4, 1, 0), input);
HashMap input2 = new HashMap(input);
input2.put(19, new ItemStack(Blocks.chest));
input2.put(20, null);
input2.put(21, null);
EGUcoreUtil.addRocketBenchT4Recipe(new ItemStack(ItemsList.rocketTier4, 1, 1), input2);
input2 = new HashMap(input);
input2.put(19, null);
input2.put(20, new ItemStack(Blocks.chest));
input2.put(21, null);
EGUcoreUtil.addRocketBenchT4Recipe(new ItemStack(ItemsList.rocketTier4, 1, 1), input2);
input2 = new HashMap(input);
input2.put(19, null);
input2.put(20, null);
input2.put(21, new ItemStack(Blocks.chest));
EGUcoreUtil.addRocketBenchT4Recipe(new ItemStack(ItemsList.rocketTier4, 1, 1), input2);
input2 = new HashMap(input);
input2.put(19, new ItemStack(Blocks.chest));
input2.put(20, new ItemStack(Blocks.chest));
input2.put(21, null);
EGUcoreUtil.addRocketBenchT4Recipe(new ItemStack(ItemsList.rocketTier4, 1, 2), input2);
input2 = new HashMap(input);
input2.put(19, new ItemStack(Blocks.chest));
input2.put(20, null);
input2.put(21, new ItemStack(Blocks.chest));
EGUcoreUtil.addRocketBenchT4Recipe(new ItemStack(ItemsList.rocketTier4, 1, 2), input2);
input2 = new HashMap(input);
input2.put(19, null);
input2.put(20, new ItemStack(Blocks.chest));
input2.put(21, new ItemStack(Blocks.chest));
EGUcoreUtil.addRocketBenchT4Recipe(new ItemStack(ItemsList.rocketTier4, 1, 2), input2);
input2 = new HashMap(input);
input2.put(19, new ItemStack(Blocks.chest));
input2.put(20, new ItemStack(Blocks.chest));
input2.put(21, new ItemStack(Blocks.chest));
EGUcoreUtil.addRocketBenchT4Recipe(new ItemStack(ItemsList.rocketTier4, 1, 3), input2);
>
@Mod.EventHandler
public void postInit(FMLPostInitializationEvent e)
>
Не тот велик, кто никогда не падал,
но тот велик, кто падал и вставал