Загрузка...
 
Печать

Создание каркаса (framework) игры


Intro

Каждый раз при создании новой игры набирать одни и те же функции и классы не рационально.1 На деле игрокодеры создают одну или несколько lib-библиотек (поддерживаются всеми версиями MS Visual C++), содержащие все необходимые функции, и затем подключают их в свои Проекты, экономя кучу времени. В этом и заключается идея фреймворка (=каркаса) приложения (application framework).
В самом простом случае фреймворк должен содержать код:

  • инициализации и создания окна;
  • различных движков (графического, звукового, ввода, сети и т.д.);
  • инициализации игры;
  • покадровых процедур (per-frame routines);
  • функции завершения работы игрового приложения.

Применение модульного подхода программирования (modular-coding techniques) также полезно, т.к. все основные компоненты (например движки) могут содержаться в отдельных файлах исходного кода.

Структурирование проекта (Structuring a Project)

Исходный код в любом Проекте можно организовать разными путями. Можно собрать весь код в одном .cpp-файле, либо разделить на несколько отдельных файлов, исходя из функционала. Например, графические функции можно разместить в файле graphics.cpp, а их объявления вынести в заголовочный файл graphics.h (не забыв, конечно, прописать в graphics.cpp соответствующую инструкцию include).
Jim Adams в своей книге "Programming Role Playing Games with DirectX 8.0" пишет, что всегда начинает свои проекты с одного файла WinMain.cpp, в котором сосредоточен весь исходный код. Здесь инициализируется окно приложения, вызываются необходимые (авторские) движковые функции (DoInit, DoPreFrame, DoFrame и т.д.). Уже по мере разрастания кода игрокодер выносит код отдельных групп функций (графика, звук, сеь и т.д.) в отдельные .cpp-файлы (с сопутствующими им заголовками .h). Позднее, при создании игры, достаточно просто подключить к своему исходнику соответствующий заголовок и создать объекты (экземпляры, инстансы) отдельных классов, которые будут задействованы. На быстродействие такие манипуляции никак не влияют. Зато позволят не заблудиться в собственном коде.

Пример простейшего фреймворка (framework) игры

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

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

  • MS Visual C++ 2010.

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

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

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

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

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

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

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

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

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

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

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

// Main application instances
HINSTANCE g_hInst;  // Глобальный дескриптор экземпляра (Global Instance handler).
HWND g_hWnd;  // Глобальный дескриптор окна (Global window handle).

// Application window dimensions, type, class and caption text
#define WNDWIDTH 400
#define WNDHEIGHT 400
#define WNDTYPE WS_OVERLAPPEDWINDOW
static char   g_szClass[]   = "FrameClass";
static char   g_szCaption[] = "Base Framework Application";

// Main application prototypes

// Entry point
int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev,          \
                   LPSTR szCmdLine, int nCmdShow);

// Function to display an error message.
void AppError(BOOL Fatal, char *Text, ...);

// Message procedure
long FAR PASCAL WindowProc(HWND hWnd, UINT uMsg,              \
                           WPARAM wParam, LPARAM lParam);

// Functions to register and unregister Windows' classes
BOOL RegisterWindowClasses(HINSTANCE hInst);
BOOL UnregisterWindowClasses(HINSTANCE hInst);

// Function to create the application window
HWND CreateMainWindow(HINSTANCE hInst);

// Functions to init, shutdown and handle per-frame functions
BOOL DoInit();
BOOL DoShutdown();
BOOL DoPreFrame();
BOOL DoFrame();
BOOL DoPostFrame();

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

  // Save application instance
  g_hInst = hInst;

  // Register windows' classes. Return on False
  if(RegisterWindowClasses(hInst) == FALSE)
   return FALSE;

  // Create window. Return on False
  if((g_hWnd = CreateMainWindow(hInst)) == NULL)
   return FALSE;

  // Do application initialization. Return on FALSE
  if(DoInit() == TRUE)
  {
   // Enter the 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);
     }
     else
     {
     // Do pre-frame processing. Break on FALSE return value
     if(DoPreFrame() == FALSE)
      break;

     // Do per-frame processing. Break on FALSE return value
     if(DoFrame() == FALSE)
      break;

     // Do post-frame processing. Break on FALSE return value
     if(DoPostFrame() == FALSE)
      break;
     }
    }
   }

  // Do shutdown functions
  DoShutdown();
  
  // Unregister window
  UnregisterWindowClasses(hInst);
  return TRUE;
}  // закрывающая скобка функции WinMain

BOOL RegisterWindowClasses(HINSTANCE hInst)  // Авторская функция-обёртка (wrapper-function).
{
 WNDCLASSEX wcex;

 // 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;

 return TRUE;
}

BOOL UnregisterWindowClasses(HINSTANCE hInst)
{
 // Unregister the window class
 UnregisterClass(g_szClass, hInst);
 return TRUE;
}

HWND CreateMainWindow(HINSTANCE hInst)  // Авторская функция-обёртка (wrapper-function).
{
 HWND hWnd;

 // Create the Main Window
 hWnd = CreateWindow(g_szClass, g_szCaption,
        WNDTYPE,
        0, 0, WNDWIDTH, WNDHEIGHT,  // Словесные константы объявлены в начале листинга.
        NULL, NULL,
        hInst, NULL );
  if(!hWnd)
    return FALSE;
  ShowWindow(hWnd, SW_NORMAL);
  UpdateWindow(hWnd);

 return hWnd;
}

void AppError(BOOL Fatal, char *Text, ...)
{
 char CaptionText[12];
 char ErrorText[2048];
 va_list valist;

 // Создаём заголовок окна ошибки на основе fatal flag.
 if(Fatal == FALSE)
  strcpy(CaptionText, "Error");
 else
  strcpy(CaptionText, "Fatal Error");

 // Build variable text buffer
 va_start(valist, Text);
 vsprintf(ErrorText, Text, valist);
 va_end(valist);

 // Display the message box
 MessageBox(NULL, ErrorText, CaptionText,
    MB_OK | MB_ICONEXCLAMATION);

 // Посылаем окну сообщение QUIT, если ошибка была фатальной.
 if(Fatal == TRUE)
  PostQuitMessage(0);
}

// The message procedure. Empty except for destroy message
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()
{
 // Здесь выполняем функции инициализации приложения.
 // Например те, что настраивают графику, звук, сеть и т.д.
 // В случае успеха возвращает TRUE. При неудачном выполнении - FALSE.
  return TRUE;
}

BOOL DoShutdown()
{
 // Здесь выполняем функции завершения работы приложения.
 // Например те, что освобождают память от графики, звука, сети и т.д.
 // В случае успеха возвращает TRUE. При неудачном выполнении - FALSE.
  return TRUE;
}

BOOL DoPreFrame()
{
 // Здесь выполняем функции предкадровой обработки.
 // Например устанавливаем таймер.
 // В случае успеха возвращает TRUE. При неудачном выполнении - FALSE.
  return TRUE;
}

BOOL DoFrame()
{
 // Здесь выполняем функции покадровой обработки.
 // Например рендеринг.
 // В случае успеха возвращает TRUE. При неудачном выполнении - FALSE.
  return TRUE;
}

BOOL DoPostFrame()
{
 // Здесь выполняем функции послекадровой обработки.
 // Например синхронизация времени.
 // В случае успеха возвращает TRUE. При неудачном выполнении - FALSE.
  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 компилироваться не будет, выдавая многочисленные ошибки.
Но мы это исправим.

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

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

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

Напомним, что такую настройку необходимо повторно проделывать при создании каждого нового Проекта. Ниже представлен алгоритм действий по настройке Проекта Framework01, созданного в 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 запущена и в ней открыт наш текущий Проект Framework01.
  • В Обозревателе решений щёлкаем правой кнопкой мыши по названию Проекта Framework01.
  • Во всплывающем контекстном меню выбираем "Свойства".
  • В появившемся окне установки свойств Проекта жмём Свойства конфигурации->Общие, в правой части в строке "Набор символов" выставляем значение "Использовать многобайтовую кодировку".

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

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

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

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

Несмотря на то, что мы неоднократно поменяли местами функции и объявления (по сравнению с примером оконного приложения из статьи Создание приложений (Cpp, Win32) + использовали классы-обёртки, код скомпилировался успешно. Ряд предупреждений (warnings) об устаревших (deprecated) функциях = не в счёт. После компилирования приложение Framework01 автоматически запустится и покажет окно c белым фоном. В данном случае функции DoInit(), DoPreFrame(), DoFrame и DoPostFrame() ничего не делают, но служат своеобразной заготовкой будущего алгоритма программного потока. Имена функций, конечно, игрокодер выбирает на своё усмотрение.

Исследуем код WinMain.cpp

Весь код WinMain.cpp инициализирует окно приложения и входит в цикл выборки сообщений (message pump), ожидая единственного системного сообщения WM_QUIT, завершающего работу программы. Весь листинг построен так: сначала объявления/вызов функций, а в конце их реализация. В конце оказалось даже заполнение оконного класса. Этим и отличается профессиональный код от кода из учебников по C++ для начинающих. Все движковые функции размещены так, чтобы программер мог легко их обслуживать без ущерба стабильности. При заполнении оконного класса мы прописали NULL в элементе hbrBackground, отвечающего за фоновую заливку окна:

Фрагмент WinMain.cpp
...
wcex.hbrBackground = NULL;
...

Это норма для DirectX Graphics приложений, т.к. именно этот компонент будет "заведовать" заливкой окна в будущем. Тем не менее при такой настройке по факту получили белую заливку окна.
Многие параметры, как например ширина (width), высота (height) и тип окна, объявлены в начале листинга в виде констант. Это удобно для их быстрого обнаружения в коде и "централизованного" внесения изменений. Также в константы вынесены название оконного класса (window class) и заголовок (caption) окна.
Самый интересный фрагмент кода это цикл выборки сообщений:

Фрагмент WinMain.cpp
...
  // Do application initialization. Return on FALSE
  if(DoInit() == TRUE)
  {
   // Enter the 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);
     }
     else
     {
     // Do pre-frame processing. Break on FALSE return value
     if(DoPreFrame() == FALSE)
      break;

     // Do per-frame processing. Break on FALSE return value
     if(DoFrame() == FALSE)
      break;

     // Do post-frame processing. Break on FALSE return value
     if(DoPostFrame() == FALSE)
      break;
     }
    }
   }

  // Do shutdown functions
  DoShutdown();
  
  // Unregister window
  UnregisterWindowClasses(hInst);
  return TRUE;
}  // закрывающая скобка функции WinMain
...

Сам цикл обрамлён в условный переход, где проверяется выполнение функции DoInit. Если на ней программа "споткнулась", значит всё остальное - тлен. В остальном это стандартный цикл выборки сообщений с функциями TranslateMessage и DispatchMessage. Через оператор else (англ. "ещё") добавлены вызовы "движковых" функций DoPreFrame, DoFrame, DoPostFrame, каждая из которых также проверяется на успешность выполнения.
Перед снятием регистрации оконного класса идёт вызов движковой функции DoShutdown, выполняющей очищающие операции. В конце листинга идут (пока пустые) реализации всех вышеперечисленных движковых функций.

Функция AppError

  • Авторская функция, которая (пока) нигде в коде не вызывается.
  • Применяется для отображения пользователю информативных сообщений об ошибках.
Фрагмент WinMain.cpp
...
void AppError(BOOL Fatal, char *Text, ...)
{
 char CaptionText[12];
 char ErrorText[2048];
 va_list valist;

 // Создаём заголовок окна ошибки на основе fatal flag.
 if(Fatal == FALSE)
  strcpy(CaptionText, "Error");
 else
  strcpy(CaptionText, "Fatal Error");

 // Build variable text buffer
 va_start(valist, Text);
 vsprintf(ErrorText, Text, valist);
 va_end(valist);

 // Display the message box
 MessageBox(NULL, ErrorText, CaptionText,
    MB_OK | MB_ICONEXCLAMATION);

 // Посылаем окну сообщение QUIT, если ошибка была фатальной.
 if(Fatal == TRUE)
  PostQuitMessage(0);
}
...

Все ошибки поделены на 2 вида: Fatal и не очень. При передаче параметру Fatal значения TRUE приложение принудительно завершает работу. При значении FALSE приложению разрешено продолжить работу после закрытия всплывающего окна сообщения об ошибке. Грамотная обработка возникающих ошибок также является признаком профессионализма.

Источники:


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

Contributors to this page: slymentat .
Последнее изменение страницы Понедельник 30 / Ноябрь, 2020 01:58:11 MSK автор slymentat.

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

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