Создание 2D-игры на Rust с помощью фреймворка Bevy

Практический курс по разработке 2D-игр на языке Rust с использованием современного движка Bevy. Вы изучите паттерн ECS, работу со спрайтами, обработку ввода и создание полноценной игровой логики.

1. Введение в движок Bevy и основы архитектуры ECS

Введение в движок Bevy и основы архитектуры ECS

Современная разработка игр требует высокой производительности и гибкости архитектуры. Традиционные подходы часто приводят к запутанному коду, который сложно поддерживать и масштабировать. Игровой движок Bevy, написанный на языке программирования Rust, предлагает элегантное решение этой проблемы через строгую реализацию паттерна ECS (Entity-Component-System).

Движок Bevy выделяется на фоне конкурентов своей ориентацией на данные (Data-Oriented Design) и максимальным использованием возможностей компилятора Rust для безопасного параллельного выполнения кода. Это делает его одним из самых перспективных инструментов для создания как 2D, так и 3D проектов.

> Entity-Component-System (ECS) — это архитектурный шаблон, который позволяет разделить данные, поведение и логику управления в отдельные части, что делает код более модульным и гибким. > > tezee.art

Ключевые особенности фреймворка Bevy: * Строгая типизация и безопасность памяти благодаря языку Rust * Встроенная параллелизация вычислений "из коробки" * Отсутствие скрытого глобального состояния * Модульная архитектура, позволяющая отключать ненужные компоненты движка (например, 3D-рендеринг при создании исключительно 2D-игры)

Проблема классического объектно-ориентированного подхода

Исторически игры создавались с использованием объектно-ориентированного программирования (ООП). Разработчики строили глубокие иерархии наследования: базовый класс GameObject, от него наследуется Character, затем Player и Enemy.

С ростом сложности игры такая структура становится хрупкой. Если необходимо создать врага, который может летать и одновременно использовать инвентарь, возникает проблема множественного наследования или дублирования кода. Разработчику приходится переписывать базовые классы, что ломает логику других объектов.

Для наглядности сравним два подхода:

| Характеристика | ООП (Наследование) | ECS (Композиция) | | --- | --- | --- | | Основа архитектуры | Объекты, совмещающие данные и методы | Разделение данных (компоненты) и логики (системы) | | Добавление новых свойств | Создание новых подклассов | Добавление компонентов к сущности | | Расположение в памяти | Фрагментировано (указатели на объекты) | Непрерывные массивы данных (Cache-friendly) | | Параллелизм | Сложен из-за гонок данных (Data Races) | Прост, так как системы декларируют доступ к данным |

Пример из практики: в классическом ООП-движке обновление 10 000 объектов требует вызова метода Update() для каждого из них. Если каждый объект разбросан по оперативной памяти, процессор тратит огромное количество тактов на поиск данных. В ECS данные хранятся плотными массивами, что ускоряет обработку в десятки раз.

Три кита архитектуры ECS

Аббревиатура ECS расшифровывается как Сущность (Entity), Компонент (Component) и Система (System). Разберем каждый элемент подробно, чтобы понять, как они взаимодействуют внутри Bevy.

Сущность (Entity)

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

Если на экране находится главный герой, дерево и враг — всё это сущности. В Bevy сущность представлена легковесной структурой, состоящей из числового ID и поколения (generation), чтобы безопасно переиспользовать удаленные идентификаторы. Сама по себе сущность ничего не делает, пока к ней не прикрепят компоненты.

Компонент (Component)

Компонент — это чистые данные, привязанные к сущности. Компоненты не содержат методов или игровой логики. В Rust они реализуются через обычные структуры (struct).

Примеры компонентов для 2D-игры: * Position { x: f32, y: f32 } — координаты на экране * Velocity { x: f32, y: f32 } — вектор скорости * Health { current: i32, max: i32 } — очки здоровья

Если сущности "Игрок" добавить компоненты Position и Velocity, она сможет перемещаться. Если в процессе игры убрать компонент Velocity (например, игрок попал в ловушку), сущность мгновенно застынет на месте, так как потеряет данные о скорости. Это и есть принцип композиции в действии.

Система (System)

Система — это логика, которая обрабатывает сущности с определенным набором компонентов. Системы в Bevy — это обычные функции Rust, которые вызываются каждый кадр.

Например, система перемещения (Movement System) запрашивает у движка все сущности, у которых есть одновременно и Position, и Velocity. Затем она обновляет координаты на основе скорости и времени кадра.

В этом коде функция movement_system ничего не знает о том, является ли объект игроком, пулей или облаком. Ей важны только наличие позиции и скорости. Это обеспечивает невероятную гибкость кода и позволяет легко добавлять новые игровые механики.

Ориентация на данные и производительность

Архитектура ECS тесно связана с концепцией Data-Oriented Design (DOD). Главная цель DOD — оптимизировать работу с кэшем процессора, что критически важно для современных игр.

Процессор читает данные из оперативной памяти не по одному байту, а блоками — кэш-линиями (обычно по 64 байта). Если данные расположены в памяти последовательно, процессор загружает сразу несколько элементов за одно обращение к памяти. ООП-подход часто разбрасывает объекты по куче (heap), заставляя процессор постоянно ждать выгрузки новых данных.

Рассмотрим пример с числами. Допустим, компонент Velocity занимает 8 байт памяти (два числа с плавающей точкой по 4 байта). В одну кэш-линию размером 64 байта поместится ровно 8 таких компонентов. Когда система запрашивает скорость первой сущности, процессор автоматически загружает в сверхбыстрый кэш L1 скорости следующих семи сущностей.

При обработке 100 000 частиц (например, искр от взрыва) такой подход сокращает время вычислений с миллисекунд до микросекунд. Если на обработку одной частицы в ООП тратится 10 наносекунд из-за промахов кэша (Cache Miss), то 100 000 частиц займут 1 миллисекунду. В ECS благодаря попаданиям в кэш (Cache Hit) время может сократиться до 0,1 миллисекунды, оставляя больше ресурсов для рендеринга и физики.

Bevy автоматически управляет размещением компонентов в памяти, группируя сущности с одинаковым набором компонентов в так называемые архетипы (Archetypes). Это позволяет разработчику сосредоточиться на геймплее, пока движок берет на себя низкоуровневую оптимизацию и распределение задач по ядрам процессора.

2. Работа с 2D-графикой, спрайтами и загрузка ассетов

Работа с 2D-графикой, спрайтами и загрузка ассетов

В архитектуре Entity-Component-System (ECS) сами по себе сущности невидимы. Это просто числовые идентификаторы в памяти компьютера. Чтобы игрок мог увидеть главного героя, врагов или элементы ландшафта, сущностям необходимо добавить компоненты, отвечающие за визуальное представление. В 2D-играх основным строительным блоком графики выступает спрайт.

> Спрайты – это двухмерные картинки в играх. Из них состоят: персонажи, монстры, объекты, которые будут двигаться на экране, и иные составляющие игрового мира. > > otus.ru

Исторически спрайты обрабатывались отдельными аппаратными блоками старых консолей, но сегодня это просто плоские изображения, которые видеокарта отрисовывает поверх игрового фона. Фреймворк Bevy предоставляет мощные и эргономичные инструменты для работы с такой графикой, скрывая сложную низкоуровневую работу с графическим API (Vulkan, DirectX или Metal).

Управление ресурсами и AssetServer

Любая игра состоит не только из исходного кода, но и из внешних файлов: изображений, звуков, 3D-моделей и шрифтов. В геймдеве такие файлы принято называть ассетами (assets).

Загрузка файла с жесткого диска в оперативную память — это невероятно медленная операция по меркам центрального процессора. Если игра попытается загрузить тяжелую текстуру прямо во время игрового цикла, экран застынет, и игрок почувствует неприятный рывок (фриз).

Для решения этой проблемы в Bevy существует системный ресурс AssetServer. Он отвечает за асинхронную фоновую загрузку файлов. Когда вы просите AssetServer загрузить картинку, он не заставляет игру ждать окончания загрузки. Вместо этого он мгновенно возвращает специальный указатель — Handle (дескриптор).

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

Пример распределения памяти: если на уровне находится 100 одинаковых врагов, вам не нужно загружать картинку врага 100 раз. Вы загружаете ее один раз через AssetServer, получаете один Handle и раздаете его копии всем 100 сущностям. Если текстура весит 5 мегабайт, то 100 врагов займут в видеопамяти ровно 5 мегабайт, а не 500.

Камера и система координат

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

В Bevy для 2D-игр используется компонент Camera2dBundle. По умолчанию камера смотрит в центр системы координат — точку .

Система координат в Bevy работает по классическим математическим правилам: * Ось направлена вправо * Ось направлена вверх * Ось используется для сортировки глубины (слоев)

Математическое правило перекрытия объектов выглядит так: если , где — координата глубины первого объекта, а — координата глубины второго объекта, то первый объект будет отрисован поверх второго. Например, если фон имеет , а игрок , игрок всегда будет отображаться на фоне, а не под ним.

Отрисовка первого спрайта

Для создания видимого объекта на экране необходимо объединить сущность, дескриптор изображения и координаты. В Bevy для этого используется готовый набор компонентов SpriteBundle.

Рассмотрим пример системы инициализации, которая создает камеру и спрайт игрока:

В этом коде функция asset_server.load() ищет файл player.png в стандартной папке assets, которая должна находиться в корне вашего проекта. Компонент Transform задает позицию сущности в пространстве. Благодаря ..default() остальные параметры спрайта (например, цвет или размер) заполняются стандартными значениями.

Проблема производительности: Draw Calls

Когда на экране появляется несколько десятков объектов, описанный выше подход работает отлично. Но что произойдет, если мы создадим уровень из 10 000 тайлов (блоков земли, травы и кирпичей)?

Каждый раз, когда видеокарте нужно нарисовать картинку, центральный процессор отправляет ей команду, которая называется Draw Call (вызов отрисовки). Подготовка одного Draw Call — ресурсоемкая задача для процессора.

Если каждый блок земли — это отдельный файл dirt.png, процессор отправит видеокарте 10 000 команд на отрисовку. При 60 кадрах в секунду это 600 000 команд ежесекундно. Процессор не справится с такой нагрузкой, и частота кадров (FPS) упадет до неиграбельных значений.

Для наглядности сравним два подхода к хранению графики:

| Характеристика | Отдельные файлы (100 картинок) | Текстурный атлас (1 файл) | | --- | --- | --- | | Количество обращений к диску | 100 (очень медленно) | 1 (очень быстро) | | Вызовы отрисовки (Draw Calls) | 100 на каждый кадр | 1 на каждый кадр | | Фрагментация видеопамяти | Высокая | Отсутствует | | Сложность анимации | Требует смены файлов | Требует смены координат внутри файла |

Оптимизация: Текстурные атласы (Sprite Sheets)

Решением проблемы Draw Calls является текстурный атлас (или Sprite Sheet). Это одно большое изображение, в котором плотно упакованы все мелкие спрайты игры.

Вместо того чтобы просить видеокарту нарисовать 100 разных картинок, движок отправляет ей одну большую картинку и говорит: "Нарисуй вот этот квадратный кусочек здесь, а вот этот — там". Поскольку текстура не меняется, все 10 000 блоков земли можно отрисовать за один единственный Draw Call.

Рассчитать количество спрайтов в атласе можно по простой формуле:

Где — общее количество спрайтов, и — ширина и высота всего атласа в пикселях, а и — ширина и высота одного спрайта.

Например, если размер атласа составляет 1024 на 1024 пикселя, а размер одного тайла — 64 на 64 пикселя, то в один файл поместится уникальных спрайтов.

Чтобы использовать текстурный атлас в Bevy, необходимо выполнить три шага:

  • Загрузить большое изображение через AssetServer.
  • Создать ресурс TextureAtlasLayout, указав размер одного кадра и количество колонок/строк.
  • Прикрепить к сущности компонент TextureAtlas, указав индекс (порядковый номер) нужного кадра.
  • Индексы в атласе считаются слева направо и сверху вниз, начиная с нуля. Если персонаж бежит, система анимации просто меняет индекс кадра в компоненте TextureAtlas каждый десяток миллисекунд, создавая иллюзию плавного движения.

    Грамотная работа с ассетами и понимание принципов батчинга (объединения Draw Calls через атласы) — это фундамент, который позволяет создавать в Bevy масштабные 2D-миры, работающие со скоростью сотен кадров в секунду даже на слабых компьютерах.

    3. Обработка пользовательского ввода и управление персонажем

    Обработка пользовательского ввода и управление персонажем

    В предыдущих материалах мы разобрали архитектуру ECS и научились выводить на экран графические ассеты. Однако статичная картинка — это еще не игра. Интерактивность рождается в тот момент, когда программа начинает реагировать на действия пользователя. В архитектуре Entity-Component-System эта задача ложится на плечи специализированных систем, которые считывают состояние оборудования и изменяют данные компонентов.

    Ресурс ввода и опрос клавиатуры

    Фреймворк Bevy предоставляет доступ к клавиатуре, мыши и геймпадам через глобальные ресурсы. Для работы с клавиатурой используется ресурс ButtonInput, параметризованный перечислением клавиш KeyCode.

    Обработка ввода в Bevy строится на принципе polling (опроса). Это означает, что игровой движок не ждет, пока операционная система пришлет сигнал о нажатии клавиши, а самостоятельно проверяет состояние нужных кнопок в каждом кадре.

    Ресурс ButtonInput предоставляет три основных метода для проверки состояния: * pressed — возвращает истину всё время, пока клавиша удерживается нажатой. Идеально подходит для непрерывного движения (например, бег персонажа). * just_pressed — срабатывает ровно один раз в том кадре, когда клавиша была нажата. Используется для одиночных действий: прыжок, выстрел, открытие инвентаря. * just_released — срабатывает один раз в момент отпускания клавиши. Полезно для прерывания действий, например, остановки зарядки сильного удара.

    > Синглтон Input — это глобально доступный объект... Это подходящий инструмент для проверки ввода в каждом кадре. > > docs.godotengine.org

    Хотя цитата выше относится к движку Godot, концепция глобального объекта для покадрового опроса ввода абсолютно идентична подходу с ресурсом Res<ButtonInput<KeyCode>> в Bevy.

    Поиск персонажа через запросы

    Когда система зафиксировала нажатие клавиши «W», ей необходимо понять, какую именно сущность нужно сдвинуть вверх. В игровом мире могут находиться тысячи объектов с компонентом координат Transform, но двигаться должен только главный герой.

    Для фильтрации сущностей в Bevy используются Query (запросы) и маркерные компоненты. Маркерный компонент — это структура без полей, которая служит исключительно как ярлык (тег) для сущности.

    Создадим пустую структуру Player и добавим ее к сущности при создании. Теперь мы можем написать запрос Query<&mut Transform, With<Player>>. Этот код буквально означает: «Дай мне изменяемый доступ к координатам только тех сущностей, у которых есть компонент Player».

    Время и независимость от частоты кадров

    Самая частая ошибка начинающих разработчиков — привязка скорости движения к частоте кадров (FPS). Если в системе перемещения просто написать «прибавляй 5 пикселей к координате X каждый кадр», скорость игры будет зависеть от мощности компьютера.

    Для решения этой проблемы используется Delta Time (дельта времени) — время, прошедшее с момента отрисовки предыдущего кадра. В физике равномерное движение описывается базовой формулой:

    Где — итоговое перемещение (расстояние), — заданная скорость персонажа, а — время, в течение которого происходило движение (в нашем случае — дельта времени).

    Рассмотрим конкретный пример с числами. Допустим, скорость нашего героя составляет 300 пикселей в секунду.

  • На слабом мониторе с частотой 60 FPS один кадр длится примерно 0,016 секунды. За один кадр персонаж сдвинется на: 300 × 0,016 = 4,8 пикселя.
  • На игровом мониторе с частотой 144 FPS один кадр длится 0,0069 секунды. За один кадр персонаж сдвинется на: 300 × 0,0069 = 2,07 пикселя.
  • В обоих случаях за одну реальную секунду персонаж пройдет ровно 300 пикселей. В Bevy доступ к дельте времени осуществляется через ресурс Res<Time> и его метод delta_seconds().

    Векторы и диагональное движение

    В 2D-играх с видом сверху (top-down) игрок может нажать одновременно две клавиши, например «W» (вверх) и «D» (вправо). Если мы просто сложим эти два направления, персонаж начнет двигаться по диагонали.

    Однако здесь кроется математическая ловушка. Длина диагонального вектора вычисляется по теореме Пифагора:

    Где — итоговая скорость по диагонали (гипотенуза), — скорость по оси X, а — скорость по оси Y.

    Если скорость по горизонтали равна 1, и по вертикали равна 1, то диагональная скорость составит . Это означает, что при движении наискосок персонаж будет бежать на 41% быстрее, чем по прямой! Чтобы этого избежать, применяется нормализация вектора — математическая операция, которая сохраняет направление вектора, но принудительно делает его длину равной единице.

    | Направление | Вектор до нормализации | Скорость | Вектор после нормализации | Итоговая скорость | | --- | --- | --- | --- | --- | | Вправо (D) | (1.0, 0.0) | 100% | (1.0, 0.0) | 100% | | Вверх (W) | (0.0, 1.0) | 100% | (0.0, 1.0) | 100% | | Диагональ (W+D) | (1.0, 1.0) | 141% | (0.707, 0.707) | 100% |

    Итоговая система перемещения

    Объединив ресурс ввода, запрос к компонентам, дельту времени и нормализацию векторов, мы можем написать полноценную и надежную систему управления персонажем на Rust:

    В этом коде метод single_mut() используется потому, что мы уверены: на уровне существует только один игрок. Мы собираем все нажатия в единый вектор direction, проверяем, не равен ли он нулю (чтобы избежать ошибки деления на ноль при нормализации), и только затем умножаем его на базовую скорость и дельту времени.

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

    4. Игровая логика, обработка столкновений и анимация

    Игровая логика, обработка столкновений и анимация

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

    Управление состояниями игры

    Глобальная игровая логика часто опирается на концепцию конечного автомата. В каждый момент времени программа находится в одном конкретном режиме: главное меню, активный геймплей, инвентарь, пауза или экран поражения. Фреймворк Bevy предоставляет встроенный механизм States для элегантного управления этими режимами.

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

    Представим, что в игре работает 50 различных систем (отрисовка интерфейса, искусственный интеллект врагов, спавн частиц пыли из-под ног). Без механизма состояний в каждой из 50 функций пришлось бы писать ручную проверку текущего статуса игры. Состояния позволяют движку автоматически отключать целые группы систем на уровне планировщика задач, экономя ресурсы процессора.

    Если на обработку одного кадра уходит 16 миллисекунд, автоматическое отключение 30 тяжелых систем ИИ во время паузы может снизить время кадра до 2 миллисекунд. Это не только предотвращает логические ошибки (когда враг убивает игрока в меню), но и значительно снижает нагрев устройства и расход батареи.

    Математика столкновений

    Чтобы игровые объекты не проходили друг сквозь друга, применяется обнаружение столкновений (collision detection). Самый простой и быстрый для вычислений метод — это проверка пересечения окружностей.

    Для этого используется формула расстояния между двумя точками на плоскости, вытекающая из классической теоремы Пифагора:

    Где — итоговое расстояние между центрами объектов, и — координаты первого объекта, а и — координаты второго объекта.

    Столкновение фиксируется в том случае, если вычисленное расстояние меньше или равно сумме радиусов двух объектов. Математически это записывается как .

    Рассмотрим пример с конкретными координатами. Главный герой находится в точке , и имеет радиус хитбокса 5 пикселей. Враг находится в точке , с радиусом 7 пикселей.

  • Вычисляем расстояние: пикселей.
  • Сумма радиусов: пикселей.
  • Так как 10 меньше 12, объекты пересекаются — произошло столкновение.
  • Для прямоугольных объектов (спрайтов) чаще используется метод AABB (Axis-Aligned Bounding Box — выровненный по осям ограничивающий параллелепипед). В этом случае проверяется наложение проекций объектов на оси X и Y. Прямоугольники пересекаются только тогда, когда их проекции накладываются друг на друга одновременно по обеим осям.

    | Тип хитбокса | Вычислительная сложность | Точность | Идеально подходит для | | --- | --- | --- | --- | | Окружность | Очень низкая | Средняя | Снаряды, круглые персонажи, радиус взрыва | | AABB (Прямоугольник) | Низкая | Высокая для 2D | Платформы, стены, тайловые карты | | Полигон | Высокая | Очень высокая | Сложный ландшафт, наклонные поверхности | | Pixel-perfect | Экстремально высокая | Абсолютная | Медленные головоломки, точные клики мышью |

    > Физический движок облегчает очень многие задачи — нам понадобится, всего лишь, несколько простых кусочков кода. Для начала, добавим проверку коллизий между мячиком и кирпичами... > > developer.mozilla.org

    В Bevy для проверки AABB-столкновений есть встроенная функция collide, которая не просто возвращает логическое значение (было столкновение или нет), но и указывает сторону, с которой произошло касание. Это критически важно для платформеров: если игрок коснулся платформы сверху — он должен приземлиться, если снизу — удариться головой и начать падение вниз.

    Разрешение столкновений

    Обнаружить пересечение — это только половина задачи. Вторая половина — разрешение столкновений (collision resolution). Это процесс изменения координат или скоростей объектов таким образом, чтобы они перестали пересекаться.

    Самый популярный подход в 2D-играх — выталкивание на глубину проникновения. Если за один кадр персонаж успел войти в стену на 4 пикселя, система должна принудительно сдвинуть его координаты на 4 пикселя в обратном направлении.

    Оживление мира: Спрайтовая анимация

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

    Вместо загрузки десятков отдельных файлов изображений, используется единый текстурный атлас (Sprite Sheet). Движок виртуально "нарезает" это изображение на сетку равных прямоугольников. Анимация сводится к циклическому изменению индекса отображаемого прямоугольника с течением времени.

    Для контроля скорости смены кадров в Bevy применяется компонент Timer. Он отсчитывает заданные интервалы времени и сообщает системе, когда пора переключить кадр на следующий.

    Допустим, анимация бега персонажа состоит из 8 кадров. Мы настраиваем Timer в цикличный режим со срабатыванием каждые 0,1 секунды. За 1 реальную секунду таймер сработает ровно 10 раз. Это означает, что полный цикл бега (8 кадров) займет 0,8 секунды, а за оставшиеся 0,2 секунды персонаж успеет проиграть еще 2 кадра из следующего цикла. Изменяя длительность таймера, разработчик может легко синхронизировать визуальную скорость перебирания ногами с фактической скоростью перемещения персонажа по экрану, избегая эффекта "скольжения" по земле.

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

    5. Создание пользовательского интерфейса (UI) и финальная сборка игры

    Создание пользовательского интерфейса (UI) и финальная сборка игры

    Когда базовая механика работает, персонаж корректно перемещается по уровню и взаимодействует с окружением, проекту не хватает слоя, который свяжет игрока с внутренними данными системы. Этим слоем выступает пользовательский интерфейс (User Interface или UI). В сочетании со звуковым сопровождением и правильной компиляцией проекта, интерфейс превращает набор механик в полноценный готовый продукт.

    Архитектура UI в Bevy

    В отличие от многих классических движков, где интерфейс строится в отдельном визуальном редакторе поверх игры, в Bevy UI является неотъемлемой частью архитектуры ECS. Кнопки, панели и текстовые блоки — это такие же сущности (Entities), как главный герой или враги, просто они обладают специфическим набором компонентов.

    Для построения интерфейса используются следующие базовые компоненты:

    * NodeBundle — базовый невидимый контейнер, аналог тега div в веб-разработке. Используется для группировки других элементов. * TextBundle — компонент для отрисовки шрифтов и текстовой информации. * ButtonBundle — интерактивная область, способная отслеживать состояния наведения курсора и клика. * ImageBundle — компонент для вывода 2D-изображений в пространстве экрана (например, иконок инвентаря).

    Система позиционирования элементов в Bevy основана на парадигме Flexbox, пришедшей из веб-разработки. Вместо жесткого указания координат в пикселях, разработчик задает правила поведения контейнеров: выравнивание по центру, распределение свободного пространства, отступы и направления (в строку или в столбец).

    Если ширина экрана составляет 1920 пикселей, а мы задаем ширину панели интерфейса как 50%, она займет ровно 960 пикселей. Если игрок изменит размер окна до 1280 пикселей, панель автоматически сожмется до 640 пикселей без необходимости переписывать код пересчета координат.

    Создание игрового дисплея (HUD)

    HUD (Heads-Up Display) — это часть интерфейса, которая постоянно отображается поверх игрового мира во время активного геймплея. Обычно здесь располагаются счетчик очков, полоса здоровья и мини-карта.

    > Пользовательский интерфейс позволяет нам прорисовывать элементы на слое поверх всей остальной игры, поэтому отображаемая информация не перекрывается никакими игровыми элементами, такими как игрок или мобы. > > docs.godotengine.org

    В Bevy для отрисовки UI используется отдельная камера. Если Camera2dBundle смотрит на игровой мир, то для интерфейса ранее требовалась отдельная UI-камера, но в современных версиях движка элементы интерфейса автоматически рендерятся поверх основной 2D-камеры.

    Чтобы счетчик обновлялся, создается отдельная система. Она использует Query для поиска сущности с компонентом ScoreText и изменяет строку текста каждый раз, когда глобальный ресурс счета увеличивается.

    Интеграция UI с игровыми состояниями

    Интерфейс должен строго соответствовать текущему режиму игры. Главное меню не должно перекрывать экран во время битвы с боссом, а счетчик очков не нужен в меню настроек. Для управления этим используются состояния (States), которые мы рассматривали ранее.

    Чтобы движок понимал, какие элементы интерфейса нужно удалить при смене состояния, применяются маркерные компоненты. Например, всем кнопкам и текстам главного меню добавляется пустой компонент OnMainMenuScreen.

    | Состояние игры | Активные UI элементы | Логика мира | Отрисовка мира | | --- | --- | --- | --- | | MainMenu | Кнопка "Старт", Название игры | Отключена | Скрыта / Фоновая | | InGame | HUD (Счет, Здоровье, Патроны) | Включена | Активна | | Paused | Меню паузы, Кнопка "Продолжить" | Отключена | Активна (заморожена) |

    При переходе из MainMenu в InGame, специальная система очистки находит все сущности с компонентом OnMainMenuScreen и вызывает команду despawn_recursive(). Это удаляет сам контейнер и все вложенные в него дочерние элементы (тексты, иконки), полностью очищая экран для игрового HUD.

    Звуковое сопровождение

    Визуальный интерфейс дает игроку информацию, но именно звук обеспечивает физическое ощущение взаимодействия. В геймдизайне это называется соком (juice) — элементы, которые не меняют правила игры, но делают ее приятной.

    В Bevy работа со звуком реализована через ресурс Audio. Звуковые файлы (например, форматы OGG или WAV) загружаются через AssetServer. Когда игрок нажимает кнопку в UI или подбирает монету, система отправляет команду на воспроизведение загруженного аудио-ассета.

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

    Оптимизация и финальная сборка

    На протяжении всего процесса разработки запуск игры осуществлялся командой cargo run. Этот режим называется Debug (отладочный). В нем компилятор Rust сохраняет огромный объем метаданных для поиска ошибок, не применяет оптимизации памяти и не векторизует математические операции. Из-за этого сложная игра в Debug-режиме может выдавать всего 15-20 кадров в секунду.

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

    В этом режиме компилятор применяет агрессивные оптимизации: удаляет неиспользуемый код, встраивает функции (inlining) и оптимизирует работу с кэшем процессора. Производительность игры может вырасти в 10-50 раз по сравнению с отладочной версией.

    Оценить этот прирост можно через расчет времени кадра. Для обеспечения плавной картинки при частоте 60 кадров в секунду (FPS), процессор должен успевать просчитать всю логику, физику и отрисовку за строго ограниченное время:

    Где — максимальное допустимое время на один кадр в миллисекундах, 1000 — количество миллисекунд в одной секунде, а 60 — целевая частота кадров.

    Если в отладочной сборке тяжелая сцена с тысячей врагов обрабатывается за 35 мс, игра будет тормозить, выдавая около 28 FPS. После компиляции с флагом --release, время обработки той же сцены может упасть до 2 мс. Это оставляет огромный запас времени до лимита в 16.67 мс, гарантируя стабильные 60 FPS даже на слабых компьютерах.

    Сборка для браузера (WebAssembly)

    Одним из главных преимуществ связки Rust и Bevy является возможность легкого портирования игры в веб-браузер с помощью технологии WebAssembly (WASM). Это бинарный формат инструкций, который выполняется в браузере с производительностью, близкой к нативным приложениям.

    Процесс портирования включает несколько шагов:

  • Добавление целевой архитектуры WASM в инструментарий Rust.
  • Компиляция проекта командой cargo build --release --target wasm32-unknown-unknown.
  • Использование утилиты wasm-bindgen для создания JavaScript-обертки, которая свяжет бинарный код игры с API браузера.
  • Размещение полученных файлов на любом статическом веб-сервере.
  • Сборка под WASM позволяет игрокам запускать вашу игру по простой ссылке, без необходимости скачивать исполняемые файлы и переживать о вирусах. Это критически важно для участия в геймджемах (соревнованиях разработчиков) и создания портфолио.

    Создание UI, добавление звука и правильная компиляция — это финальные штрихи, которые превращают набор разрозненных систем на Rust в законченный, оптимизированный и готовый к публикации продукт.