1. Вы находитесь в сообществе Rubukkit. Мы - администраторы серверов Minecraft, разрабатываем собственные плагины и переводим на различные языки плагины наших коллег из других стран.
    Скрыть объявление
Скрыть объявление
В преддверии глобального обновления, мы проводим исследования, которые помогут нам сделать опыт пользования форумом ещё удобнее. Помогите нам, примите участие!

Стартап [Туториал] Работа с большим количеством блоков в мире

Тема в разделе "Разработка плагинов для новичков", создана пользователем Dymeth, 4 ноя 2022.

  1. Автор темы
    Dymeth

    Dymeth Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    Dymeth
    Всем привет!

    Хочу поделиться своим опытом, который позволит вам создавать механики буквально для всего мира:
    - Выполнять операции в чанках во время отсутствия игроков в этих чанках
    - Хранить собственную информацию в любых блоках (а не только в тайлах)

    Так, например, я на версии 1.12.2 реализовывал плагин, запрещающий игрокам в выживании разрушать блоки, поставленные другими игроками в креативе. В данном плагине присутствовала поддержка FastAsyncWorldEdit. Благодаря описанным ниже техническим решениям плагин оперировал десятками миллионов блоков - всё это происходило без долгих загрузок сервера и без сильной нагрузки при большом онлайне. И это при том, что на версии 1.12 не существовало PersistentDataContainer'ов. Более того, даже чанки на этой версии грузились в основном потоке сервера.

    Ниже я расскажу общую концепцию того, как вы сможете реализовать подобное самостоятельно.
    Сразу предупрежу, что статья состоит исключительно из теории, поэтому пилить предложенные мной решения вам придётся самостоятельно :)

    Текста много, но вы держитесь. Поехали!

    Для примера задачи предлагаю взять эту тему: https://rubukkit.org/threads/184788
    Человеку требуется раз в сутки производить рост культур по всему миру, включая выгруженные чанки. Я же со своей стороны хочу усложнить задачу. Допустим, нам необходимо, чтобы рост происходил раз в 5 минут, чтобы со стороны игроков всё выглядело так, будто рост происходит в реальном времени.

    Прежде всего, я предлагаю сформировать список блоков, с которыми мы будем работать в будущем.
    Это позволит не перебирать все блоки в мире при наших операциях, а перебирать исключительно блоки, с которыми мы планируем работать.
    В нашем примере для составления этого списка требуется проверить, что каждый блок в мире является блоком культуры. Поскольку в чанке множество блоков, а в мире множество чанков - придётся придумать, как оптимизировать этот процесс, чтобы он не занимал часы или даже дни.

    Мы можем хранить список блоков индивидуально для каждого чанка. В этом случае нам не придётся перебирать весь мир. Теперь формировать список блоков в каждом чанке мы должны в двух случаях:
    1) В ChunkLoadEvent
    2) При включении плагина получать все чанки, которые уже загружены в оперативную память

    Отлично, теперь у нас есть список блоков культур для каждого чанка. Теперь для роста культур нам достаточно перебирать этот список, а не перебирать каждый раз все блоки в чанках. Перебирать целевые блоки мы можем хоть раз в сутки, хоть раз в 5 минут - на производительности это сильно не скажется.

    Однако при увеличении онлайна мы столкнёмся с ещё одной проблемой. Чем больше игроков - тем больше чанков загружается в оперативную память в единицу времени. А перебор всех блоков в чанке - операция достаточно ресурсозатратная. Таким образом, мы увидим существенную нагрузку на процессор от нашего плагина в ChunkLoadEvent.

    Окей, плавно подходим к мысли, что необходимо хранить список целевых блоков во внешнем хранилище, чтобы не перебирать блоки каждый раз, а получать из этого хранилища. Естественно, при первом взаимодействии с тем или иным чанком мы будем обязаны перебрать все блоки в чанке. Но при повторном использовании достаточно будет просто обратиться к нашему хранилищу.
    Для простоты назовём наше хранилище кэшем.

    Кэш может храниться где угодно: в базе данных, на диске, даже на условном бэкенде веб-сервера при желании. Но для господ с версией 1.14+ я рекомендую использовать PersistentDataContaner чанков. Это просто удобно. PersistentDataContaner хранит данные на диске в dat-файлах регионов.
    Мой плагин с блоками из креатива писался под 1.12.2, когда ещё не существовало PersistentDataContaner, поэтому тогда было принято решение вручную хранить данные чанков в папках миров - т.е. файлы создавались самостоятельно.

    Однако учтите, что кэш каждого чанка должен сохраняться полностью индивидуально, т.е. отдельно от других чанков. Так, например, если вы попытаетесь хранить информацию обо всех чанках в одном YML-файле, то сведёте на нет все усилия по разделению системы на чанки. Чтение и запись одного лишь чанка приведёт к чтению/записи всего YML-файла целиком. Поэтому чем больше чанков мы имеем - тем дольше будет происходить операция загрузки и сохранения. Нас такой вариант не устраивает, будьте внимательнее.

    Что ж, мы создали кэш. Получать из него информацию будем в ChunkLoadEvent и onEnable(), а сохранять в ChunkUnloadEvent.
    Алгоритм получения информации из кэша примерно следующий:
    - Пытаемся получить информацию
    - Если информация найдена, то возвращаем
    - Если информация отстствует, то перебираем все блоки в чанке и заносим в кэш
    - Возвращаем информацию, которую только что закэшировали

    Также можно сэкономить существенное кол-во ресурсов, если мы НЕ будем сохранять информацию, которая НЕ была изменена в процессе использования чанка. Это существенно поможет, например, при перемещении игроков при помощи элитр и трезубцев - кэш будет исключительно загружаться, но необходимости производить сохранение просто-напросто не будет.
    Технически реализовать такую систему довольно просто - достаточно в оперативной памяти вместе со списком всех блоков хранить ещё флаг - условный boolean isDirty = false, который будет переводиться в значение true, только если список блоков изменился:
    - игрок посадил новую культуру
    - игрок собрал старую культуру (она не обязательно может быть выросшей)
    - культура выросла самостоятельно

    Важный момент: отсутствие кэша чанка и отсутствие целевых блоков в кэше - это два разных состояния. Даже если в чанке нет целевых блоков, мы должны сохранить эту информацию в кэш, чтобы при очередной загрузке этого чанка вновь не перебирать все блоки.

    Окей, теперь у нас готов достаточно удобный и производительный инструмент хранения данных о наших блоках. Можем ли мы улучшить что-то ещё? Вполне:

    1) Загружать данные из кэша асинхронно. В новых версиях ChunkLoadEvent сам по себе вызывается асинхронно, но я не рекомендовал бы использовать этот поток для загрузки данных из кэша. Если есть возможность - лучше загружать данные асинхронно относительно асинхронной загрузки чанка. Это позволит отдать игроку чанк не дожидаясь загрузки наших кастомных данных. Это хорошо скажется на игровом опыте пользователя и позволит избежать ситуаций, когда из-за условной проблемы с базой данных у игрока перестанут полностью грузиться чанки.

    2) Для перебора всех блоков в чанке для заполнения кэша можно использовать ChunkSnapshot - это позволит выполнять перебор асинхронно.

    3) Использовать свои собственные оптимизации в зависимости от задачи. Например, в задаче с культурами можно отказаться от перебора 4-х нижних блоков мира, поскольку ничего кроме бедрока там находиться не может. Можно ограничить и верхнюю координату - достаточно получать в каждом столбце ZX высочайшую точку мира при помощи метода event.getWorld().getHighestBlockYAt()

    Отлично. Теперь встаёт вопрос о том, как оптимально обновлять информацию о блоках?
    Вновь обратимся к нашей задаче с ростом культур каждые 5 минут. Если допустить, что в оперативной памяти сейчас находятся десятки и сотни тысяч блоков, то их одновременное одновременное обновление заставит сервер изрядно попотеть. Но нам ничего не мешает обновлять их не одновременно. Не одновременно - это значит не в один серверный тик. Ответ очевиден - разделить большую задачу обновления на несколько задач поменьше и выполнять их в разные тики. Для этого можно использовать баккитовский шедулер. Например, каждый тик мы можем обновлять не более 5-и тысяч блоков.
    В конечном итоге схема будет выглядеть так:
    - при запуске сервера создаём шедулер с периодом в 5 минут
    - внутри шедулера формируем лист/массив блоков, которые должны быть обновлены за этот цикл (можно рандомизировать)
    - создаем новый шедулер с периодом в 1 тик
    - внутри этого шедулера имеем переменную кол-ва запусков
    - исходя из количества запусков считаем, блоки под какими номерами/индексами нам надо обновить
    - после выполнения всех мини-задач останавливаем внутренний шедулер

    Вместо массива или листа можно использовать итератор любой коллекции - тут всё зависит уже от того, как вам самим удобнее. Возможно даже нет смысла раз в 5 минут составлять список блоков, а выбирать их рандомно каждый тик. Но в этом случае существует вероятность, что одна и та же культура вырастет два раза подряд за один пятиминутный цикл. Но можно по ходу выполнения мини-задач заполнять список блоков, который уже выросли. Короче говоря, тут всё зависит от вашей фантазии. Основная ваша задача - разбить большую задачу на разные тики.

    Поскольку мы оперируем исключительно чанками, загруженными в оперативную память, такой подход не вызовет никаких проблем. Хотя я бы всё равно рекомендую вынести в конфиг максимальное кол-во операций в тик, чтобы в любой момент можно было скорректировать значение исходя из характеристик железа. Если совсем запариться, то можно автоматически корректировать это значение исходя, например, из времени выполнения тика на сервере. Примерно по такому принципу и работает FastAsyncWorldEdit.

    Ну и, пожалуй, последний нераскрытый вопрос - как обновлять блоки в чанках, которые не загружены в оперативную память?
    Ответ очень прост - никак не обновлять. В этом нет никакой необходимости, поскольку выгруженный чанк никем не используется. Но нам ничего не мешает произвести необходимые действия при загрузке этого чанка в оперативную память.

    Более того, мы можем точно узнать, сколько времени чанк был "оффлайн". Для этого при выгрузке чанка из памяти нужно сохранять любое число, обозначающее что-то из этого:
    - время реального мира в миллиасекундах: System.currentTimeMillis()
    - время игрового мира в тиках: world.getFullTime()

    Путём нехитрых вычислений мы получием время, которое прошло с момента выгрузки чанка. И исходя из этого времени можем "сэмулировать" игровые механики.
    В случае с ростом культур логика примерно такая:
    - Если прошло 5 минут, то увеличить стадию роста всех культур на 1 единицу
    - Если прошло 10 минут, то увеличить стадию роста всех культур на 2 единицу
    ...
    - Если прошли сутки, то увеличить стадию роста всех культур на максимальную

    Таким образом, с точки зрения игрока всё получится максимально интуитивно: будто мир без его присутствия продолжает развиваться. Он только вошёл на сервер, телепортировался к себе домой, а там уже выросшие культуры.

    Собственно, на этом всё. Мы оперируем большим кол-вом блоков, сервер работает стабильно, игроки довольны!
    Если что-то непонятно - задавайте вопросы

    Статья подготовлена Dymeth.Ru
     
    Последнее редактирование: 4 ноя 2022
  2. Автор темы
    Dymeth

    Dymeth Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    Dymeth
    Резерв
     
  3. Krongss_fur

    Krongss_fur Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    Krong
    Прям огромное количество предложений, я даже в шоке что ради такого было написано столько текста.
     
  4. Автор темы
    Dymeth

    Dymeth Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    Dymeth
    На самом деле, на форуме уже не первый раз возникают вопросы подобного рода, а короткого ответа на них нет.
    Решил рассказать, как можно действовать в такой ситуации, чтобы закрыть вопрос раз и навсегда
     
  5. OJIEKCAHDP

    OJIEKCAHDP Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    OJIEKCAHDP
    Неплохо!:good:
    Вообще, первая большая тема, которую было даже интересно почитать
     
  6. imDaniX

    imDaniX Активный участник Пользователь

    Баллы:
    96
    Имя в Minecraft:
    imDaniX
    Круто, круто. Террария, кажется, примерно также обрабатывает незагруженные участки карты.

    Хотя у себя я несколько иначе делаю - не требуется изменять чанки вне прогрузки игрока, а блоков для обработки слишком много (буквально все камни). Собираю координаты чанков вокруг игроков в небольшой класс, потом потихоньку их обрабатываю небольшой утилитой:
    PHP:
    public static <Tvoid runIteratingTask(Plugin pluginCollection<TcolConsumer<Tactionlong timeout) {
        
    Iterator<Titerator col.iterator();
        
    plugin.getServer().getScheduler().runTaskTimer(plugin, (task) -> {
            if (
    iterator.hasNext()) {
                
    action.accept(iterator.next());
            } else {
                
    task.cancel();
            }
        }, 
    timeouttimeout);
    }
    В качестве action - беру ChunkSnapshot и проверяю все блоки в асинхроне. После можно либо прошлым методом воспользоваться, если блоков слишком много, либо сетнуть все сразу.
     
    Последнее редактирование: 4 ноя 2022
  7. Автор темы
    Dymeth

    Dymeth Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    Dymeth
    У меня примерно так же, только ещё возможность обработки сразу нескольких элементов за тик (передаётся параметром).
    Ну, и каллбэк при завершении обязательно.
    Очень удобно, да
     
  8. imDaniX

    imDaniX Активный участник Пользователь

    Баллы:
    96
    Имя в Minecraft:
    imDaniX
    Не заметил при первом прочтении - снапшоты же уже давно существуют, как минимум с 1.8.
     
  9. Автор темы
    Dymeth

    Dymeth Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    Dymeth
    Удивительное рядом. Спасибо, поправил
     
  10. Krongss_fur

    Krongss_fur Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    Krong
    Может моё утверждение неправильное, но на версиях 1.12 и выше у игроков есть полная возможность сломать
    бедрок на такой высоте, тот же пример если с помощью ада делать порталы которые будут спавниться в бедроке тем самым разрушать бедрок, хотя может и можно сделать систему которая будет мешать такое сделать игрокам(ну есть наверное и другие способы) и если зациклиться на концепции игры, может там не будет возможности попасть к бедроку или в ад. Но я хотел сказать, что сломать бедрок в ваниле вполне возможно.
     
  11. Автор темы
    Dymeth

    Dymeth Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    Dymeth
    Всё можно, да. Но это из разряда прикрутить к телевизору микроволновку. Сделать можно, но зачем?
    Да и суть статьи не в этом, конечно же
     
  12. Автор темы
    Dymeth

    Dymeth Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    Dymeth

Поделиться этой страницей