Перейти к содержимому

От объектов к данным

Меняется. Следите: #367, #357, #323

mcrs построен на Bevy ECS (entity component system). Если вы писали плагины для Paper или Minestom, почти все, что вы знаете о Minecraft, пригодится и здесь. Отвыкать придется от трех привычек, и эта страница разбирает их по очереди.

В Paper игрок - объект. Вы держите Player, читаете его поля и вызываете методы:

for (Player p : world.getPlayers()) {
if (p.getGameMode() == GameMode.CREATIVE) {
log.info(p.getName());
}
}

В mcrs объекта игрока нет. Игрок - это Entity, а Entity - всего лишь id. Все, что сервер знает об игроке, лежит в компонентах, привязанных к этому id: в GameProfile хранятся имя и UUID, в PlayerGameMode - режим игры. С компонентами работает обычная функция, ее называют системой. Тот же цикл в виде системы выглядит так (это набросок, чтобы показать форму; в туториалах код взят из репозитория):

fn log_creative_players(players: Query<(&GameProfile, &PlayerGameMode), With<Player>>) {
for (profile, mode) in &players {
if mode.0 == GameMode::Creative {
info!("{}", profile.username);
}
}
}

Проще всего представить базу данных. Сущность - строка, компонент - колонка, а система - запрос по всем строкам, где есть нужные ей колонки. Query из примера читается почти как SELECT profile, mode FROM entities WHERE has(Player).

В таблице ниже слева то, к чему вы привыкли в Java, справа - ближайший родственник в mcrs. Это аналогии для старта. Полная таблица соответствий есть на странице Paper и Minestom в терминах mcrs.

Java ECS в mcrs
class Player extends LivingEntity id Entity с компонентами Player, GameProfile, PlayerGameMode и другими
Поле player.gameMode Компонент PlayerGameMode на этой сущности
Метод player.fly() Система, которая читает и пишет PlayerFlying
for (Player p : world.getPlayers()) Query<(&GameProfile, &PlayerGameMode), With<Player>>
Ссылка на игрока в поле класса id Entity в компоненте или ресурсе
Bukkit.getServer() Параметры системы: Res<T>, Query<..>, Commands
Метод Listener Наблюдатель (observer), функция вида fn(e: On<Event>), или система
runTaskTimer Система в FixedUpdate, она выполняется 20 раз в секунду

Данные вашего плагина живут по тому же правилу. Кладите их в компоненты и ресурсы, а если нужно запомнить игрока, сохраните id Entity и найдите игрока заново, когда он понадобится. Компоненты режима игры, полета и уровня оператора лежат в ability.rs.

В mcrs нет статической точки входа. Система перечисляет в параметрах все, что ей нужно, и Bevy передает эти значения при каждом запуске: Res<T> для общего ресурса, Query<..> для сущностей, Commands для их создания и изменения.

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

Bevy сравнивает сигнатуры ваших систем. Если две системы работают с разными данными, они выполняются одновременно, и писать для этого ничего не надо. Если две системы пишут в одни и те же данные, возникает конфликт, и вы сами указываете, какая из них идет первой, через .before() или .after(). Bevy находит такие конфликты, когда строит расписание при запуске. Компилятор Rust их не ловит.

Вызывать Bukkit.getScheduler().runTask(...), чтобы вернуться в главный поток, не придется, потому что такого потока нет. От Folia эта модель тоже отличается. Folia делит мир на регионы по координатам, а mcrs делит работу по данным, с которыми работает каждая система.

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

Демо ниже прогоняет одно и то же стадо коров дважды: в порядке ванили и в порядке mcrs.

Коровы: один тик, порядок ванили и порядок mcrs - открыть отдельно
Демо упрощает модель. Оно показывает идею и не запускает настоящий планировщик.

Одинаковый вход дает одинаковый результат

Заголовок раздела «Одинаковый вход дает одинаковый результат»

К параллельному коду обычно один вопрос: не получатся ли из двух запусков два разных мира? В mcrs результат тика не зависит от того, сколько потоков его считало. Когда две сущности претендуют на одно и то же, победителя определяет стабильный ключ, например порядок прибытия или id сущности, и расклад по потокам на исход не влияет.

Насколько строга гарантия, зависит от части игры. В проекте три уровня:2

Уровень Гарантия Примеры
Точный Результат совпадает с эталонной игрой Генерация мира, порядок блочных обновлений, арифметика движения игрока
Не зависит от планировщика Одни и те же входные данные дают один и тот же тик при любом порядке потоков Почти все остальное
Свободный Порядок ни на что не влияет, потому что общих записываемых данных нет Сущности и системы, которые пишут только в свои данные

За пределами точного уровня mcrs повторяет распределения случайных чисел эталонной игры, но сами последовательности не воспроизводит.

Для плагина из этого следует одно правило. Если одна ваша система должна выполняться раньше другой, задайте это через .before() или .after() и не полагайтесь на порядок, который случайно увидели при запуске. CI запускает расписания симуляции так, что неоднозначный порядок считается ошибкой, поэтому неупорядоченный конфликт роняет тест.3 Тесты лежат в tick_determinism.rs и schedule_ambiguity.rs.

Сервер ставит запасной обработчик ошибок в src/main.rs. Когда система паникует, обработчик пишет панику в лог, а сервер работает дальше. Это похоже на непойманное исключение в обработчике событий Bukkit.

Дальше: Rust для Java-разработчика.

  1. Работает#357 Решение о том, что глобальное изменяемое состояние пишет один владелец. ↩

  2. Работает#367 Решение, которое вводит три уровня детерминизма. ↩

  3. Работает#323 Расписания симуляции падают, если порядок систем неоднозначен. ↩

Источники и проверка

Проверено: 26.4-snapshot-7, 51d45c26a, 2026-11-09

Переведено с версии 51d45c26a

Следить: #367, #357, #323