netlib.narod.ru< Назад | Оглавление | Далее >

Состояния и процессы

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

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

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

Состояния приложения

Я упомянул состояния — что это такое? Если коротко, состояние (state) — это образ действий текущего потока вашего приложения во время выполнения. Главное меню вашей игры — это состояние, точно также как и основной игровой экран тоже состояние. Экран со списком предметов, который вы хотите добавить — это еще одно состояние.

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

switch(CurrentState) {
    case STATE_TITLESCREEN:
        DoTitleScreen();
        break;

    case STATE_MAINMENU:
        DoMainMenu();
        break;

    case STATE_INGAME:
        DoGameFrame();
        break;
}

Подобная конструкция едва работает, особенно если ваша программа перегружена различными состояниями, и практичски нежизнеспособна, если вы попытаетесь управлять состояниями для каждого кадра! Вместо этого лучше использовать технику, которую я называю программированием на основании состояний (state-based programming или, для краткости, SBP). Основу этой техники составляет перенаправление потока исполнения на основе стека состояний. Каждое состояние представляется объектом или набором функций. Если вам потребовались функции, вы добавляете их в стек. Когда работа с функциями завершена, они удаляются из стека. Этот процесс показан на рис. 1.10.


Рис. 1.10. Стек позволяет помещать и извлекать состояния по мере надобности

Рис. 1.10. Стек позволяет помещать и извлекать состояния по мере надобности


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

По упомянутым выше причинам необходимо реализовать диспетчер состояний, который получает указатели на функции (которые представляют состояния). Помещение состояния в стек добавляет на вершину стека указатель на функцию. Ваша задача состоит в вызове диспетчера состояний, который передаст управление верхнему состоянию в стеке. Работа с диспетчером состояний действительно очень проста, так что позвольте мне показать пример, демонстрирующий ее:

class cStateManager
{
    // Структура для хранения указателей на функции
    // в связанном списке
    typedef struct sState {
        void (*Function)();
        sState *Next;
    } sState;

    protected:
        sState *m_StateParent; // Верхнее состояние в стеке
                               // (голова стека)
    public:
        cStateManager() { m_StateParent = NULL; }
        ~cStateManager()
        {
            sState *StatePtr;

            // Удаляем состояния из стека
            while((StatePtr = m_StateParent) != NULL) {
                m_StateParent = StatePtr->Next;
                delete StatePtr;
            }
        }

        // Помещаем функцию в стек
        void Push(void (*Function)())
        {
            // Нельзя помещать нулевой указатель
            if(Function != NULL) {
                // Выделяем ресурсы для нового состояния
                // и помещаем его в стек
                sState *StatePtr = new sState;
                StatePtr->Next = m_StateParent;
                m_StateParent = StatePtr;
                StatePtr->Function = Function;
            }
        }

        // Извлекаем функцию из стека
        BOOL Pop()
        {
            sState *StatePtr = m_StateParent;

            // Удаляем верхнее значение из стека
            // (если оно есть)
            if(StatePtr != NULL) {
                m_StateParent = StatePtr->Next;
                delete StatePtr;
            }

            // Возвращаем TRUE если в стеке есть еще состояния,
            // и FALSE если стек пуст
            if(m_StateParent == NULL)
                return FALSE;
            return TRUE;
        }

        // Обрабатываем верхнее сосотояние в стеке
        BOOL Process()
        {
            // Возвращаем ошибку, если больше нет состояний
            if(m_StateParent == NULL)
                return FALSE;

            // Передаем управление верхнему состоянию
            // (если оно есть)
            m_StateParent->Function();

            return TRUE;
        }
};

Как видите, класс очень мал, но не позволяйте его размеру одурачить вас. Объект cStateManager позволяет вам в любой момент добавлять новые состояния, а в функции визуализации кадра будет достаточно вызвать метод Process, и быть уверенным, что управление будет передано требуемой функции.

Вот пример:

cStateManager SM;

// Макрос для простого вызова функции MessageBox
#define MB(s) MessageBox(NULL, s, s, MB_OK);

// Прототипы функций состояний - следуйте этим прототипам!
void Func1() { MB("1"); SM.Pop(); }
void Func2() { MB("2"); SM.Pop(); }
void Func3() { MB("3"); SM.Pop(); }

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,
                   LPSTR szCmdLine, int nCmdShow)
{
    SM.Push(Func1);
    SM.Push(Func2);
    SM.Push(Func3);

    while(SM.Process() == TRUE);
}

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

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

Процессы

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

class cProcessManager
{
    // Структура для хранения указателей на функции
    // в виде связанного списка
    typedef struct sProcess {
        void (*Function)();
        sProcess *Next;
    } sProcess;

    protected:
        sProcess *m_ProcessParent; // Верхнее состояние в стеке
                                   // (голова стека)
    public:
        cProcessManager() { m_ProcessParent = NULL; }

        ~cProcessManager()
        {
            sProcess *ProcessPtr;

            // Удаляем все процессы из стека
            while((ProcessPtr = m_ProcessParent) != NULL) {
                m_ProcessParent = ProcessPtr->Next;
                delete ProcessPtr;
            }
        }

        // Добавляем функцию в стек
        void Add(void (*Process)())
        {
            // Нельзя помещать значение NULL
            if(Process != NULL) {
                // Создаем новый процесс
                // и помещаем его в стек
                sProcess *ProcessPtr = new sProcess;
                ProcessPtr->Next = m_ProcessParent;
                m_ProcessParent = ProcessPtr;
                ProcessPtr->Function = Process;
            }
        }

        // Выполняем все функции
        void Process()
        {
            sProcess *ProcessPtr = m_ProcessParent;
            while(ProcessPtr != NULL) {
                ProcessPtr->Function();
                ProcessPtr = ProcessPtr->Next;
            }
        }
};

Снова мы видим простой объект, во многом похожий на объект cStateManager, но с одним важным отличием. Объект cProcessManager только добавляет процессы; он не может удалять их. Вот пример использования cProcessManager:

cProcessManager PM;

// Макрос, упрощающий вызов функции MessageBox
#define MB(s) MessageBox(NULL, s, s, MB_OK);

// Прототипы функций процессов - следуйте этим прототипам!
void Func1() { MB("1"); }
void Func2() { MB("2"); }
void Func3() { MB("3"); }

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev, \
                   LPSTR szCmdLine, int nCmdShow)
{
    PM.Add(Func1);
    PM.Add(Func2);
    PM.Add(Func3);

    PM.Process();
    PM.Process();
}

Обратите внимание, что при каждом вызове Process вызываются все находящиеся в стеке процессы (как показано на рис. 1.11). Это очень полезно для быстрого вызова часто используемых функций. У вас может быть несколько объектов диспетчеров процессов для различных ситуаций — например, один для обработки ввода и работы с сетью и другой для обработки ввода и работы со звуком.


Рис. 1.11. Стек процессов состоит из часто вызываемых функций

Рис. 1.11. Стек процессов состоит из часто вызываемых функций. При вызове cProcessManager::Process управление последовательно будет передано каждой добавленной диспетчером функции



netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz