- PVSM.RU - https://www.pvsm.ru -
Хочу рассказать о своем первом опыте в геймдеве. Сразу стоит оговориться, что статья будет чисто технической, поскольку моей целью было всего лишь получение навыков разработки графических приложений, использующих Direct3D, без привлечения высокоуровневых средств разработки игр типа Unity. Соответственно, никаких разговоров о внедрении, монетизации и раскрутке игры также не будет. Статья ориентирована на новичков в программировании приложений Direct3D, а также просто на людей, интересующихся ключевыми механизмами работы подобных приложений. Также в конце привожу список литературы по геймдеву, тщательно отобранный мной из более чем ста книг по программированию игр и компьютерной графики.
Итак, в свободное от работы время решил я изучить популярный графический API. Прочитав несколько книг и разобрав кучу примеров и туториалов (в том числе из DirectX SDK), я осознал, что настал тот самый момент, когда стоит попробовать свои силы самостоятельно. Основная проблема была в том, что большинство существующих примеров просто демонстрируют ту или иную возможность API и реализованы процедурно чуть ли не в одном cpp-файле, да еще и с использованием обертки DXUT, и не дают представления о том, какую структуру должно иметь конечное приложение, какие классы нужно спроектировать и как это все должно друг с другом взаимодействовать, чтобы все было красиво, читабельно и эффективно работало. Данный недостаток касается и книг по Direct3D: например, для многих новичков не является очевидным тот факт, что состояния рендера (render states) не всегда нужно обновлять при отрисовке каждого кадра, а также, что большинство тяжеловесных операций (типа заполнения вершинного буфера) следует выполнить всего один раз при инициализации приложения (либо при загрузке игрового уровня).
Первым делом мне необходимо было определиться с самой идеей игры. На ум сразу пришла старая игра из 1992 года под MS-DOS, которая, я думаю, многим знакома. Это логическая игра Lines [1] компании Gamos.
Что ж, вызов принят. Вот что мы имеем:
Теперь посмотрим с точки зрения приложения Direct3D:
По сути дела пункты, перечисленные выше, являются жалким подобием документа под названием дизайн-проект. Настоятельно рекомендую перед началом разработки расписать в нем все до мелких подробностей, распечатать и держать перед глазами! Забегая вперед сразу показываю демо-ролик для наглядности реализации пунктов (кстати, видео записано с помощью программы ezvid [2], так что не пугайтесь их сплэш-скрина в начале):
До сих пор я не упоминал, какие инструменты использовались. Во-первых, необходим DirectX software development kit (SDK), всегда доступный для свободного скачивания на сайте Microsoft: DirectX SDK [3]. Если вы собираетесь использовать версию Direct3D 9, как я, то после установки необходимо через главное меню открыть DirectX Control Panel и на вкладке Direct3D 9 выбрать, какая версия библиотек будет использоваться при сборке — retail либо debug (это влияет на то, будет ли Direct3D сообщать отладчику о результатах своей деятельности):
Почему Direct3D 9-й версии? Потому что это последняя версия, где все еще есть fixed function pipeline, то есть фиксированный графический конвейер, включающий в себя, например, функции расчета освещения, обработки вершин, смешивания и так далее. Начиная с 10-й версии, разработчикам предлагается самостоятельно реализовывать эти функции в шейдерах, что является неоспоримым преимуществом, но, на мой взгляд, сложновато для восприятия при первых опытах с Direct3D.
Почему Code::blocks? Наверное, глупо было использовать кросс-платформенную IDE для разработки приложения, использующего некросс-платформенный API. Просто Code::blocks занимает в несколько раз меньше места, чем Visual Studio, что оказалось очень актуальным для моего дачного ПК.
Старт разработки с Direct3D оказался очень прост. В Code::blocks я создал пустой проект (empty project), затем в build options нужно было сделать две вещи:
1) На вкладке search directories и подвкладке compiler добавить путь к директории include DirectX SDK — например, так:
2) На вкладке linker добавить две библиотеки — d3d9.lib и d3dx9.lib:
После этого в исходном коде приложения нужно будет включить заголовочные файлы Direct3D:
#include "d3d9.h"
#include "d3dx9.h"
Здесь я допустил первую ошибку: начал размышлять над тем, какой шаблон проектирования выбрать. Пришел к выводу, что лучше всего подходит MVC (model-view-controller): моделью будет класс игры (game), включающий всю логику — вычисление путей перемещения, появление шаров, разбор взрывных комбинаций; представлением будет класс движка (engine), отвечающий за отрисовку и взаимодействие с Direct3D; контроллером будет собственно обертка (app) — сюда входит цикл обработки сообщений, обработка пользовательского ввода, а также, что самое главное, менеджер состояний и обеспечение взаимодействия объектов game и engine. Вроде бы все просто, и можно начинать писать заголовочные файлы, но не тут-то было! На этом этапе оказалось очень сложно сориентироваться и понять, какие методы должны быть у этих классов. Понятно, что сказалось полное отсутствие опыта, и я решил прибегнуть к совету одной из книг: «Не пытайтесь с самого начала написать идеальный код, пусть он будет неоптимальным и сумбурным. Понимание приходит со временем, и рефакторингом можно заняться потом.» В итоге после нескольких итераций рефакторинга уже работающего макета определение трех основных классов приняло вид:
class TGame {
private:
BOOL gameOver;
TCell *cells;
WORD *path;
WORD pathLen;
LONG score;
void ClearField();
WORD GetSelected();
WORD GetNeighbours(WORD cellId, WORD *pNeighbours);
BOOL CheckPipeDetonate(WORD *pPipeCells);
public:
TGame();
~TGame();
void New();
BOOL CreateBalls(WORD count);
void Select(WORD cellId);
BOOL TryMove(WORD targetCellId);
BOOL DetonateTest();
WORD GetNewBallList(TBallInfo **ppNewList);
WORD GetLastMovePath(WORD **ppMovePath);
WORD GetDetonateList(WORD **ppDetonateList);
LONG GetScore();
BOOL IsGameOver();
};
class TEngine {
private:
HWND hWindow;
RECT WinRect;
D3DXVECTOR3 CameraPos;
LPDIRECT3D9 pD3d;
LPDIRECT3DDEVICE9 pDevice;
LPDIRECT3DTEXTURE9 pTex;
LPD3DXFONT pFont;
D3DPRESENT_PARAMETERS settings;
clock_t currentTime;
TGeometry *cellGeometry;
TGeometry *ballGeometry;
TParticleSystem *psystem;
TBall *balls;
TAnimate *jumpAnimation;
TAnimate *moveAnimation;
TAnimate *appearAnimation;
LONG score;
void InitD3d();
void InitGeometry();
void InitAnimation();
void DrawPlatform();
void DrawBalls();
void UpdateView();
public:
TEngine(HWND hWindow);
~TEngine();
void AppearBalls(TBallInfo *ballInfo, WORD count);
void MoveBall(WORD *path, WORD pathLen);
void DetonateBalls(WORD *detonateList, WORD count);
BOOL IsSelected();
BOOL IsMoving();
BOOL IsAppearing();
BOOL IsDetonating();
void OnResetGame();
WORD OnClick(WORD x, WORD y, BOOL *IsCell);
void OnRotateY(INT offset);
void OnRotateX(INT offset);
void OnZoom(INT zoom);
void OnResize();
void OnUpdateScore(LONG score);
void Render();
};
class TApplication {
private:
HINSTANCE hInstance;
HWND hWindow;
POINT mouseCoords;
TEngine* engine;
TGame* game;
BOOL moveStarted;
BOOL detonateStarted;
BOOL appearStarted;
void RegWindow();
static LRESULT CALLBACK MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
void ProcessGame();
public:
TApplication(HINSTANCE hInstance, INT cmdShow);
~TApplication();
TEngine* GetEngine();
TGame* GetGame();
INT MainLoop();
};
Класс TGame имеет всего 3 метода, которые может инициировать сам пользователь — New (новая игра), Select (выбор шара) и TryMove (попытка переместить шар). Остальные вспомогательные и вызываются контроллером в особых случаях. Например, DetonateTest (тест на взрывные комбинации) вызывается после появления новых шаров или после попытки перемещения. GetNewBallList, GetLastMovePath, GetDetonateList вызываются, соответственно, после появления шаров, после перемещения и после взрыва, с одной целью: получить список конкретных шаров и передать его на обработку объекту engine, чтобы он что-то нарисовал. На логике работы TGame не хочется подробно останавливаться, поскольку есть исходники с комментариями [4]. Скажу только, что определение пути перемещения шара реализовано с помощью алгоритма Дейкстры [5] по неориентированному графу с равными весами всех ребер.
Рассмотрим подробнее классы движка и контроллера.
Рассмотрим основной метод Render:
void TEngine::Render()
{
//вычисляем, сколько миллисекунд прошло с момента отрисовки предыдущего кадра
clock_t elapsed=clock(), deltaTime=elapsed-currentTime;
currentTime=elapsed;
//обновляем позиции анимаций, если они активны
if(jumpAnimation->IsActive())
{
jumpAnimation->UpdatePosition(deltaTime);
}
if(appearAnimation->IsActive())
{
appearAnimation->UpdatePosition(deltaTime);
}
if(moveAnimation->IsActive())
{
moveAnimation->UpdatePosition(deltaTime);
}
pDevice->Clear(0,NULL,D3DCLEAR_STENCIL|D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,D3DCOLOR_XRGB(0,0,0),1.0f,0);
pDevice->BeginScene();
//рисуем платформу
DrawPlatform();
//рисуем шары
DrawBalls();
//если активна система частиц, то обновляем положения частиц и рендерим их с текстурой
if(psystem->IsActive())
{
pDevice->SetTexture(0,pTex);
psystem->Update(deltaTime);
psystem->Render();
pDevice->SetTexture(0,0);
}
//вывод заработанных очков
char buf[255]="Score: ",tmp[255];
itoa(score,tmp,10);
strcat(buf,tmp);
RECT fontRect;
fontRect.left=0;
fontRect.right=GetSystemMetrics(SM_CXSCREEN);
fontRect.top=0;
fontRect.bottom=40;
pFont->DrawText(NULL,_T(buf),-1,&fontRect,DT_CENTER,D3DCOLOR_XRGB(0,255,255));
pDevice->EndScene();
pDevice->Present(NULL,NULL,NULL,NULL);
}
В самом начале вычисляется, сколько миллисекунд прошло с предыдущего вызова Render(), затем обновляются прогрессы анимаций, если они активны. Очищаются буферы методом Clear и последовательно рисуются платформа, шары и система частиц, если она активна. Напоследок выводится строка с текущим значением заработанных очков.
Вот такой легковесный контроллер. Подобный цикл обработки сообщений можно встретить в любой книге по Direct3D:
INT TApplication::MainLoop()
{
MSG msg;
ZeroMemory(&msg,sizeof(MSG));
while(msg.message!=WM_QUIT)
{
if(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
//если нет сообщений, то обрабатываем состояние игры и занимаемся рендерингом
ProcessGame();
engine->Render();
}
}
return (INT)msg.wParam;
}
Внимания заслуживает только то, что находится внутри блока else — это так называемая IdleFunction, которая выполняется при отсутствии сообщений.
А вот и функция менеджера состояний:
void TApplication::ProcessGame()
{
if(moveStarted)
{
//ждем до окончания анимации перемещения
if(!engine->IsMoving())
{
//перемещение окончено - тестируем на взрыв
moveStarted=FALSE;
if(game->DetonateTest())
{
//инициируем взрыв и увеличиваем очки
WORD *detonateList,
count=game->GetDetonateList(&detonateList);
detonateStarted=TRUE;
engine->DetonateBalls(detonateList,count);
engine->OnUpdateScore(game->GetScore());
}
else
{
//иначе пытаемся добавить шары
if(game->CreateBalls(APPEAR_COUNT))
{
TBallInfo *appearList;
WORD count=game->GetNewBallList(&appearList);
appearStarted=TRUE;
engine->AppearBalls(appearList,count);
}
else
{
//game over!
}
}
}
}
if(appearStarted)
{
//ждем до окончания анимации появления
if(!engine->IsAppearing())
{
appearStarted=FALSE;
//появление окончено - тестируем на взрыв на всякий случай
if(game->DetonateTest())
{
//инициируем взрыв и увеличиваем очки
WORD *detonateList,
count=game->GetDetonateList(&detonateList);
detonateStarted=TRUE;
engine->DetonateBalls(detonateList,count);
engine->OnUpdateScore(game->GetScore());
}
}
}
if(detonateStarted)
{
//ждем до окончания анимации взрыва
if(!engine->IsDetonating())
{
//просто сбрасываем флаг
detonateStarted=FALSE;
}
}
}
Что ж, пожалуй, на этом все!
Здесь самое место, чтобы перечислить недостатки. Разумеется, в коде куча мест для оптимизаций. Кроме того, я не упомянул о таких вещах, как смена параметров видеорежима (разрешение экрана, multisampling) и обработка потери устройства (LostDevice). На счет последнего имеется подробное обсуждение на сайте gamedev.ru [6].
Надеюсь, мои изыскания принесут кому-то пользу. Кстати, исходники на github [4].
Спасибо за внимание!
1. Frank D. Luna Введение в программирование трехмерных игр с DirectX 9.0 — для понимания основ;
2. Горнаков С. DirectX9 уроки программирования на С++ — тоже основы, но есть главы по DirectInput, DirectSound и DirectMusic. В примерах программ иногда встречаются ошибки;
3. Фленов М. Е. DirectX и C++ искусство программирования — забавный стиль изложения. В основном, целью книги является создание анимационных роликов с использованием интересных эффектов, в том числе с шейдерами. Судите сами по названию разделов: сердечный приступ, огненный дракон;
4. Баррон Тодд Программирование стратегических игр с DirectX 9 — полностью посвящена темам, связанным со стратегическими играми: блочная графика, ИИ, создание карт и ландшафтов, спрайты, спецэффекты с системами частиц, а также разработка экранных интерфейсов и работа с DirectSound/Music;
5. Bill Fleming 3D Creature WorkShop — книга не по программированию, а по разработке трехмерных моделей персонажей в средах LightWave, 3D Studio Max, Animation Master;
6. Thorn Alan DirectX 9 User interfaces Design and implementation — подробная книга о разработке графических интерфейсов с DirectX. Рассматривается иерархическая модель компонентов экранных форм, подобная реализованной в Delphi;
7. Adams Jim Advanced Animation with DirectX — рассматриваются типы анимации (скелетная, морфинг и разновидности) и их реализация, а также работа с геометрией и анимацией из X-файлов;
8. Ламот Андре Программирование игр для Windows. Советы профессионала — эта книга уже посерьезнее: рассматриваются вопросы оптимизации, выбора структур данных под различные задачи, многопоточность, физическое моделирование, ИИ. В последней главе описывается создание игры про космический корабль и пришельцев;
9. David H. Eberly 3D Game engine design — хорошая книга для понимания всей теории игростроения: сначала описываются технологии графических API (трансформации, растеризация, затенение, смешивание, мультитекстурирование, туман и т.д.), затем такие темы, как граф сцены, выбор объектов, определение столкновений, анимация персонажей, level of detail, ландшафты;
10. Daniel Sánchez-Crespo Dalmau Core techniques and algorithmes in game programming — подробно рассматриваются алгоритмы и структуры данных, используемые в задачах игростроения, такие как ИИ, скриптинг, рендеринг в закрытых и в открытых пространствах, алгоритмы отсечения, процедурные техники, способы реализации теней, реализация камеры и т.д.;
11. Ламот Андре Programming Role Playing Games with directX 9 — тысячестраничное подробное руководство по разработке RPG. Включает как теоретические главы по программированию с Direct3D, DirectInput, DirectSound, DirectPlay, так и прикладные главы, имеющие непосредственное отношение к игровому движку.
Автор: boeing777
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/83509
Ссылки в тексте:
[1] Lines: https://ru.wikipedia.org/wiki/Lines
[2] ezvid: http://www.ezvid.com/
[3] DirectX SDK: http://www.microsoft.com/en-us/download/details.aspx?id=6812
[4] исходники с комментариями: https://github.com/psylancer/balls
[5] алгоритма Дейкстры: https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC_%D0%94%D0%B5%D0%B9%D0%BA%D1%81%D1%82%D1%80%D1%8B
[6] на сайте gamedev.ru: http://www.gamedev.ru/code/forum/?id=81910
[7] Источник: http://habrahabr.ru/post/251081/
Нажмите здесь для печати.