- Баллы
- 96
- Имя в 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 и 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)
Как едим?
Посмотрим на сценарии, приближенные к реальности.Например, задача такова: телепортировать игрока, если он просит об этом в чате. 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
Подразумевается, что метод startTimer() будет вызван при или сразу после создания экземпляра класса.
Учите - если вам не нужно постоянно знать кулдаун игрока, или делать что-то активно по его истечению, то скорее всего вам не понадобится шедулер. Загляните в послесловие.
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);
}
Учите - если вам не нужно постоянно знать кулдаун игрока, или делать что-то активно по его истечению, то скорее всего вам не понадобится шедулер. Загляните в послесловие.
Задача: получить сохраненные в БД сообщения игроку по команде. Для простоты получения игрока в данном примере используется pattern matching, доступный с Java 14.
Подразумевается, что Database#loadMessages возвращает список сообщений, отправленных игроку.
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;
}
Весь код писался от руки и может содержать ошибки. Если кто-чего заметит - буду рад исправить.
Последнее редактирование:
