Загрузка...
 

Программный поток (Program Flow)

  • Является синонимом понятия "процесс выполнения программы".1


Содержание

Intro

Когда игрокодер с головой погружен в написание кода, часто очень трудно держать под контролем все внесённые в код изменения и поддерживать порядок в исходных файлах. Здесь не обойтись без понимания принципов работы программного потока. Грамотно структурированное приложение (или фреймворк) позволяет легко ориентироваться в своём исходном коде и вносить в него необходимые изменения. Имея на руках готовый дизайн-документ игры (Ты же сделал его, не так ли?), осталось лишь на его основе построить структуру процесса выполнения программы (structure of the processing flow).

Жизненый цикл игрового приложения

Типичная программа начинает свою работу с инициализации всех своих "систем" и данных. Затем она входит в т.н. главный цикл (main loop). Именно здесь происходит само выполнение программы. В зависимости от текущего игрового стейта (главный экран, игровой процесс и т.д.) игровые ввод и вывод обрабатываются по-разному.

При создании любого игрового приложения игрокодер, как правило, выполняет следующие шаги:

  1. Инициализация систем (Windows, графика, ввод, звук и т.д.).
  2. Подготовка данных (загрузка конфигурационных файлов).
  3. Конфигурирование стейта по умолчанию (default state; обычно это стейт с титульным экраном).
  4. Старт главного цикла программы (main loop).
  5. Определение текущего стейта и его обработка (путём обработки по необходимости ввода и вывода).
  6. Возврат к шагу 5 до тех пор, пока приложение не будет закрыто, после чего переход к шагу 7.
  7. Очищаем данные, освобождаем ранее занятые ресурсы.
  8. Освобождаем ресурсы системы (закрываем окна, обнуляем графику, ввод и т.д.).

Шаги 1-3 типичны при создании любой игры: настройка текущей системы, загрузка необходимых сопутствующих файлов (графика, звуки и т.д.), подготовка игрового процесса (actual gameplay). Игровое приложение обычно тратит уйму времени на определение текущего стейта и его обработку (шаг 5). Этот шаг можно разделить на 3 части:

  • докадровая обработка (pre-frame processing),
  • покадровая обработка (per-frame processing),
  • послекадровая обработка (post-frame processing).

Докадровая обработка выполняет ряд простых операций, вроде получения текущего времени (для зависимых от времени событий, например синхронизации) или обновления положения/состояния объектов сцены.
Покадровая обработка также обновляет статус объектов (если это не было сделано в докадровой обработке) и рендерит графику.
Послекадровая обработка выполняет оставшиеся функции вроде синхронизации временных операций или даже вывод уже отрендеренной графики.
В твоей игре может быть несколько докадровых стейтов: один для меню, один для организации внутриигрового процесса и т.д. Грамотная организация стейтов в игре называется стейт-процессинг (state-processing) и является ключом к созданию шустрого игрового приложения.
Удаление данных и закрытие приложения (шаги 7 и 8) освобождают системные ресурсы, которые выделялись на стадии препроцессинга. Графика должна быть удалена из памяти, окно приложения уничтожено и т.д. Пропуск данных шагов категорически противопоказан.
При создании программного потока каждому шагу сопоставлен соответствующий блок исходного кода. Поэтому, чем яснее и чётче структура этого кода, тем проще будет на его основе создать приложение. Часто код размещают в отдельных lib-файлах (системное ядро, графическое ядро, звуковое ядро и т.д.), подключаемых к проектам Visual C++ при необходимости.

Пример простейшего каркаса игры

Создадим простейшее оконное приложение и тупо добавим в самом конце файла исходного кода 3 функции (пустые, для простоты изложения):

  • DoInit() (докадровая обработка);
  • DoFrame() (покадровая обработка);
  • DoShutdown() (послекадровая обработка).


Перед началом проверь, что у тебя установлены следующие программные компоненты:

  • MS Visual C++ 2010.

Инструкции по её установке ты найдёшь в разделе "Софт" нашего сайта.

Создаём Проект приложения

  • Создай пустой Проект с именем Shell01. Проект автоматически разместится внутри Решения с таким же именем.

Весь процесс подробно расписан в статье Настройка MS Visual C plus plus 2010 и DirectX SDK.

Добавляем в Проект WinMain.cpp

Для чистоты эксперимента мы создали пустой Проект, т.е. без каких-либо файлов в нём. Создадим единственный файл с исходным кодом WinMain.cpp.

  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта Shell01.
  • Во всплывающем меню Добавить->Создать элемент...
Добавляем исходный файл
Добавляем исходный файл
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи WinMain.cpp.
  • Жмём "Добавить".

Image
Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле WinMain.cpp набираем следующий код:
WinMain.cpp
/**************************************************
WinMain.cpp
Chapter 5 Shell Application

Programming Role-Playing Games with DirectX
by Jim Adams (01 Jan 2002)
**************************************************/

// Include files
#include <windows.h>
#include <stdio.h>

// Window handles, class and caption text
HWND          g_hWnd;
HINSTANCE     g_hInst;
static char   g_szClass[]   = "ShellClass";
static char   g_szCaption[] = "Shell Application";

// Function prototypes
int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,          \
                   LPSTR szCmdLine, int nCmdShow);
long FAR PASCAL WindowProc(HWND hWnd, UINT uMsg,              \
                           WPARAM wParam, LPARAM lParam);

BOOL DoInit();
BOOL DoShutdown();
BOOL DoFrame();

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,          \
                   LPSTR szCmdLine, int nCmdShow)
{
  WNDCLASSEX wcex;
  MSG        Msg;

  g_hInst = hInst;

  // Create the window class here and register it
  wcex.cbSize        = sizeof(wcex);
  wcex.style         = CS_CLASSDC;
  wcex.lpfnWndProc   = WindowProc;
  wcex.cbClsExtra    = 0;
  wcex.cbWndExtra    = 0;
  wcex.hInstance     = hInst;
  wcex.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
  wcex.hCursor       = LoadCursor(NULL, IDC_ARROW);
  wcex.hbrBackground = NULL;
  wcex.lpszMenuName  = NULL;
  wcex.lpszClassName = g_szClass;
  wcex.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);
  if(!RegisterClassEx(&wcex))
    return FALSE;

  // Create the Main Window
  g_hWnd = CreateWindow(g_szClass, g_szCaption,
        WS_CAPTION | WS_SYSMENU,
        0, 0, 400, 400,
        NULL, NULL,
        hInst, NULL );
  if(!g_hWnd)
    return FALSE;
  ShowWindow(g_hWnd, SW_NORMAL);
  UpdateWindow(g_hWnd);

  // Run init function and return on error
  if(DoInit() == FALSE)
    return FALSE;

  // Start message pump, waiting for signal to quit
  ZeroMemory(&Msg, sizeof(MSG));
  while(Msg.message != WM_QUIT) {
    if(PeekMessage(&Msg, NULL, 0, 0, PM_REMOVE)) {
      TranslateMessage(&Msg);
      DispatchMessage(&Msg);
    }
    if(DoFrame() == FALSE)
      break;
  }

  // Run shutdown function
  DoShutdown();
  
  UnregisterClass(g_szClass, hInst);

  return Msg.wParam;
}

long FAR PASCAL WindowProc(HWND hWnd, UINT uMsg,              \
                           WPARAM wParam, LPARAM lParam)
{
  switch(uMsg) {
    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;

  }

  return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

BOOL DoInit()
{
  return TRUE;
}

BOOL DoShutdown()
{
  return TRUE;
}

BOOL DoFrame()
{
  return TRUE;
}
  • Сохрани Решение (Файл -> Сохранить все).

Данный исходный код целиком взят из примера к книге Programming Role-Playing Games with DirectX by Jim Adams (01 Jan 2002). С момента выхода MS Visual C++ 6.0, на котором прогал Jim Adams, прошло немало времени. С MSVC++2010 Express их разделяют аж 12 лет. Из-за этого вышеприведённый код (написанный в начале 2002 г.) на данном этапе в MSVC++2010 Express компилироваться не будет, выдавая многочисленные ошибки.
Но мы это исправим.

Готовим Проект Shell01 к компиляции

Для успешной компиляции изменим настройки (=свойства) текущего Проекта Shell01, созданного в MSVC++2010. При этом сам код из книги 2002 года останется нетронутым.

Закрыть
noteОбрати внимание

Напомним, что такую настройку необходимо повторно проделывать при создании каждого нового Проекта. Ниже представлен алгоритм действий по настройке Проекта Shell01, созданного в MSVC++2010 Express. При создании приложений под платформы, отличные от Win32, либо применении более новых версий DirectX SDK, процесс конфигурирования Проекта может отличаться от приведённого ниже.

Выбираем многобайтовую кодировку

Закрыть
noteПримечание

В MS Visual C++ 2010 в настройках по умолчанию стоит набор (кодировка) символов UNICODE. В MS Visual C++ 6.0 - напротив, по умолчанию стоит кодировка ANSI (многобайтовая). Данная настройка сильно влияет на типы используемых переменных, что приводит к заметным различиям в исходном коде.
Несмотря на то, что во всех случаях рекомендуется использовать кодировку UNICODE, поддерживаемую во всех современных ОС семейства MS Windows (начиная с Win 2000/XP), большинство книг по программированию игр на классическом C++ придерживаются именно многобайтовой кодировки. Чтобы сильно не переделывать исходные коды под UNICODE, все наши игровые Проекты мы настроим под многобайтовую кодировку. Для этого...

  • Убедись, что MSVC++2010 запущена и в ней открыт наш текущий Проект Shell01.
  • В Обозревателе решений щёлкаем правой кнопкой мыши по названию Проекта Shell01.
  • Во всплывающем контекстном меню выбираем "Свойства".
  • В появившемся окне установки свойств Проекта жмём Свойства конфигурации->Общие, в правой части в строке "Набор символов" выставляем значение "Использовать многобайтовую кодировку".

Image

  • Жмём ОК.
  • Сохрани Решение (Файл->Сохранить все)

Отключаем инкрементную компоновку (incremental linking)

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

Error LNK1123: сбой при преобразовании в COFF: файл недопустим или поврежден

Отключается в свойствах открытого Проекта. Для MS Visual C++ 2010 порядок следующий:

  • Убедись, что MSVC++2010 запущена и в ней открыт наш текущий Проект Shell01.
  • В Главном меню MS Visual C++ 2010 выбираем Проект -> Свойства (Project -> Properties).
  • В появившемся окне установки свойств Проекта жмём Свойства конфигурации -> Компоновщик -> Общие (Configuration Properties -> Linker -> General), в правой части в строке "Включить инкрементную компоновку" ставим значение Нет (/INCREMENTAL:NO).
  • Жмём ОК.
  • Сохрани Решение (Файл->Сохранить все)

Компилируем Проект Shell01

Наконец, наш тестовый Проект готов к компиляции.

  • Жми кнопку с зелёным треугольником на панели инструментов главного окна MSVC++2010 или F5 нак лавиатуре.

После компилирования приложение Shell01 автоматически запустится и покажет окно c белым фоном. В данном случае функции DoInit(), DoFrame() и DoShutdown() ничего не делают, но служат своеобразной заготовкой будущего алгоритма программного потока. Имена функций, конечно, игрокодер выбирает на своё усмотрение.

Стейты и процессы (States and Processes)

Оптимизация программного потока является одной из главных целей любого игрокодера. Исходный код небольшого размера прост в управлении. Но по мере роста приложения работать с ним становится всё сложнее. Особенно когда для изменения небольшой функции приходится переписывать огромные куски кода.
Допустим, разработка игры в процессе и ты решил добавить новую фичу, которая открывает в игре экран инвентори по нажатию клавиши i. Экран инвентори (inventory display screen) может быть активирован только во время игры, а не на главном экране игрового приложения. А значит необходимо внедрить код, определяющий нажатие клавиши i и открывающий по нажатию окно inventory.
Если тупо сделать одну функцию, которая рендерит каждый экран (display screen) независимо от того, что делает игрок в игре, то очень скоро функция рендеринга станет очень большой, сложной и не имеющей возможности отследить, какой из стейтов активен в данный момент.
На помощь придут две методики: Системы управления стейтами и процессами.

Рис.1 Стек стейтов позволяет помещать (push) и удалять (pop) из него стейты по мере необходимости.
Рис.1 Стек стейтов позволяет помещать (push) и удалять (pop) из него стейты по мере необходимости.

Стейты приложения (Application States)

Стейт - это сокращение от "стейт операции" (state of operation), представляющий собой текущее состояние (процесс), которое приложение обрабатывает. Главное меню игры - это стейт. Игровой процесс - это тоже стейт, также как и экран inventroy.
При добавлении в приложение различных стейтов, необходимо также продумать способ определить, как их обрабатывать в зависимости от текущего стейта операции (который постоянно меняется с одного на другой). Процесс определения необходимого стейта может обернуться таким ужасающим кодом:

switch(CurrentState) {
 case STATE_TITLESCREEN:
  DoTitleScreen();
  break;
 case STATE_MAINMENU:
  DoMainMenu();
  break;
 case STATE_INGAME:
  DoGameFrame();
  break;
}

Со временем такой список стейтов значительно разрастётся. И если весь его обрабатывать в каждом кадре, то получим значительное снижение производительности.
На практике вместо такого цикла switch...case применяют т.н. программирование на основе стейтов (state-based programming; SBP). Его суть заключается в ветвлении выполнения приложения на основе т.н. стэка стейтов (stack of states). В этом случае каждый стейт представляет собой объект или набор функций, которые добавляются в стек при необходимости. Более неиспользуемые функции удаляются из стека (См. Рис.1).
Стейты добавляются, удаляются и обрабатываются (process) с помощью менеджера стейтов (state manager). Будучи выброшенным (popped) из стека стейтов, самый верхний стейт удаляется (не насовсем), и очередь на обработку сдвигается так, что в обработку идёт следующий за ним стейт. Менеджер стейтов должен получать указатели (pointers) на функции (которые представляют те или иные стейты). При отправке стейта на обработку его указатель добавляется в стек к остальным. Задача игрокодера - своевременно вызывать менеджер стейтов, который будет обрабатывать самый верхний стейт в своём списке. На самом деле с менеджером стейтов работать нетрудно. Рассмотрим пример его реализации:

class cStateManager
{
 // Структура, которая хранит указатель функции и связный список (linked list)
 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;
  }

 // Помещаем (push) функцию в стек.
 void Push(void (*Function)())
 {
  // Не помещаем нулевое значение
  if(Function != NULL)
   {
    // Выделяем память (allocate) под новый стейт и помещаем (push) его в стек.
    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;
 }
};

Закрыть
noteОбрати внимание

В вышеприведённом коде виден идентификатор Next. Такими снабжается код, оснащённый поддержкой связных списков. Эта технология широко распространена во многих языках программирования. В зависимости от реализации, каждый член связного списка обладает следующими идентификаторами, хранящими информацию о своих "соседях" по последовательности: Next, Previous, First, Last и т.д.


Внедрив класс cStateManager в свой код, ты можешь добавлять в его объект новые стейты по необходимости. И во время вызова функции рендеринга кадра можно вызывать метод Process только на функции нужного стейта. Вот пример применения класса cStateManager:

cStateManager SM;

// Макрос для упрощённого вызова окна сообщения
#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, LPCSTR szCmdLine, int nCmdShow)
{
 SM.Push(Func1);
 SM.Push(Func2);
 SM.Push(Func3);
 while(SM.Process() == TRUE);
}


Вышеприведённая программа позволяет отслеживать состояние трёх стейтов, каждый из которых выдаёт окно сообщения (MessageBox) с определённой цифрой. Каждый стейт выдаёт сообщение прямо из стека и автоматически передаёт очередь другому стейту до тех пор, пока не закончатся все стейты и программа не будет завершена.
А теперь прикинь, если внедрить такой связный список (стек) в покадровую выборку сообщений (per-frame message pump). Допустим, что надо выдать сообщение игроку, но, как на зло, игра находится в стадии прорисовки внутриигровых объектов. В этом случае достаточно просто поместить функцию сообщения в стек и вызвать метод Process при подготовке следующего кадра.

Пример приложения с менеджером стейтов

Перед началом проверь, что у тебя установлены следующие программные компоненты:

  • MS Visual C++ 2010.

Инструкции по её установке ты найдёшь в разделе "Софт" нашего сайта.

Создаём Проект приложения

  • Создай пустой Проект с именем State01. Проект автоматически разместится внутри Решения с таким же именем.

Весь процесс подробно расписан в статье Настройка MS Visual C plus plus 2010 и DirectX SDK.

Добавляем в Проект WinMain.cpp

Для чистоты эксперимента мы создали пустой Проект, т.е. без каких-либо файлов в нём. Создадим единственный файл с исходным кодом WinMain.cpp.

  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта State01.
  • Во всплывающем меню Добавить->Создать элемент...
Добавляем исходный файл
Добавляем исходный файл
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи WinMain.cpp.
  • Жмём "Добавить".

Image
Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле WinMain.cpp набираем следующий код:
WinMain.cpp
/**************************************************
WinMain.cpp
Chapter 5 State-based processing Demo

Programming Role-Playing Games with DirectX
by Jim Adams (01 Jan 2002)
**************************************************/

// Include files
#include <windows.h>
#include <stdio.h>

class cStateManager
{
  // A structure that stores a function pointer and linked list
  typedef struct sState {
    void  (*Function)();
    sState *Next;
  } sState;

  protected:
    sState *m_StateParent; // The top state in the stack
                           // (the head of the stack)

  public:
    cStateManager() { m_StateParent = NULL; }

    ~cStateManager() 
    {
      sState *StatePtr;

      // Remove all states from the stack
      while((StatePtr = m_StateParent) != NULL) {
        m_StateParent = StatePtr->Next;
        delete StatePtr;
      }
    }

    // Push a function on to the stack
    void Push(void (*Function)())
    {
      // Don't push a NULL value
      if(Function != NULL) {
        // Allocate a new state and push it on stack
        sState *StatePtr = new sState;
        StatePtr->Next = m_StateParent;
        m_StateParent = StatePtr;
        StatePtr->Function = Function;
      }
    }

    BOOL Pop()
    {
      sState *StatePtr = m_StateParent;

      // Remove the head of stack (if any)
      if(StatePtr != NULL) {
        m_StateParent = StatePtr->Next;
        delete StatePtr;
      }

      // return TRUE if more states exist, FALSE otherwise
      if(m_StateParent == NULL)
        return FALSE;
      return TRUE;
    }

    BOOL Process()
    { 
      // return an error if no more states
      if(m_StateParent == NULL)
        return FALSE;
      // Process the top-most state (if any)
      m_StateParent->Function(); 
      return TRUE;
    }
};

cStateManager g_StateManager;

// Function prototypes
int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,          \
                   LPSTR szCmdLine, int nCmdShow);

// Macro to ease the use of MessageBox function
#define MB(s) MessageBox(NULL, s, s, MB_OK);

// State function prototypes - must follow this prototype!
void Func1() { MB("1"); g_StateManager.Pop(); }
void Func2() { MB("2"); g_StateManager.Pop(); }
void Func3() { MB("3"); g_StateManager.Pop(); }

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,          \
                   LPSTR szCmdLine, int nCmdShow)
{
  g_StateManager.Push(Func1);
  g_StateManager.Push(Func2);
  g_StateManager.Push(Func3);
  while(g_StateManager.Process() == TRUE);

  return 0;
}
  • Сохрани Решение (Файл -> Сохранить все).

Данный исходный код целиком взят из примера к книге Programming Role-Playing Games with DirectX by Jim Adams (01 Jan 2002). С момента выхода MS Visual C++ 6.0, на котором прогал Jim Adams, прошло немало времени. С MSVC++2010 Express их разделяют аж 12 лет. Из-за этого вышеприведённый код (написанный в начале 2002 г.) на данном этапе в MSVC++2010 Express компилироваться не будет, выдавая многочисленные ошибки.
Но мы это исправим.

Готовим Проект State01 к компиляции

  • Выбери многобайтовую кодировку.
  • Отключи инкрементную компоновку (incremental linking).

Оба этих действия подробно рассмотрены выше, при подготовке к компиляции Проекта Shell01.

  • Сохрани Решение (Файл->Сохранить все).

Компилируем Проект State01

Наконец, наш тестовый Проект готов к компиляции.

  • Жми кнопку с зелёным треугольником на панели инструментов главного окна MSVC++2010 или F5 нак лавиатуре.

После компилирования приложение Shell01 автоматически запустится и покажет поочерёдно 3 модальных (Popup) окна с кнопкой ОК. По её нажатию одно окно закрывается и тут же появляется другое. При этом отображаемый счётчик стейтов уменьшается на 1. Каждый стейт "выталкивает" (pops) сам себя из стека. Когда стейтов больше не осталось, программа определяет, что стек пуст и завершает работу.

Процессы (Processes)

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

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

 protected:
  sProcess *m_ProcessParent; // Самый верхний стейт в стеке ("голова" стека).
 
 public:
  cProcessManager() {m_ProcesParent=NULL;}
  
  `cProcessManager()
  {
   sProcess *sProcessPtr;

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

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

 // Обрабатываем все функции
 void Process()
 {
  sProcess *ProcessPtr = m_ProcessParent;

  while(ProcessPtr != NULL)
  {
   ProcessPtr->Function();
   ProcessPtr = ProcessPtr->Next;
  }
 }

};

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

cProcessManager PM;

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

// Прототипы функций процессов (process functions).
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();
}

Рис.2 Стек процессов состоит из часто вызываемых функций. Каждая функция, добавленная в менедже процессов, выполняется при общем вызове cProcessManager::Process.
Рис.2 Стек процессов состоит из часто вызываемых функций. Каждая функция, добавленная в менедже процессов, выполняется при общем вызове cProcessManager::Process.

Если заметил, при каждом вызове метода Process вызываются все процессы в стеке (См. Рис.2). Часто бывает очень полезно быстро вызывать опередлённые функции. Можно создать отдельные менеджеры процессов для разных ситуаций. К примеру, один обрабатывает сеть и пользовательский ввод (input), другой - работает над звуком и ещё чем-нибудь.

Пример приложения с менеджером процессов

Перед началом проверь, что у тебя установлены следующие программные компоненты:

  • MS Visual C++ 2010.

Инструкции по её установке ты найдёшь в разделе "Софт" нашего сайта.

Создаём Проект приложения

  • Создай пустой Проект с именем Process01. Проект автоматически разместится внутри Решения с таким же именем.

Весь процесс подробно расписан в статье Настройка MS Visual C plus plus 2010 и DirectX SDK.

Добавляем в Проект WinMain.cpp

Для чистоты эксперимента мы создали пустой Проект, т.е. без каких-либо файлов в нём. Создадим единственный файл с исходным кодом WinMain.cpp.

  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта Process01.
  • Во всплывающем меню Добавить->Создать элемент...
Добавляем исходный файл
Добавляем исходный файл
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи WinMain.cpp.
  • Жмём "Добавить".

Image
Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле WinMain.cpp набираем следующий код:
WinMain.cpp
/**************************************************
WinMain.cpp
Chapter 5 Stack Process Demo

Programming Role-Playing Games with DirectX
by Jim Adams (01 Jan 2002)
**************************************************/

// Include files
#include <windows.h>
#include <stdio.h>

class cProcessManager
{
  // A structure that stores a function pointer and linked list
  typedef struct sProcess {
    void  (*Function)();
    sProcess *Next;
  } sProcess;

  protected:
    sProcess *m_ProcessParent; // The top state in the stack
                               // (the head of the stack)

  public:
    cProcessManager() { m_ProcessParent = NULL; }

    ~cProcessManager() 
    {
      sProcess *ProcessPtr;

      // Remove all processes from the stack
      while((ProcessPtr = m_ProcessParent) != NULL) {
        m_ProcessParent = ProcessPtr->Next;
        delete ProcessPtr;
      }
    }

    // Add function on to the stack
    void Add(void (*Process)())
    {
      // Don't push a NULL value
      if(Process != NULL) {
        // Allocate a new process and push it on stack
        sProcess *ProcessPtr = new sProcess;
        ProcessPtr->Next = m_ProcessParent;
        m_ProcessParent = ProcessPtr;
        ProcessPtr->Function = Process;
      }
    }

    // Process all functions
    void Process()
    { 
      sProcess *ProcessPtr = m_ProcessParent;

      while(ProcessPtr != NULL) {
        ProcessPtr->Function();
        ProcessPtr = ProcessPtr->Next;
      }
    }
};

cProcessManager g_ProcessManager;

// Function prototypes
int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,          \
                   LPSTR szCmdLine, int nCmdShow);

// Macro to ease the use of MessageBox function
#define MB(s) MessageBox(NULL, s, s, MB_OK);

// Processfunction prototypes - must follow this prototype!
void Func1() { MB("1"); }
void Func2() { MB("2"); }
void Func3() { MB("3"); }

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

  return 0;
}
  • Сохрани Решение (Файл -> Сохранить все).

Вышеприведённый код (написанный в начале 2002 г.) на данном этапе в MSVC++2010 Express компилироваться также не будет.
Но мы это исправим.

Готовим Проект Process01 к компиляции

  • Выбери многобайтовую кодировку.
  • Отключи инкрементную компоновку (incremental linking).

Оба этих действия подробно рассмотрены выше, при подготовке к компиляции Проекта Shell01.

  • Сохрани Решение (Файл->Сохранить все).

Компилируем Проект Process01

Наконец, наш тестовый Проект готов к компиляции.

  • Жми кнопку с зелёным треугольником на панели инструментов главного окна MSVC++2010 или F5 нак лавиатуре.

После компилирования приложение Process01 автоматически запустится и покажет поочерёдно 3 модальных (Popup) окна с кнопкой ОК. По её нажатию одно окно закрывается и тут же появляется другое. При этом отображаемый счётчик процессов уменьшается на 1. Стэк процессов состоит из часто вызываемых функций. Каждая функция, добавленная в менеджер процессов, выполняется при вызове метода cProcessManager::Process .

Закрыть
noteОбрати внимание

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

Работа с данными приложения (Handling an Application Data)

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

Система работы с данными (Data Packaging System)

Самый простой способ организовать работу с данными приложения - это создать систему работы с данными (data packaging system), которая будет ответственна за сохранение данных игры и их последующую загрузку. Создадим объект, который содержит буфер данных (data buffer) и добавим в него несколько функций, сохраняющих и загружающих данные:

class cDataPackage
{
 protected:
  // Буфер данных и его размер
  void *m_Buf;
  unsigned long m_Size;

 public:
  cDataPackage() {m_Buf=NULL; m_Size=0;}
  `cDataPackage() {Free();}

 void *Create(unsigned long Size)
  {
   // Освобождаем ранее созданный буфер
   Free();

   // Выделяем память и возвращаем указатель
   return (m_Buf = (void*)new char[(m_Size = Size)]);
  }

 // Освобождаем ранее выделенную память
 void Free() {delete m_Buf; m_Buf = NULL; m_Size = 0;}

 BOOL Save(char *Filename)
 {
  FILE *fp;

  // Убедись, что есть, что записывать
  if(m_Buf != NULL && m_Size) {
   // Открываем файл, пишем размер данных и сами данные
   fwrite(&m_Size, 1, 4, fp);
   fwrite(m_Buf, 1, m_Size, fp);
   fclose(fp);
   return TRUE;
   }
 }

 return FALSE;

 void *Load(char *FileName, unsigned long *Size)
 {
  FILE *fp;

  // Освобождаем основной буфер
  Free()

  if((fp=open(Filename, "rb"))!=NULL
  {
   // Читаем размер данных и сами данные
   fread(&m_Size, 1, 4, fp);
   if((m_Buf = (void*) new char[m_Size]) != NULL)
   {
    fread(m_Buf, 1, m_Size, fp);
   fclose(fp);

   // Сохраняем возвращаемый размер данных
   if(Size != NULL)
    {*Size = m_Size;}

   // Возвращаем указатель
   return m_Buf;
   }

  return NULL;
  }
 }

 };

Класс cDataPackage содержит 4 функции (вообще, их 6, если считать конструктор и деструктор класса).
Функция Create выделяет блок памяти размера, заданного в её параметре (Size). Функция Free освобождает данный блок памяти. Функции Save и Load сохраняет и загружает данный, используя указанное в параметре имя файла (filename).
Обрати внимание, что функции Create и Load возвращают указатель на буфер данных. Можно подставить свой собственный указатель на буфер данных.

Проверяем систему работы с данными

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

// Структура, содержащая имена
typedef struct {char Name[32];} sName;

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR szCmdLine, int nCmdShow)
{
 cDataPackage DP;
 DWORD Size;

 // Создаём систему работы с данными (data package)
 // и передаём ей указатель на созданный пользовательский тип структуры данных sName
 sName *Names = (sName*)DP.Create(64);

 // Раз уж у нас есть под каждое имя 64 байта, а каждое имя содержит 32 байта,
 // то можно сохранять двойные имена.
 strcpy(Names[0].Name, "Jim");
 strcpy(Names[1].Name, "Adams");

 // Сохраняем имена на жёсткий диск и освобождаем буфер данных
 DP.Save("names.dat");
 DP.Free();

 // Загружаем имена с жёсткого диска. Когда функция загрузки
 // вернёт значение, его размер будет 64 байт.
 Names = (sName*)DP.Load("names.dat", &Size);

 // Показываем имена на экране
 MessageBox(NULL, Names[0].Name, "1st Name", MB_OK);
 MessageBox(NULL, Names[1].Name, "2nd Name", MB_OK);

 // Освобождаем структуру данных (data package).
 DP.Free()
}
Размер буфера данных (64 байта) достаточен для сохранения в нём обоих частей имени персонажа. Каждая из частей имени хранится в отдельном сегменте, размер которого 32 байт.
Размер буфера данных (64 байта) достаточен для сохранения в нём обоих частей имени персонажа. Каждая из частей имени хранится в отдельном сегменте, размер которого 32 байт.

Из кода видно, что 64-байтный буфер данных с именем делится на 2 блока по 32 байт каждый.

Применение системы работы с данными приложения

Возможности применения на практике системы работы с данными огромны. Оперируя двумя маленькими объектами дата-пэкэджа (data package), мы можем создавать на них указатели, при этом держа все данные приложения в едином объекте, который может сам себя сохранять и загружать.

Источники:


1. Jim Adams. Programming Role Playing Games with DirectX 8.0. - Premier Press. 2002

Contributors to this page: slymentat .
Последнее изменение страницы Воскресенье 29 / Ноябрь, 2020 00:27:16 MSK автор slymentat.

Помочь проекту

Яндекс-деньги: 410011791055108