Стартап BukkitScheduler - таймеры, кулдауны, потоки

imDaniX

Активный участник
Пользователь
Баллы
98
Ник в Minecraft
imDaniX
Что едим?​
Или, проще говоря, что такое BukkitScheduler и как им пользоваться.
BukkitScheduler (далее - просто шедулер) по сути, выполняет две роли: во-первых, позволяет разграничивать ваши задачи по времени; во-вторых, позволяет назначать задачи как на основной поток (в котором выполняется основная логика игры), так и на асинхронный.

Ещё, в предисловии хочу упомянуть - для работы с шедулером вам понадобится экземпляр вашего плагина. Для его получения многие из вас наверняка знакомы со статичным синглтоном (static singleton) - этот пресловутый getInstance(). Просто скажу, что так делать не стоит - это ломает структуру проекта, и зачастую очень негативно влияет на неё при расширении. Вместо этого, посмотрите в сторону простого внедрение зависимости (dependency injection). Пример использования есть в послесловии.

С чем едим?​
В сущности, шедулер имеет три метода принимающих Runnable, которые вы так или иначе будете использовать, плюс их асинхронные варианты, и на каждый прибавляется вариант с Consumer<BukkitTask>.
Для чуть лучшего понимания, если не знакомы с Runnable - это интерфейс, представляющий из себя лишь один метод run(). Когда мы передаем его в шедулер, тот позже просто выполняет этот метод. Посмотрим на небольшом примере:
PHP:
Server server = plugin.getServer();
Runnable action = () -> {
    server.broadcastMessage("Это сообщение будет отправлено вторым!");
    server.broadcastMessage("Это сообщение будет отправлено третьим!");
};
server.broadcastMessage("Это сообщение будет отправлено первым!");
action.run();
Соответственно, в чате мы сначала увидим "Это сообщение будет отправлено первым!", и лишь потом "...вторым!" и "...третьим!". То есть, Runnable это некий код, который мы можем вызвать в любой момент отдельно. Таким образом и работает BukkitScheduler - он вызывает наш код, когда приходит его время.

В этом туториале в качестве Runnable и Consumer<BukkitTask> как правило будут использоваться лямбда-выражения - если очень кратко, это микро-классы, которые имеют лишь один метод, и при написании видно лишь его аргументы в круглых скобках. Пример уже был выше - так как у Runnable#run() нет аргументов вовсе, скобки вообще пустые. У Consumer<BukkitTask> там будет один аргумент с типом BukkitTask. Если есть проблемы с английским языком, или материал попросту непонятен, можете погуглить на русском - статей, благо, полно.

Пометка: в одной секунде обычно 20 тиков, соответственно, 1 тик ~= 50 миллисекунд. При лагах сервера это значение может увеличиваться. Мы будем изначально ставить время в секундах, и просто умножать их на 20L (L значит тип long вместо int по умолчанию). Для лучшего понимания время перемещено в отдельные переменные, но никто не мешает использовать числа на месте.

runTask(Plugin plugin, Runnable task) - запустит задачу Runnable на следующем тике сервера.
PHP:
Server server = plugin.getServer();
BukkitTask task = server.getScheduler().runTask(plugin, () -> {
    server.broadcastMessage("Это сообщение покажется примерно через 50мс!");
    // TODO: Остальной код
});


runTaskLater(Plugin plugin, Runnable task, long delay) - запустит задачу Runnable через указанное в параметре delay количество тиков.
PHP:
Server server = plugin.getServer();
int delay = 5;
BukkitTask task = server.getScheduler().runTaskLater(plugin, () -> {
    server.broadcastMessage("Это сообщение покажется примерно через 5 секунд!");
    // TODO: Остальной код
}, delay * 20L);


runTaskTimer(Plugin plugin, Runnable task, long delay, long period) - запустит задачу Runnable через указанное в параметре delay количество тиков, повторяя её раз в period тиков.
PHP:
Server server = plugin.getServer();
int delay = 5;
int period = 42;
BukkitTask task = server.getScheduler().runTaskTimer(plugin, () -> {
    server.broadcastMessage("Это сообщение покажется примерно через 5 секунд и будет повторяться каждые 42 секунды!");
    // TODO: Остальной код
}, delay * 20L, period * 20L);


Пометка: совсем не обязательно использовать Server server = plugin.getServer(); - эту строку можно стереть, и вместо переменной server просто использовать класс Bukkit. Здесь оно используется в рамках "лучшей практики", но так как BukkitAPI сам предлагаем вам все методы Server, дело за вами. Сохранять переменную BukkitTask тоже не требуется. То есть, при самом минимуме, последний пример может выглядеть и так:
PHP:
Bukkit.getScheduler().runTaskLater(plugin, () -> {
    Bukkit.broadcastMessage("Это сообщение покажется примерно через 5 секунд и будет повторяться каждые 42 секунды!");
    // TODO: Остальной код
}, 100L, 840L);


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

Все упомянутые методы возвращают BukkitTask. На практике, его основная роль - остановка выполнения задачи. Например, если вы хотите в какой-то момент остановить выполнение runTaskTimer, достаточно сохранить его в переменную и просто использовать task.cancel(). Если вам не требуется это, можете, соответственно, просто не сохранять значение в переменную.
Наконец, если вы хотите остановить задачу в какой-то момент, но не хотите сохранять BukkitTask, воспользуйтесь вариантом методов с Consumer<BukkitTask> - он предоставляет экземпляр BukkitTask непосредственно в вашу задачу, из которой его можно остановить. Методы с ним называются точно также, меняется только второй аргумент.
PHP:
server.getScheduler().runTaskTimer(plugin, (task) -> {
    server.broadcast("Сообщение будет какое-то время повторяться примерно раз в секунду.")
    if (isTimeToStop()) {
        task.cancel();
    }
}, 0L, 20L)
Подразумевается, что метод isTimeToStop() возвращает true, когда задача должна быть остановлена.

Как едим?​
Посмотрим на сценарии, приближенные к реальности.
Например, задача такова: телепортировать игрока, если он просит об этом в чате. AsyncPlayerChatEvent как правило проходит в асинхронном потоке - значит взаимодействовать с миром просто так нельзя, а то получим ошибку.
PHP:
private final Plugin plugin; // Наш плагин
private final Location spawn; // Локация спавна

@EventHandler
public void onChat(AsyncPlayerChatEvent event) {
    if (event.getMessage().contains("Хочу на спавн")) {
        plugin.getServer().getScheduler().runTask(plugin, () -> {
            if (player.isOnline()) player.teleport(spawn);
        });
    }
}
Допустим, задача: игрок может ставить один блок раз в минуту. При этом хотим показывать отсчет до следующей установки. В рамках примера используется Paper вместо Spigot/Bukkit, просто потому что там есть более удобный метод Player#sendActionBar
PHP:
private static final int COOLDOWN_VALUE = 60; // Наш кулдаун в секундах
private final Plugin plugin; // Наш плагин
private final Map<UUID, Integer> timers = new HashMap<>();

@EventHandler
public void onPlace(BlockPlaceEvent event) { // При установке блока
    Player player = event.getPlayer();
    UUID id = player.getUniqueId();
    if (timers.containsKey(id)) { // Проверяем наличие кулдауна
        event.setCancelled(true);
        player.sendMessage(ChatColor.RED + "Вы не можете ставить блоки ещё " + timers.get(id) + " секунд.")
    } else {
        timers.put(id, COOLDOWN_VALUE); // Иначе добавляем
    }
}

public void startTimer() {
    Server server = plugin.getServer();
    server.getScheduler().runTaskTimer(plugin, () -> {
        Iterator<Map.Entry<UUID, Integer>> iterator = timers.entrySet().iterator(); // Проходимся по всем значениям в Map'е
        while (iterator.hasNext()) {
            Map.Entry<UUID, Integer> entry = iterator.next();
            Player player = server.getPlayer(entry.getKey());
            if (player != null) { // Считаем, только если игрок онлайн
                int cooldown = entry.getValue() - 1;
                if (cooldown > 0) {
                    entry.setValue(cooldown);
                    player.sendActionBar("До следующей установки блока " + cooldown + " секунд");
                } else {
                    iterator.remove();
                    player.sendActionBar("Вы можете поставить новый блок!");
                }
            }
        }
    }, 20L, 20L);
}
Подразумевается, что метод startTimer() будет вызван при или сразу после создания экземпляра класса.
Учите - если вам не нужно постоянно знать кулдаун игрока, или делать что-то активно по его истечению, то скорее всего вам не понадобится шедулер. Загляните в послесловие.
Задача: получить сохраненные в БД сообщения игроку по команде. Для простоты получения игрока в данном примере используется pattern matching, доступный с Java 14.
PHP:
private final Plugin plugin; // Наш плагин
private final Database db; // Некая база данных

@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
    if (!(sender instanceof Player player)) {
        sender.sendMessage("Данная команда доступна только игрокам!");
        return true;
    }
    BukkitScheduler scheduler = plugin.getServer().getScheduler();
    scheduler.runTaskAsynchronously(plugin, () -> { // Запустим задачу в асинхронном потоке
        player.sendMessage("Загружаем вашу почту...");
        List<String> messages = db.loadMessages(player);
        scheduler.runTask(plugin, () -> { // Вернемся в основной поток
            if (!player.isOnline()) return; // Если игрок офлайн, отправлять данные некому
            player.sendMessage("У вас " + messages.size() + " писем.")
            messages.forEach(player::sendMessage);
        });
    });
    return true;
}
Подразумевается, что Database#loadMessages возвращает список сообщений, отправленных игроку.
Данные отрывки кода - лишь примеры применения шедулера, и не должны быть использованы как есть. Места для оптимизации и улучшения здесь полно, но для показа работы с шедулером - пойдет. Буду рад добавить и ваши примеры, если они будут достаточно показательны.

Весь код писался от руки и может содержать ошибки. Если кто-чего заметит - буду рад исправить.
 
Последнее редактирование:
Есть ещё вот такая штука для игрищ с CompletableFuture, но её пока не тестировал. Но по логике должно работать корректно:
PHP:
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitScheduler;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;

// Author: Dymeth.Ru
public class CompletableFutureBukkit<T> extends CompletableFuture<T> {
    private final Executor defaultExecutor;

    public CompletableFutureBukkit(Plugin plugin, boolean async) {
        BukkitScheduler scheduler = plugin.getServer().getScheduler();
        this.defaultExecutor = async
                ? actions -> scheduler.runTaskAsynchronously(plugin, actions)
                : actions -> scheduler.runTask(plugin, actions);
    }

    @Override
    public Executor defaultExecutor() {
        return this.defaultExecutor;
    }
}

null-safety аннотации удалены

Преимущество перед обычным CompletableFuture в том, что такие таски должны останавливаться ядром при выгрузке плагина
А когда это может пригодится?
 
А когда это может пригодится?
Ну у себя бы я всё на них перевёл, пожалуй. Ток протестить сначала надо, корректно ли оно работает, а то как херанет в крафтшедулер 100500 потоков ))
 
Ну у себя бы я всё на них перевёл, пожалуй. Ток протестить сначала надо, корректно ли оно работает, а то как херанет в крафтшедулер 100500 потоков ))
Оно не так работает, там под капотом просто лист тасков и 2 потока основной и асинк.
 
Оно не так работает, там под капотом просто лист тасков и 2 потока основной и асинк.
Оно может так сработать, потому что размер пула не ограничен в крафтшедулере. У меня же в некоторых задачах он был фиксированного размера. Так что в любом случае пока с опаской использовать
 
Назад
Верх Низ