Войти
K5EngineСтатьи

Система действий

Автор:

Статья о основной системе для построения логики игрового приложения. Описаны основные элементы.

Введение.

В данной статье рассматриваются основные элементы системы действий движка а так же даются примеры работы с некоторыми из готовых блоков логики.

Класс-действие.

Главная идея всей системы действий — для написания логики игры использовать единообразный подход с использованием классов-реакций на события движка.
В роли базового класса для построении такой системы выступает TBaseAction, заголовочный файл которого можно найти по пути [K5Engine][Core][ActionSystem][BaseAction.h]. Это абстрактный класс, у которого при наследовании обязательно нужно переопределять метод void ToRun(const TEvent &Event).
Рассмотрим этот класс более подробно:

class TBaseAction
{
  protected:
    static unsigned long IdCounter;
    unsigned long Id;

    bool    Active;
    wstring Name;
  protected:
    virtual void ToSetActive();
    virtual void ToStart    ();
    virtual void ToStop     ();
    virtual void ToRun(const TEvent &Event) = 0;
  public:
    TBaseAction();
    TBaseAction(const bool &ActiveVal);
    virtual ~TBaseAction();

    virtual TBaseAction* Clone();
    virtual void Update();

    unsigned long GetID() const;

    void    SetName(const wstring &Val);
    wstring GetName() const;

    void SetActive(const bool &Val);
    bool GetActive() const;

    void Start();
    void StartIsNotActive();

    void Stop();
    void StopIsActive();

    void Run(const TEvent &Event = TEvent());
};
Класс имеет такие элементы:
unsigned long Id — уникальный идентификатор класса. Он генерируется при создании класса автоматически, получить его можно при помощи метода GetID().
Используется крайне редко.
bool Active — флаг активности действия. По умолчанию действие не активно, то есть его метод Run(..) не будет вызывать ToRun(...), пока действие не будет активно.
Для старта действия используется метод Start(), для остановки — Stop(). Так же можно задавать флаг активности действия в обход старта или остановки при помощи метода SetActive(const bool &Val), но этого делать в большинстве случаев не желательно, так часто в Start() или Stop() выполняется какая то логика. Метод SetActive(const bool &Val) может использоваться для приостановки выполнения действия.
Для получения текущего состояния флага активности служит метод  bool GetActive() const.
wstring Name — имя действия, по умолчанию пустое. Задаётся методом SetName(const wstring &Val), получить значение можно при помощи  wstring GetName() const.

Далее рассмотрим ключевые виртуальные методы класса:
virtual void ToSetActive() — виртуальный метод, вызывается в SetActive(...). Используется редко, может быть полезен, когда нужно проконтролировать или выполнитль какие либо действия при выставлении флага активности.
virtual void ToStart() — виртуальный метод, вызывается в  Start(). Полезен для описания какой либо логики при старте действия.
virtual void ToStop() — виртуальный метод, вызывается в  Stop(). Полезен для описания какой либо логики при остановки действия.
virtual void ToRun(const TEvent &Event) = 0 — ключевой метод логики действия. Он вызывается при активном действии в методе Run(...). При создании собственного класса-действия метод  ToRun(...) нужно обязательно описывать.

Далее рассмотрим два вспомогательных виртуальных метода, они используются не часто, но всё же иногда могут быть полезны в работе:
virtual TBaseAction* Clone() - метод позволяет описать создание копии класса-действия. В TBaseAction возвращается NULL значение.
virtual void Update() - какое либо обновление параметров или состояния класса-действия. В TBaseAction ничего не делает, предназначен для использования в наследниках.

Далее рассмотрим ключевые публичные методы класса:
void Start() - стартует класс-действие: флаг активности выставляется в true и вызывается метод ToStart().
void StartIsNotActive() - стартует класс-действие, если он ещё не активен.
void Stop() - останавливает класс-действие: флаг активности выставляется в false и вызывается метод ToStop().
void StopIsActive() - останавливает класс-действие, если он в данный момент активен.
void Run(const TEvent &Event = TEvent()) - главный метод класса-действия, в нём если файл активности равен true, вызывается метод  ToRun(...). Стоит обратить внимание, что по умолчанию в метод Run(...) передаётся пустое событие.
 

Списки и менеджеры действий.

При использовании большого количества классов-действий требуется инструмент для их групповой обработки. Для этого предназначены списки и менеджеры действий.
В они аналогичны спискам и менеджерам из графической системы. Точно так же TActionList наследуется от базового шаблонного класса TBaseActionList, точно так же менеджеры предназначены для управления группами списков. Код TBaseActionList можно найти по пути [K5Engine][Core][ActionSystem][BaseActionList.h]. От него уже наследуются TActionList и TActionListManager, заголовочные файлы которых можно найти в том же каталоге, что и у  TBaseActionList. Так же в случае потребности хранения и обработки групп указателей на классы-действия есть базовый шаблонный класс TBaseActionPointerList  и его наследник TActionPointerList.

Рассмотрим группы методов TBaseActionList.

Как и у других объектов движка, у список есть собственный уникальный индекс а так же можно задать имя. Для получения индекса используется метод  GetID(), для задания и получения имени соответственно  SetName(...) и GetName().

Так же как и у  TBaseAction у списков есть флаг активности. Он задаётся и берётся методами SetActive(...) и GetActive() соответственно. Если список не активен, но он не будет вызывать методы Run(...) у добавленных классов-действий.

Для получения указателя на класс-действие используется группа методов Get(...), которые могут принимать позицию действия в списке, индивидуальный индекс или имя объекта. Если объекта с таким именем или индексом нет или запрашиваемая позиция выходит за пределы размера списка, то будет создано исключение. Так же получить указатель на объект можно при помощи группы методов GetIfExist(...), если такого объекта нет, то будет возвращён NULL. Можно получить указатель на первый и последний объект в списке при помощи GetFirst() и GetLast() соответственно.

Для проверки наличия объекта в списке применяются методы IfExist(...), которые могут принимать уникальный идентификатор объекта или его имя. Возвращают true если объект есть и false, если такого объекта нет.

Для получения позиции объекта в списке используются методы GetIndex(...), принимающие  уникальный идентификатор или имя объекта.

Если же надо получить количество объектов в списке, тогда нужно воспользоваться методом GetSize().

Для добавления созданного класса-действия используется метод Add(TObject *Object). Соответственно нужно передавать указатель на созданный объект. Список хранит созданные объекты, при разрушении списка он выполняет delete для каждого объекта. Из этого следует несколько правил:
- Нельзя удалять самостоятельно объект по указателю,  для этого надо использовать соответствующий метод списка.
- Нельзя добавлять один и тот же объект в несколько списков, при их разрушении произойдёт попытка удалить данные повторно.

Для удаления объекта из списка используется группа методов Del(...), которые принимают параметры, аналогичные методам Add(...). Для удаления всех объектов используется метод  Clear(). Так же можно отчистить список указателей без удаления созданных объектов при помощи ClearElems(), но применять метод нужно очень осторожно, так как он чреват утечками памяти.

Для старта всех объектов, добавленных в список, применяется метод Start(). Для остановки  - Stop(). Что бы остановить или запустить одних объект-действие, применяются методы StartOne(...) и StopOne(...).

Для того, что бы проверить, активен ли один объект, применяеются методы IsOneActive(...), которые могут принимать позицию действия в списке, индивидуальный индекс или имя. Активен или не активен хотя бы один объект — IsOneActive() и IsOneNotActive(), активны иди не активны  все — IsAllActive() и IsAllNotActive() соответственно.

Есть возможность задавать только один исполняемый объект в данный момент. Для этого используется группа методов SetActiveObject(...), которые могут принимать указатель на объект, индекс объекта в списке, уникальный идентификатор или имя объекта. Для отмены такого режима работы используется метод UnSetActiveObject(). Для получения указателя выбранного активного объекта применяется метод GetActiveObject(). Для проверки, установлен ли активный объект — IsSetActiveObject().

Если нужно вызвать метод обновления у всех классов-действий, которые добавлены в список, применяется метод Update().

Главный метод списка - Run(const TEvent &Event = TEvent()), который, если список активен, пройдёт по всем классам-действиям и передаст им структуру события. При этом как видно, можно вызывать этот метод без передаваемых параметров. Для выполнения одного объекта можно воспользоваться группой методов RunOne(....), которые могут принимать  позицию объекта в списке, его уникальный идентификатор или его имя.

При помощи StartIsNotActive() и StopIsActive() можно вызвать одноимённый метод у объектов, при этом если список не активен, то данные методы не сработают. 

Что бы извлечь объект из списка, используется группа методов Extract(...), которые принимают в качестве параметра уже ранее не раз упоминающиеся три переменных: позицию в списке, уникальный идентификатор или имя. Опять же, после извлечения объекта из списка поместить его в другой список или удалить в ручную, что бы не произошла утечка памяти.

Для перемещения объектов из одно списка в другой можно воспользоваться методом Merge(...). При этом все объекты-действия из передаваемого списка будут перемещены в список, который вызывает метод слияния.

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

Менеджер списков TActionListManager наследуется от того же TBaseActionList. При этом у него есть дополнительный набор методов, позволяющий получать доступ к классам-действиям, находящимся в списках, добавлять или удалять объект из целевого списка. Дополнительные методы описаны в заголовочном файле, который находится по пути [K5Engine][Core][ActionSystem][ActionListManager.h]. Их назначение должно быть понятно из названий и принимаемых параметров.

Готовые классы-действия.

Рассмотрим готовые для применения классы-действия. Они находятся в библиотеке-расширении движка, их заголовочные файлы можно найти по пути [K5EngineExtensions][ActionSystem].
Так как классов довольно много, по каждому будет дана краткая справка, без подробного описания их методов.

Сначала на счёт общих моментов в структуре классов:
- Когда задаётся единичный объект для его итерации или каких либо манипуляций над ним, в большинстве случаев используется метод Set(...) с соответствующим параметром.
- Когда есть возможность проводить операции над группой объектов, они добавляются в класс-действие методом Add(...).
- Итераторы наследуются от TActionBaseIterator, от него они получают возможность выполнения с задержкой и возможность установки финального значения при принудительной остановке. Так же итераторы для инициализации режима работы используют методы с префиксом Use..., например UseAngleAndTime(...) или UseDistAndSpeed(...).
- У большинства классов-действий переопределён оператор присваивания и присутствует конструктор копирования. Исключения — TActionQueue и TActionContainer.

Далее рассмотрим каждый из доступных классов-действий:
TActionAngleIterator — итератор TValue. Позволяет линейно изменять значение класса в ту или иную сторону за время или с определённой скоростью а так же задавать новое значение.

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

TActionColorArrayIterator и TActionColorIterator — итераторы объектов цвета. Изменяют отдельную компоненту цвета, по умолчанию это альфа-канал.

TActionContainer — фактически это список действий, оформленный как отдельный класс-действие.

TActionJoin — позволяет создавать простые связи родитель-потомок между TPoint и TValue объектами. У действия-соединения есть собственные Pos и Angle значения, которые инициируются после задания целевых объектов для трансформации. Класс полезен, когда надо вращать или передвигать объекты группой или в зависимости от какой то иерархической структуры. При помощи этого класса можно организовать простую скелетную анимацию.

TActionPointArrayIterator и TActionPointIterator — итераторы объектов точки и массива точек. Позволяют изменять за определённое время или в один момент координаты точки на какую то дистанцию или приводить их к какому то заданному значению. Оба действия наследуются от TActionBaseIterator.

TActionQueue — очередь выполнения действий. Фактически это тот же список, только с выполнением каждого класса по очереди. Плюс выполнение очереди можно зациклить для создания постоянно повторяющихся трансформации.

TActionSpriteTextureAnimation — текстурная анимация у указанного спрайта. Можно задавать между текстурами и между выделенными частями текстур. Сначала нужно добавить трансформации при помощи методов AddRect(...). Далее после того, как добавлены трансформации, нужно добавить порядок их переключений при помощи AddFrameId(...). Для установки времени, сколько будет отображаться один кадр анимации, используется метод SetFrameTime(...).

TActionSpriteTransform — приведение одного спрайта к заданному шаблону. Фактически это набор трансформаций. Данному действию задаётся спрайт, шаблонный спрайт и время, за которое должна произойти трансформация. Для того, что бы указать, какой именно параметр трансформировать, используются соответствующие флаги, например ChangeView, ChangePos, ChangeCenter и тд. По умолчанию они все false.

TActionTexturesLoader — действие загружает список текстур. Пути к текстурам, их параметры и имена задаются при помощи специальной структуры TDataNode, описание которой будет дано в следующей статье.
 

Итог.


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

27 февраля 2012 (Обновление: 28 фев. 2012)

Комментарии [8]