Продвинутое использование BukkitRunnable

Руководство Продвинутое использование BukkitRunnable

Поддерживаемые версии
  1. 1.16
  2. 1.17
  3. 1.18
  4. 1.19
  5. 1.20
Примечание: хотя в поддерживаемых версиях указаны 1.16+, насколько я знаю, 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);
}
Как видите, ничего особо сложного в использовании BukkitRunnable для реализации простых задач с задержкой нет
Чуть сложнее, но всё ещё должен знать и понимать каждый
Например, я хочу. чтобы столб частиц продержался некоторое время, а потом исчез. Однако при попытке использования какого-либо счётчика вместе с runTaskTimer и лямбда выражением мы сталкиваемся с проблемами: если установить счётчик внутри лямбды
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);
то при каждом вызове i будет заново приравниваться нулю и частицы будут спавниться, пока сервер не будет выключен/перезапущен. Тогда вы скорее всего попробуете вынести счётчик за пределы Runnable:
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);
Однако вот незадача, IDE говорит что i должно быть каким-то атомным. Вы пробуете авто-исправление от вашей IDE, вроде даже что-то получается, но на самом деле это всё те же костыли. Решение же было простым:
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);
Однако теперь IDE показывает метод runTaskTimer(Plugin, BukkitRunnable, int, int) как устаревший. Теперь нам не нужно просить планировщик вызвать BukkitRunnable, а обратится к самой задаче:
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:
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;
    }
}
Теперь мы можем пользоваться public методами GameRunnable из любой точки кода
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);
    }
}
Главное не забывать регистрировать листенер при создании/условии и выключать, чтобы не засорять шину событий

На этом статья подходит к концу, хотя возможно она ещё будет дополнятся. Буду рад фидбеку/вопросам
  • Мне нравится (+1)
Реакции: Kelsi
Автор
Hyrancood
Просмотры
1 618
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок

Поделиться ресурсом

Назад
Сверху Снизу