Игровой движок Amethyst | 3.1 Концепции. Состояния

Игровой движок Amethyst | 3.1 Концепции. Состояния

Rust Lang Сообщество - Николай Калугин

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

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

Если вы не понимаете, как что-то работает в Amethyst, знание представленных здесь концепций поможет вам понять, как сделаны некоторые реализации.

Состояния

Слово «состояние» (State) может означать много разных вещей в компьютерной науке. В случае Amethyst оно используется для представления «игрового состояния».

Состояние игры является общей и глобальной частью игры.

Пример

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

Игру можно разделить на разные состояния:

  • Загрузка (LoadingState)
  • Главное меню (MainMenuState)
  • Игровой процесс (GameplayState)
  • Пауза (PauseState)
  • Результаты (ResultState)

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

Менеджер состояний

Amethyst имеет встроенный менеджер состояний, который позволяет легко переключаться между различными состояниями. Он основан на концепции автомата с магазинной памятью (pushdown-automaton), который представляет собой комбинацию стека и конечного автомата.

Стек

Концепция стека позволяет вам «накладывать» состояния друг на друга.

Если мы возьмем пример понга рассмотренный ранее, вы можете вызвать PauseState поверх GameplayState. Когда вы хотите выйти из паузы, вы вытаскиваете PauseState из стека и возвращаетесь в GameplayState, как раз на тот момент, когда вы его покинули.

Конечный автомат

Понятие конечного автомата (State Machine) может быть довольно сложным, но здесь мы рассмотрим только его основы.

Конечный автомат обычно состоит из двух элементов: переходов и событий.

Переходы - это просто «переключение» между двумя состояниями. Например, из LoadingState перейдите в состояние MainMenuState.

Amethyst имеет несколько типов переходов:

  • Вы можете выдвинуть одно состояние поверх другого;
  • Вы также можете переключить состояние, которое заменяет текущее состояние новым.

События - это то, что вызывает переходы. В случае с Amethyst это разные методы, вызываемые состоянием. Продолжайте читать, чтобы узнать о них.

Жизненный цикл

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

  • on_start: когда состояние добавляется в стек;
  • on_stop: когда состояние удаляется из стека;
  • on_pause: когда текущее состояние приостанавливается другим, помещенным поверх состоянием;
  • on_resume: когда приостановленное состояние возобновляется;
  • handle_event: позволяет обрабатывать события (например закрытие окна или нажатие клавиши);
  • fixed_update: вызывается в активном состоянии через фиксированный интервал времени (по умолчанию 1/60 секунды).
  • update: вызывается в активном состоянии, с максимально возможной частотой.
  • shadow_update: вызывается с максимально возможной частотой, для всех состояний, которые находятся в стеке StateMachines, включая активное состояние. В отличие от update, это не возвращает Trans.
  • shadow_fixed_update: вызывается через фиксированный интервал времени (по умолчанию 1/60 секунды) для всех состояний, которые находятся в стеке StateMachines, включая активное состояние. В отличие от fixed_update, это не возвращает Trans.

Если вы не используете SimpleState или EmptyState, вы должны реализовать метод update для вызова data.data.update (& mut data.world).

Данные игры

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

Состояния также имеют внутренние данные любого типа T. В большинстве случаев используются два следующих: () и GameData.

  • () - означает, что нет данных, связанных с этим состоянием. Это обычно используется для тестов, а не для реальных игр.
  • GameData - является стандартом де-факто. Это структура, содержащая Dispatcher. Это будет обсуждаться позже.

При вызове методов вашего State, движок будет передавать структуру StateData, которая содержит и World (его обсудим позже), и тип игровых данных, который вы выбрали.

Код

Да! Наконец-то пришло время добавить код!

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

Создание состояния

extern crate amethyst;
use amethyst::prelude::*;

struct GameplayState {
    /// Локальные данные состояния. Обычно здесь ничего не будет.
    /// В данном случае, здесь количество игроков.
    player_count: u8,
}

impl SimpleState for GameplayState {
    fn on_start(&mut self, _data: StateData<'_, GameData<'_, '_>>) {
        println!("Number of players: {}", self.player_count);
    }
}

Это действительно много кода!

Сначала мы объявляем структуру состояния GameplayState.

В этом случае мы даем ему некоторые данные: player_count, размером в байт.

Затем мы реализуем типаж SimpleState для нашего GameplayState. SimpleState - это сокращение для State <GameData <'static, 'static>, ()>, где GameData - это внутренние общие данные между состояниями.

Переключение состояний

Теперь, если мы хотим перейти во второе состояние, как мы это сделаем?

Что же, нам нужно использовать один из методов, которые возвращают тип Trans. Это:

  • handle_event
  • fixed_update
  • update

Давайте используем handle_event, чтобы перейти к PausedState и вернуться, нажав клавишу «Escape».

extern crate amethyst;
use amethyst::prelude::*;
use amethyst::input::{VirtualKeyCode, is_key_down};

struct GameplayState;
struct PausedState;

// На этот раз мы используем () вместо GameData, потому что у нас нет систем, которые нужно обновлять.
// (Они рассматриваются в специальном разделе книги.)
// Вместо того, чтобы писать `State <(), StateEvent>`, мы можем использовать `EmptyState`.

impl EmptyState for GameplayState {
    fn handle_event(&mut self, _data: StateData<()>, event: StateEvent) -> EmptyTrans {
        if let StateEvent::Window(event) = &event {
            if is_key_down(&event, VirtualKeyCode::Escape) {
                // Приостанавливаем игру переходом к `PausedState`.
                return Trans::Push(Box::new(PausedState));
            }
        }

        // Escape не нажат, поэтому мы остаемся в этом состоянии.
        Trans::None
    }
}

impl EmptyState for PausedState {
    fn handle_event(&mut self, _data: StateData<()>, event: StateEvent) -> EmptyTrans {
        if let StateEvent::Window(event) = &event {
            if is_key_down(&event, VirtualKeyCode::Escape) {
                // Возвращаемся обратно к `GameplayState`.
                return Trans::Pop;
            }
        }

        // Escape не нажат, поэтому мы остаемся в этом состоянии.
        Trans::None
    }
}

Обработка событий

Как вы уже видели, мы можем обрабатывать события из метода handle_event. Но что это за странный StateEvent?

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

// Эти импорты требуются #[derive(EventReader)] для сборки кода
use amethyst::core::{
    ecs::{Read, SystemData, World},
    shrev::{ReaderId, EventChannel},
    EventReader
};

#[derive(Clone, Debug)]
pub struct AppEvent {
    data: i32,
}

#[derive(Debug, EventReader, Clone)]
#[reader(MyEventReader)]
pub enum MyEvent {
    Window(Event),
    Ui(UiEvent),
    App(AppEvent),
}

struct GameplayState;

impl State<(), MyEvent> for GameplayState {
    fn handle_event(&mut self, _data: StateData<()>, event: MyEvent) -> Trans<(), MyEvent> {
        match event {
            // События, связанные с окном и вводом.
            MyEvent::Window(_) => {}, 
            // Ui событие. Нажатие кнопок, наведение мыши и т.д ...
            MyEvent::Ui(_) => {}, 
            MyEvent::App(ev) => println!("Got an app event: {:?}", ev),
        };

        Trans::None
    }
}

Чтобы приложение узнало об изменении событий, отправляемых в состояние, вам также необходимо указать и тип события, и тип EventReader (имя, которое вы даете в атрибуте #[reader(SomeReader)]), когда приложение создано. Это делается путем замены Application :: build (или Application :: new) на CoreApplication::<_, MyEvent, MyEventReader> :: build() (или CoreApplication::<_, MyEvent, MyEventReader> :: new()) ,

Примечание. События собираются из EventChannels. EventChannels описаны в отдельном разделе книги.


Статья была переведена для "Rust Lang Сообщества". (Ссылка на оригинал)

Если вас интересуют подобные переводы, рекомендую обратить внимание на Telegram канал - @rust_lang_ru. На котором вы можете найти статьи, переводы, новости и другие интересные материалы по языку Rust, там же вы можете обсудить всё это с единомышленниками.

Также имеется YouTube канал.

Подписывайтесь!


Предыдущая глава | В начало | Следующая глава