- Поддерживаемые версии
- 1.16
- 1.17
- 1.18
- 1.19
- 1.20
Примечание: хотя в поддерживаемых версиях указаны 1.16+, насколько я знаю, BukkitRunnable работает аналогичным образом и на более ранних версиях, но утверждать этого не могу.
В этой статье я постараюсь показать все возможные способы использования BukkitRunnable при разработке плагинов, идя от простого к сложному. Без лишних слов, приступим.
Базовый уровень
Чуть сложнее, но всё ещё должен знать и понимать каждый
Продвинутый уровень
На этом статья подходит к концу, хотя возможно она ещё будет дополнятся. Буду рад фидбеку/вопросам
В этой статье я постараюсь показать все возможные способы использования BukkitRunnable при разработке плагинов, идя от простого к сложному. Без лишних слов, приступим.
Базовый уровень
Для начала зарегистрируем основной класс плагина и класс с событиями:
Теперь можно переходить непосредственно к использованию планировщика событий. Например, я хочу, чтобы после нажатия ПКМ по блоку, над ним появлялись частицы:
Однако с помощью
можно использовать не один метод, а множество, с использованием ветвлений и т.п.:
Как видите, ничего особо сложного в использовании BukkitRunnable для реализации простых задач с задержкой нет
Java:
public final class MainPluginClass extends JavaPlugin {
public static MainPluginClass instance;
public static MainPluginClass getInstance() {
return instance;
}
@Override
public void onEnable() {
instance = this;
Bukkit.getPluginManager().registerEvents(new Events(), this);
}
@Override
public void onDisable() {
Bukkit.getScheduler().cancelTasks(instance);
}
}
Java:
public class Events implements Listener {
@EventHandler
public void onBlockRMC(PlayerInteractEvent event) {
Block block = event.getClickedBlock();
if (block == null || !block.isSolid()) return;
}
}
Java:
@EventHandler
public void onBlockRMC(PlayerInteractEvent event) {
Block block = event.getClickedBlock();
if (block == null || !block.isSolid()) return;
World world = block.getWorld();
Location location = world.getHighestBlockAt(block.getLocation()).getLocation().add(0, 1, 0);
Bukkit.getScheduler().runTaskLater(MainPluginClass.getInstance(), () -> world.spawnParticle(Particle.CRIT, location, 8), 50);
}
() -> someMethod()
Java:
@EventHandler
public void onBlockRMC(PlayerInteractEvent event) {
Block block = event.getClickedBlock();
if (block == null || !block.isSolid()) return;
World world = block.getWorld();
Location location = world.getHighestBlockAt(block.getLocation()).getLocation().add(0, 1, 0);
//Вызываем столб частиц над местом, по которому нажали ПКМ с задержкой в 50 тиков
Bukkit.getScheduler().runTaskLater(MainPluginClass.getInstance(), () -> {
for (int y = 0; y < 10; y++) {
world.spawnParticle(Particle.CRIT, location.clone().add(0, y, 0), 8);
}
}, 50);
}
Например, я хочу. чтобы столб частиц продержался некоторое время, а потом исчез. Однако при попытке использования какого-либо счётчика вместе с runTaskTimer и лямбда выражением мы сталкиваемся с проблемами: если установить счётчик внутри лямбды
то при каждом вызове i будет заново приравниваться нулю и частицы будут спавниться, пока сервер не будет выключен/перезапущен. Тогда вы скорее всего попробуете вынести счётчик за пределы Runnable:
Однако вот незадача, IDE говорит что i должно быть каким-то атомным. Вы пробуете авто-исправление от вашей IDE, вроде даже что-то получается, но на самом деле это всё те же костыли. Решение же было простым:
И правда, ничего сложного - просто попросить планировщик отменить задачу по её айди через какое-то время.
Однако, что делать, если захотелось установить переменное условие для остановки повторяющейся задачи? Придётся отказаться от лямбда выражения, однако это откроет много новых возможностей. Для примера будем создавать некий снаряд из партиклов, летящий в одном направлении, пока не встретит плотный блок на своём пути:
Однако теперь IDE показывает метод runTaskTimer(Plugin, BukkitRunnable, int, int) как устаревший. Теперь нам не нужно просить планировщик вызвать BukkitRunnable, а обратится к самой задаче:
Помимо этого мы можем перезаписать метод cancel():
Однако нам не обязательно вызывать задачу сразу. Мы можем вынести её, например, в переменную и вызывать в другом событии:
Как видите, даже используя не самые сложные конструкции, можно решать даже достаточно сложные задачи
Java:
Bukkit.getScheduler().runTaskTimer(MainPluginClass.getInstance(), () -> {
int i = 0;
if (i < 10) {
for (int y = 0; y < 10; y++) {
world.spawnParticle(Particle.CRIT, location.clone().add(0, y, 0), 8);
}
i++;
}
}, 50, 5);
Java:
int i = 0;
Bukkit.getScheduler().runTaskTimer(MainPluginClass.getInstance(), () -> {
if (i < 10) {
for (int y = 0; y < 10; y++) {
world.spawnParticle(Particle.CRIT, location.clone().add(0, y, 0), 8);
}
i++;
}
}, 50, 5);
Java:
int id = Bukkit.getScheduler().runTaskTimer(MainPluginClass.getInstance(), () -> {
for (int y = 0; y < 10; y++) {
world.spawnParticle(Particle.CRIT, location.clone().add(0, y, 0), 8);
}
}, 50, 5).getTaskId();
Bukkit.getScheduler().runTaskLater(MainPluginClass.getInstance(), () -> Bukkit.getScheduler().cancelTask(id), 90);
Однако, что делать, если захотелось установить переменное условие для остановки повторяющейся задачи? Придётся отказаться от лямбда выражения, однако это откроет много новых возможностей. Для примера будем создавать некий снаряд из партиклов, летящий в одном направлении, пока не встретит плотный блок на своём пути:
Java:
Bukkit.getScheduler().runTaskTimer(MainPluginClass.getInstance(), new BukkitRunnable() {
final Location loc = location;
@Override
public void run() {
if (loc.getBlock().isSolid()) this.cancel();
world.spawnParticle(Particle.CRIT, loc, 8, 0.5, 0.5, 0.5);
loc.add(1, 0, 0);
}
}, 50, 5);
Java:
new BukkitRunnable() {
final Location loc = location;
@Override
public void run() {
if (loc.getBlock().isSolid()) this.cancel();
world.spawnParticle(Particle.CRIT, loc, 8, 0.5, 0.5, 0.5);
loc.add(1, 0, 0);
}
}.runTaskTimer(MainPluginClass.getInstance(), 50, 5);
Помимо этого мы можем перезаписать метод cancel():
Java:
new BukkitRunnable() {
final Location loc = location;
final int max = 64;
@Override
public void run() {
if (loc.getBlock().isSolid() || loc.distance(location) > max) this.cancel();
world.spawnParticle(Particle.CRIT, loc, 8, 0.5, 0.5, 0.5);
loc.add(1, 0, 0);
}
@Override
public void cancel() {
super.cancel();
//Для примера будем вызывать большое количество партиклов и наносить урон игрокам вокруг
world.spawnParticle(Particle.CRIT, loc, 50, 2, 2, 2);
for (Player player: loc.getNearbyPlayers(2)) {
player.damage(5, event.getPlayer());
}
}
}}.runTaskTimer(MainPluginClass.getInstance(), 50, 5);
Однако нам не обязательно вызывать задачу сразу. Мы можем вынести её, например, в переменную и вызывать в другом событии:
Java:
public class Events implements Listener {
public BukkitRunnable task;
@EventHandler
public void onBlockRMC(PlayerInteractEvent event) {
Block block = event.getClickedBlock();
if (block == null || !block.isSolid()) return;
World world = block.getWorld();
Location location = world.getHighestBlockAt(block.getLocation()).getLocation().add(0, 1, 0);
this.task = new BukkitRunnable() {
final Location loc = location;
final int max = 64;
@Override
public void run() {
if (loc.getBlock().isSolid() || loc.distance(location) > max) this.cancel();
world.spawnParticle(Particle.CRIT, loc, 8, 0.5, 0.5, 0.5);
loc.add(1, 0, 0);
}
@Override
public void cancel() {
super.cancel();
world.spawnParticle(Particle.CRIT, loc, 8, 2, 2, 2);
for (Player player: loc.getNearbyPlayers(2)) {
player.damage(5, event.getPlayer());
}
}
};
}
@EventHandler
public void onEntityDeath(EntityDeathEvent event) {
if (event.getEntity() instanceof Player) {
if (this.task == null) return;
this.task.runTaskTimer(MainPluginClass.getInstance(), 50, 5);
}
}
}
Как видите, даже используя не самые сложные конструкции, можно решать даже достаточно сложные задачи
Если же вам нужно написать что-то на уровень выше, что должно меняться от постоянных действий игроков, и к чему по-хорошему у вас должен быть доступ из любой точки кода плагина, например главный поток мини-игры, то стоит задуматься о вынесении задачи в отдельный класс посредством наследования от BukkitRunnable:
Создадим новую задачу при инициализации плагина и добавим геттер:
Теперь мы можем пользоваться public методами GameRunnable из любой точки кода
Другой пример. Мы хотим что-то делать с игроком при выполненных условиях. Здесь мы тоже можем использовать наследование от BukkitRunnable, в данном случае чтобы сделать класс-обёртку игрока:
Мы даже можем сделать этот класс листенером и слушать внутри него события:
Главное не забывать регистрировать листенер при создании/условии и выключать, чтобы не засорять шину событий
Java:
public class GameRunnable extends BukkitRunnable {
public static final int TIMER = 6000;
int time = TIMER;
private final Location arena;
public GameRunnable(Location location) {
this.arena = location;
this.runTaskTimer(MainPluginClass.getInstance(), 1, 1);
}
@Override
public void run() {
Collection<? extends Player> players = Bukkit.getOnlinePlayers();
if (players.size() > 2) {
this.time--;
if (this.time % 100 == 0) {
if (this.time > 0) for (Player player: players) announcement(player);
else startGame(players);
}
} else if (this.time < TIMER) this.time = TIMER;
}
private void startGame(Collection<? extends Player> players) {
for (Player player: players) {
player.teleport(this.arena);
this.arena.getWorld().spawnParticle(Particle.CRIT, this.arena.clone().add(0, 0.5, 0), 30, 1, 1, 1);
}
}
private void announcement(Player player) {
player.sendTitle(String.format(ChatColor.GOLD + "Игра начнётся через %s секунд", this.time/20), "", 2, 20, 2);
player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.5f);
if (smth(player)) doSmth();
}
private boolean smth(Player p) {
boolean result = true;
// какая-то логика
return result;
}
private void doSmth() {
// что-то делаем
}
//Например, в нашей игре можно сломать какое-либо ядро команды противника
public void breakAnnouncement(Player player) {
String announcement = ChatColor.RED + String.format("Ядро было сломано игроком %s!", player.getName());
for (Player p: Bukkit.getOnlinePlayers()) {
p.sendTitle(announcement, "", 2, 16, 2);
}
Bukkit.getServer().sendMessage(Component.text(announcement));
player.sendMessage("Вам начислено 25 монет");
}
}
Java:
public final class MainPluginClass extends JavaPlugin {
public static MainPluginClass instance;
private GameRunnable game;
public static MainPluginClass getInstance() {
return instance;
}
@Override
public void onEnable() {
instance = this;
Bukkit.getPluginManager().registerEvents(new Events(), this);
instance.game = new GameRunnable(new Location(Bukkit.getWorlds().get(0), 0d, 60d, 0d));
}
@Override
public void onDisable() {
Bukkit.getScheduler().cancelTasks(instance);
}
public static GameRunnable getGame() {
return instance.game;
}
}
Java:
public class Events implements Listener {
@EventHandler
public void onCoreBreak(BlockBreakEvent event) {
final Material core = Material.REDSTONE_BLOCK;
if (event.getBlock().getType() == core) {
MainPluginClass.getGame().breakAnnouncement(event.getPlayer());
}
}
}
Другой пример. Мы хотим что-то делать с игроком при выполненных условиях. Здесь мы тоже можем использовать наследование от BukkitRunnable, в данном случае чтобы сделать класс-обёртку игрока:
Java:
public class PlayerRunnable extends BukkitRunnable {
private static final int UPDATE_PER_SECOND = 5;
private final Player player;
private int abilityTimer;
private static final int ABILITY_COOLDOWN = 600;
public PlayerRunnable(Player p) {
this.player = p;
this.abilityTimer = 0;
this.runTaskTimer(MainPluginClass.getInstance(), 40, 20/UPDATE_PER_SECOND);
}
@Override
public void run() {
if (this.player.getHealth() < this.player.getHealthScale()/2) {
Location location = this.player.getLocation();
location.getWorld().spawnParticle(Particle.CRIT_MAGIC, location.clone().add(0, 2.2d, 0), 20, 0.5, 0.1, 0.5);
}
if (smth()) {
doSmth();
//....
}
//...
//Ещё много классных вещей
//...
if (this.abilityTimer > 0) this.abilityTimer = Math.max(0, this.abilityTimer - UPDATE_PER_SECOND);
if (this.player.getHealth() < 3 && this.abilityTimer == 0) {
this.abilityTimer = ABILITY_COOLDOWN;
}
}
}
Java:
public class PlayerRunnable extends BukkitRunnable implements Listener {
private static final int UPDATE_PER_SECOND = 5;
private final Player player;
private int abilityTimer;
private static final int ABILITY_COOLDOWN = 600;
public PlayerRunnable(Player p) {
this.player = p;
this.abilityTimer = 0;
this.runTaskTimer(MainPluginClass.getInstance(), 40, 20/UPDATE_PER_SECOND);
Bukkit.getPluginManager().registerEvents(this, MainPluginClass.getInstance());
}
@Override
public void run() {
if (this.player.getHealth() < this.player.getHealthScale()/2) {
Location location = this.player.getLocation();
location.getWorld().spawnParticle(Particle.CRIT_MAGIC, location.clone().add(0, 2.2d, 0), 20, 0.5, 0.1, 0.5);
}
if (smth()) {
doSmth();
//....
}
//...
//Ещё много классных вещей
//...
if (this.abilityTimer > 0) this.abilityTimer = Math.max(0, this.abilityTimer - UPDATE_PER_SECOND);
if (this.player.getHealth() < 3 && this.abilityTimer == 0) {
this.abilityTimer = ABILITY_COOLDOWN;
}
}
private boolean smth() {
boolean result = true;
// какая-то логика
// if (this.player ....) ...
return result;
}
private void doSmth() {
// что-то делаем
}
@EventHandler
public void onDeath(PlayerDeathEvent event) {
if (event.getEntity() != this.player) return;
this.player.kick(Component.text("Слабакам здесь не место!"));
HandlerList.unregisterAll(this);
}
@EventHandler
public void onLeave(PlayerQuitEvent event) {
if (event.getPlayer() != this.player) return;
HandlerList.unregisterAll(this);
}
}
На этом статья подходит к концу, хотя возможно она ещё будет дополнятся. Буду рад фидбеку/вопросам