Среда, 20 Ноября 2024, 05:21

Приветствую Вас Гость

[ Новые сообщения · Игроделы · Правила · Поиск ]
  • Страница 1 из 1
  • 1
Простая змейка на Bevy
minecrafter2Дата: Воскресенье, 25 Июня 2023, 19:48 | Сообщение # 1
участник
Сейчас нет на сайте
Bevy молодой игровой движок общего назначения который предоставляет ECS логику для создания игровых сценариев. Основное отличие от ООП подхода как раз в отсутствии самого понятия "объект", вместо работает структура из Entity (сущности) Component (компоненты) и Systems (системы) (а так же ещё Resources, но их в аббревиатуру по какой-то причине не включают). Так же стоит отметить что ECS не ноу-хау Bevy, а в общем-то довольно известная технология. К примеру года 3 назад Unity предприняла попытку создать альтернативную логику движка на ECS.

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

Для начала настройки рабочего окружения.

Добавляем Bevy к проекту:
Код
cargo add bevy


Так как вы уже должны быть минимально знакомы с Rust'ом вы знаете какие у него долгие компиляция так что необходимо добавить пару оптимизаций что бы в результате наш .toml файл выглядел так:
Код
// ...
[dependencies]
bevy = "0.10.1"
rand = "0.8.5"

[profile.dev]
opt-level = 1

[profile.dev.package."*"]
opt-level = 3


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


После таких настроек нажимаем кнопку build и ждём компиляции. Имейте ввиду это может занять довольно приличное время (около 10 -+3 минут). Зато последующие запуски проекта будут в течении нескольких секунд.

Базовая настройка приложения

Для использования базовых функци движка необходимо использовать указать:
Код
use bevy::prelude::*;

Остальные же импорты сама IDE предложит.

Самый корень Bevy приложения это App. С помощью которого мы добавляем Системы. Системы это обычные функции Rust. Давайте добавить встроенную системы close_on_esc что по нажатию на соответствующую клавишу игра закрывалась. Код будет выглядеть так:

Код
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(
            WindowPlugin {
                primary_window: Some(Window {
                    title: String::from("Bevy Tutorial"),
                    ..default()
                }),
                ..default()
            }
        ))
        .add_system(bevy::window::close_on_esc)
        .run()
}


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

Для этого сначало создадим новую систему:
Код
fn spawn_camera(mut commands: Commands) {
    commands.spawn(Camera2dBundle::default());
}

и добавим её:
Код
.add_startup_system(spawn_camera)


В качестве параметров системе принимает Commands - инструмент для работы с сущностями (удаление, создание). Обратите внимание, при добавлении системы передавать аргументы не надо! Bevy решит это самостоятельно какие параметры системе нужны.

Структура змейки

Код
#[derive(Resource)]
struct Snake(VecDeque<Entity>);

impl Default for Snake {
    fn default() -> Self {
        Self(VecDeque::new())
    }
}

#[derive(Resource)]
struct SnakeDirection(Direction);

impl Default for SnakeDirection {
    fn default() -> Self {
        Self(Direction::Right)
    }
}

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

#[derive(Component)]
struct SnakeSegment;


Для змейки нам необходим ресурс (глобально доступная структура) которая будет хранить вектор с id сущностей змейки. Так же необходим компонент SnakeSegment для маркировки сущностей принадлежностью к змейке и направление змейки. Так же не забываем имплементацию "по умолчанию" для того что бы инициализировать ресурс.

Инициализация ресурсов происходит так: (в App)
Код
.init_resource::<Snake>()
.init_resource::<SnakeDirection>()


Спавн начальной змейки

Код
fn spawn_default_snake(
    mut commands: Commands,
    mut snake: ResMut<Snake>,
    asset_server: Res<AssetServer>,
) {
    let default_snake = &[(0., 0.), (42., 0.), (84., 0.)];

    for (segment_x, segment_y) in default_snake {
        snake.0.push_back(
            commands
                .spawn((
                    SpriteBundle {
                        transform: Transform::from_xyz(*segment_x, *segment_y, 0.),
                        texture: asset_server.load("images/snake_segment.png"),
                        ..default()
                    },
                    SnakeSegment,
                ))
                .id(),
        );
    }
}


В качестве параметров ресурс берёт Commands для спавна сущности змейки, ресурс змейки и вектором (обязательно mut, нам его менять надо), и сервер по загрузки ассетов. Помимо спавна спрайта, добавляем ещё SnakeSegment, теперь сущности можно легко найти по фильтру этого компонента. Добавляем системы как стартовую.

Движение змейки

Код
fn snake_movement(
    mut commands: Commands,
    mut snake: ResMut<Snake>,
    snake_query: Query<&Transform, With<SnakeSegment>>,
    snake_direction: Res<SnakeDirection>,
    asset_server: Res<AssetServer>,
) {
    commands.entity(*snake.0.front().unwrap()).despawn();
    snake.0.pop_front();

    let snake_head = snake_query.iter().last().unwrap().translation;

    let (offset_x, offset_y) = match snake_direction.0 {
        Direction::Up => (0., 42.),
        Direction::Down => (0., -42.),
        Direction::Left => (-42., 0.),
        Direction::Right => (42., 0.),
    };

    snake.0.push_back(
        commands
            .spawn((
                SpriteBundle {
                    transform: Transform::from_xyz(
                        snake_head.x + offset_x,
                        snake_head.y + offset_y,
                        0.,
                    ),
                    texture: asset_server.load("images/snake_segment.png"),
                    ..default()
                },
                SnakeSegment,
            ))
            .id(),
    );
}


Тут логика игры стандартная - удаляем хвост добавляем спереди и так движемся по направлению. У данной системы есть очень важный параметр - запрос (Query). С помощью запросов можно легко найти все необходимые элементы с необходимыми компонентами. Это ключевая функция Bevy и ECS системы.

Ввод с клавиатуры

Код
fn snake_direction_input(
    mut snake_direction: ResMut<SnakeDirection>,
    keyboard: Res<Input<KeyCode>>,
) {
    if keyboard.just_pressed(KeyCode::Up) {
        snake_direction.0 = Direction::Up
    } else if keyboard.just_pressed(KeyCode::Down) {
        snake_direction.0 = Direction::Down
    } else if keyboard.just_pressed(KeyCode::Left) {
        snake_direction.0 = Direction::Left
    } else if keyboard.just_pressed(KeyCode::Right) {
        snake_direction.0 = Direction::Right
    }
}


Bevy использует ввод с клавиатуры как Ресурс, так что просто добавляем ресурс к системы и можем слушать клавиатуру.

Рост змейки

Код
fn snake_growth_event_handler(
    mut commands: Commands,
    mut snake: ResMut<Snake>,
    mut snake_growth_event_reader: EventReader<SnakeGrowthEvent>,
    snake_direction: Res<SnakeDirection>,
    snake_query: Query<&Transform, With<SnakeSegment>>,
    asset_server: Res<AssetServer>,
) {
    for _ in snake_growth_event_reader.iter() {
        let snake_head = snake_query.iter().last().unwrap().translation;

        let (offset_x, offset_y) = match snake_direction.0 {
            Direction::Up => (0., 42.),
            Direction::Down => (0., -42.),
            Direction::Left => (-42., 0.),
            Direction::Right => (42., 0.),
        };

        snake.0.push_back(
            commands
                .spawn((
                    SpriteBundle {
                        transform: Transform::from_xyz(
                            snake_head.x + offset_x,
                            snake_head.y + offset_y,
                            0.,
                        ),
                        texture: asset_server.load("images/snake_segment.png"),
                        ..default()
                    },
                    SnakeSegment,
                ))
                .id(),
        );

        println!("snake_growth_event_handler");
    }
}


Для создания системы роста змейки будем использовать ещё одну функцию Bevy - события (Event). События может отправить любая система и принять так же любая система кто имеет соотвествующий параметр.

Создаём пустую структуру и добавляем её как событие:
Код
.add_event::<SnakeGrowthEvent>()


Спавн еды
Для начала нам нужно создать еду по умолчанию:
Код
fn spawn_default_food(mut commands: Commands, asset_server: Res<AssetServer>) {
    let x = rand::thread_rng().gen_range(-7..=7) as f32;
    let y = rand::thread_rng().gen_range(-7..=7) as f32;

    commands.spawn((
        SpriteBundle {
            transform: Transform::from_xyz(x * SEGMENT_SIZE, y * SEGMENT_SIZE, 0.),
            texture: asset_server.load("images/food.png"),
            ..default()
        },
        Food,
    ));
}

Добавим эту сисему как стартовую

И по такой же логике проверяем столкновение головы змейки с едой:
Код
fn snake_eat_food(
    mut commands: Commands,
    mut snake_growth_event_writer: EventWriter<SnakeGrowthEvent>,
    food_query: Query<(Entity, &Transform), With<Food>>,
    snake_query: Query<&Transform, With<SnakeSegment>>,
    asset_server: Res<AssetServer>,
) {
    let snake_head = snake_query.iter().last().unwrap().translation;

    food_query.for_each(|food| {
        if snake_head == food.1.translation {
            commands.entity(food.0).despawn();
            snake_growth_event_writer.send(SnakeGrowthEvent);

            let x = rand::thread_rng().gen_range(-7..=7) as f32;
            let y = rand::thread_rng().gen_range(-7..=7) as f32;

            commands.spawn((
                SpriteBundle {
                    transform: Transform::from_xyz(x * SEGMENT_SIZE, y * SEGMENT_SIZE, 0.),
                    texture: asset_server.load("images/food.png"),
                    ..default()
                },
                Food,
            ));

            println!("Snake Eat Food!");
        }
    });
}


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

Код
fn game_over(
    mut app_exit_event_writer: EventWriter<bevy::app::AppExit>,
    snake_query: Query<&Transform, With<SnakeSegment>>,
) {
    let snake_head = snake_query.iter().last().unwrap().translation;
    let skip_count = snake_query.iter().len()
        - snake_query
            .iter()
            .filter(|segment| segment.translation != snake_head)
            .count();

    if skip_count > 1 {
        app_exit_event_writer.send(bevy::app::AppExit);
        println!("Game Over!");
    }
}


Настройки порядка работы систем

Код
    App::new()
        .add_plugins(
            DefaultPlugins.set(WindowPlugin {
                primary_window: Some(Window {
                    title: String::from("Bevy Snake Game"),
                    resolution: WindowResolution::new(WIDTH * SEGMENT_SIZE, HEIGHT * SEGMENT_SIZE)
                        .with_scale_factor_override(1.),
                    ..default()
                }),
                ..default()
            }),
        )
        .insert_resource(FixedTime::new_from_secs(1. / 10.))
        .init_resource::<Snake>()
        .init_resource::<SnakeDirection>()
        .add_event::<SnakeGrowthEvent>()
        .add_startup_system(spawn_camera)
        .add_startup_system(spawn_default_snake)
        .add_startup_system(spawn_default_food)
        .add_system(bevy::window::close_on_esc)
        .add_system(snake_direction_input)
        .add_system(snake_growth_event_handler)
        .add_systems(
            (
                snake_movement,
                snake_eat_food.after(snake_movement),
                game_over.after(snake_movement),
            )
                .in_schedule(CoreSchedule::FixedUpdate),
        )
        .run()


Для того что бы змейка двигалась не так быстро создадим ресурс с фиксированным временем. Далее вместо поочерёдного добавление систем. Используем функцию добавление пачки систем и объеденим из Schedule. Такой способ позволет настроить порядок запуска систем и как часто они запускаются. В нашем случае необходимо что бы система движение всегда была перед система поедания и проигрыша.

Послесловие
Ещё хочу обратить внимание что при таком подходе нужно как можно больше разбивать логику работы на мелкие системы, тогда движок будет работать на все сто. Движок очень молодой (ему вот только 3 года), бесплатный, так что специально не обращал внимание на некоторые особенности API так они крайне часто меняются (тут версия 0.10.1). Но как по мне явно перспективный, явно не будет брошен, всё таки 25 тысяч звёзд на гитхабе о чём то говорит (к примеру у libgdx за всё его время существования в почти 13 лет только 21 тысяча).



Добавлено (29 Июня 2023, 18:04)
---------------------------------------------
Простейший графический интерфейс
Создание простейшего меню

Для создания графического интерфейса будем использовать встроенные функции движка Bevy UI. Графический интерфейс ещё достаточно сыроват (на момент версии 0.10.1) и код будет выглядеть достаточно громоздко но в общем полностью выполняет необходимые требования + возможна полная кастомизация внешнего вида.

Первое что нам понадобиться это дополнительный файл main_menu.rs (или лучше просто menu.rs, так больше подходит стилистике Rust кода)...

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

...после создания файла с меню нам нужно подключить файл к основному (к примеру IntelliJ IDEA сама предложит подключение), сделать это так:
Код
mod main_menu;


Это указывает компилятору видимость данного файла но важно - не само содержимое файла.

Создадим в main_menu.rs структуру:
Код
pub struct MainMenuPlugin;


И указываем возможность его использования в main.rs:
Код
use crate::main_menu::MainMenuPlugin;


Теперь когда базовая настройка завершена перейдём к логике.

Состояния
Состояния это обычный enum с перечислением всех возможных вариантов.

Нам подойдёт такой:
Код
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash, States)]
enum AppState {
    #[default]
    Menu,
    Game,
}


Для того что бы Bevy понял что это состояния необходимо указать черту States.

Вас могут испугать остальной набор атрибутов, но с помощью "cargo check" довольно просто понять какие именно нам необходимы - он сам нам их предложит.

И добавляем:
Код
.add_state::<AppState>()


Условия выполнения систем
К сожалению на текущей версии движка 0.10.1 не очень удобно задавать условия выполнения систем.

Обратите внимание что на скриншоте ниже ошибок нету, но такой способ всё равно работать не будет.


Вместо этого нам придётся указать критерий конкретной системе:
Код

        .add_systems(
            (
                snake_movement.run_if(in_state(AppState::Game)),
                snake_eat_food.after(snake_movement),
                game_over.after(snake_movement),
            )
                .in_schedule(CoreSchedule::FixedUpdate)
        )

Это очень неудобно (хотя есть немного другие пути решение такого, но всё равно не очевидные и не удобны), очень надеюсь что в 0.11 это исправят.

Создание плагина

Плагин в понятии Bevy это просто набор систем вынесенных в отдельное место. (Можно рассматривать как объект с функциями в ООП).

Реализация плагина:
Код
pub struct MainMenuPlugin;

impl Plugin for MainMenuPlugin {
    fn build(&self, app: &mut App) {
        app
            .add_startup_system(spawn_main_menu)
            .add_system(play_button_interaction);
    }
}


Так же создадим компоненты...

Код
#[derive(Component)]
struct RootNode;

#[derive(Component)]
struct PlayButton;


...для поиска самой кнопки "Играть" и корневого компонента этой кнопки.

Сама модель UI строиться по логике Flexbox и предстовляет собой довольно гибкую систему, но слишком громоздкую для человеческого глаза и может очень легко спугнуть новичка.

Создания кнопки:


Хотя с другой стороны трудно тут как либо прокомментировать, код сам себя объясняет. Единственное что отметить можно это логику: КорневойКомпонент -> Кнопка -> Текст.

Взаимодействие с кнопками

Для взаимодействия с кнопкой нужно использовать запрос (Query) вместе с параметром Interaction (это enum со всеми возможными действиями над кнопкой) и компонентов <ИмяНашейКнопочки>.

Так же не забываем использовать конструкцию...
Код
if let Ok(...) = ... {}

...что бы исключить ошибку если элемента не существует.

Код
fn play_button_interaction(
    mut commands: Commands,
    menu_query: Query<(&Interaction, Entity), With<PlayButton>>,
    mut state: ResMut<NextState<AppState>>,
) {
    if let Ok((interaction, entity)) = menu_query.get_single() {
        match interaction {
            Interaction::Clicked => {
                commands.entity(entity).despawn_recursive();
                state.set(AppState::Game);
            }
            Interaction::Hovered => {}
            Interaction::None => {}
        }
    }
}


Сущность удаляем рекурсивно что бы его наследники тоже были удалены.

На этом всё.

P.S.
Долька арбуза? Мечта детства получить награду на Gcup выполнена!


Майнкрафт - лучшая игра в мире

Сообщение отредактировал minecrafter2 - Четверг, 29 Июня 2023, 18:07
TLTДата: Пятница, 30 Июня 2023, 13:52 | Сообщение # 2
Сейчас нет на сайте
Bevy - движок очень перспективный, а главное - он регулярно обновляется.

Дао, выраженное словами, не есть истинное Дао.
minecrafter2Дата: Суббота, 01 Июля 2023, 00:33 | Сообщение # 3
участник
Сейчас нет на сайте
Цитата TLT ()
Bevy - движок очень перспективный, а главное - он регулярно обновляется.


Согласен. Недостатки (совершенно нормальное явление для молодого проекта) которые я упоминал выше имеют далеко не критичный характер, я просто старался не создать ложное впечатление о движке как "ультра убийца Unity\UE".

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


Майнкрафт - лучшая игра в мире
  • Страница 1 из 1
  • 1
Поиск:

Все права сохранены. GcUp.ru © 2008-2024 Рейтинг