Разработка Java-серверов на Paper · 6 мин чтения

Состояния мини-игры: один результат от лобби до очистки

Опишите переходы раунда как маленькую модель и испытайте её без Minecraft. Пример Java 21 ниже показывает завершение один раз и отказ старого таймера; после этого подключайте к модели события и задачи Paper.

Редакция LonelyCraft · Обновлено

Схема: Состояние → Событие → Завершение
Состояние и идентификатор связывают события с одним завершением и очисткой. Авторская схема LonelyCraft.

Сначала зафиксируйте допустимые переходы

Учебная модель использует WAITING → RUNNING → FINISHING → WAITING. Новый runId появляется при старте и сохраняется у каждого таймера. Для реальной арены добавьте PREPARING/CLEANUP, когда требуется дождаться строительства или возврата игроков: эти переходы должны подтверждаться отдельным результатом.

Создайте пустую папку round-demo, установите Temurin JDK 21 и проверьте javac -version. Код ниже не зависит от Bukkit, не создаёт мир и не выдаёт награды; его задача — проверить принятие события по владельцу и состоянию.

Сохраните и запустите рабочую модель

Сохраните файл RoundDemo.java без package. В терминале этой папки выполните javac -encoding UTF-8 RoundDemo.java, затем java RoundDemo. Команды одинаковы в Windows PowerShell и Linux/macOS. Синтаксис запуска/компиляции есть в документации javac.

import java.util.UUID;

public final class RoundDemo {
    enum State { WAITING, RUNNING, FINISHING }
    private State state = State.WAITING;
    private UUID runId;

    UUID start() {
        if (state != State.WAITING) throw new IllegalStateException("Раунд уже занят");
        runId = UUID.randomUUID();
        state = State.RUNNING;
        return runId;
    }
    boolean finish(UUID expected) {
        if (state != State.RUNNING || !expected.equals(runId)) return false;
        state = State.FINISHING;
        return true;
    }
    void cleanup(UUID expected) {
        if (state != State.FINISHING || !expected.equals(runId))
            throw new IllegalStateException("Не тот этап очистки");
        state = State.WAITING;
        runId = null;
    }
    public static void main(String[] args) {
        RoundDemo round = new RoundDemo();
        UUID first = round.start();
        System.out.println("Первый финиш: " + round.finish(first));
        System.out.println("Повторный финиш: " + round.finish(first));
        round.cleanup(first);
        UUID second = round.start();
        System.out.println("Старый таймер: " + round.finish(first));
        System.out.println("Новый финиш: " + round.finish(second));
        round.cleanup(second);
    }
}

Сравните вывод с ожидаемым порядком

Должны получиться четыре результата: true, false, false, true. Первый закрывает приём результатов, второй отклоняет повтор той же сессии, третий отклоняет таймер прежнего раунда, четвёртый завершает актуальный. Между первым и новым стартом обязательно вызывается cleanup.

Для проверки запрета старта добавьте второй round.start() сразу после первого: программа должна остановиться с IllegalStateException, пока арена занята. Для поздней очистки вызов cleanup(first) после старта второго раунда тоже обязан отклоняться. Эти случаи проверяют последствия переходов, а не только enum.

Подключите модель к Paper без гонки потоков

В Paper вызывайте переходы и игровые API в основном потоке; таймер получает захваченный UUID и проверяет его перед действием. Создание/отмена задач описаны в Paper Scheduling. Async-запрос базы возвращает результат через main-задачу и сверяет тот же runId.

FINISHING сначала сохраняет результат и намерение награды, затем возвращает игрока и очищает принадлежащие сессии ресурсы. Возврат true в нашем примере ещё не является устойчивой записью: для рестарта нужен журнал. Если очистка не удалась, слот остаётся занятым и новый старт запрещён до завершения компенсации.

Опишите переходы таблицей, прежде чем писать таймеры

Для каждого состояния перечислите вход, выход и допустимые события. В WAITING можно присоединяться; в PREPARING проверяется набор участников и готовность арены; в RUNNING считается игровой результат; в FINISHING результат уже закрыт; в CLEANUP освобождаются ресурсы. Названия условные, но их смысл должен быть однозначным во всех обработчиках.

У перехода есть причина: заполнено лобби, истёк таймер, выполнена цель, погиб игрок или выключается сервер. Вместе с состоянием храните runId либо поколение. Тогда задача отсчёта, запущенная для старого раунда, не может случайно начать новый. Проверка «игра активна» слишком общая: активной уже может быть другая сессия.

Пример двух почти одновременных событий

Игрок закончил задачу и сразу отключился. Обработчик успеха переводит RUNNING в FINISHING, а обработчик выхода пытается завершить ту же сессию. Вторая операция должна увидеть принятое решение и выполнить только предусмотренную очистку. Она не меняет победу на отказ и не выдаёт вторую награду. Правило приоритетов причин определите до реализации.

Несколько событий одного действия не должны создавать несколько решений раунда. Перевод событий в результат.

Сделайте вход в игру отдельной операцией

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

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

Снимок исходного игрока должен переживать сбой между очисткой и завершением игры. Сохранение и возврат вещей.

Завершение разбейте на подтверждённые этапы

Сначала закройте приём новых игровых действий и зафиксируйте причину. Затем запишите результат и намерение награды. После этого возвращайте игрока и очищайте принадлежащие сессии сущности, блоки и задачи. Если награда внешняя, её журнал может завершаться отдельно: очистка мира не должна стирать незавершённое начисление.

У таймеров сохраняйте владельца и отменяйте их при выходе из состояния. Однако отмена сама по себе не заменяет проверку поколения внутри callback: уже запланированный вызов может успеть начать выполнение. Две защиты описывают разные границы. Та же проверка нужна для асинхронного ответа базы или построителя комнаты.

  1. Переход в FINISHING принимается только один раз.
  2. Поздние удары и таймеры больше не меняют счёт.
  3. Результат получает устойчивый идентификатор.
  4. Возвращается правильное исходное состояние игрока.
  5. Удаляются только ресурсы завершённой сессии.
  6. Слот становится доступным после подтверждённой очистки.

Пауза отличается от окончательного выхода

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

Для отключения определите срок и место повторного входа. Не оставляйте игрока в мире арены без активной сессии. Либо восстановите документированное состояние, либо завершите его штатно и верните на спавн. «Всё лежит в HashMap» не является решением для рестарта и аварии процесса.

Проверяйте последовательности, а не только победу

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

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

Частые вопросы

Зачем runId, если есть флаг active?

Флаг не отличает старый раунд от нового. Идентификатор или поколение позволяет отклонить поздний callback, относящийся к уже завершённой сессии.

Можно ли обрабатывать смерть и выход отдельно?

Причины различаются, но решение завершения и награда должны иметь один согласованный вход. Иначе два обработчика способны принять противоречивые результаты.

Нужно ли сохранять каждую мини-игру после рестарта?

Нет, это зависит от правил. Но выбранное завершение или продолжение должно быть явным, а игрок и награды — восстановлены предсказуемо.

Когда освобождать слот арены?

После безопасного возврата игрока и подтверждённой очистки ресурсов сессии. Раннее освобождение может смешать старый и новый раунды.

Просмотры: … · Один просмотр с браузера за день

Было полезно?Общий счёт: …

Одна реакция с браузера. Нажмите повторно, чтобы снять её, или выберите другую.

Источники и документация

Minecraftразработка плагиновPaper

Читайте дальше