Розробка простої гри в Code::blocks з використанням Direct3D 9

Хочу розповісти про свій перший досвід у геймдеве. Відразу варто обмовитися, що стаття буде суто технічною, оскільки моєю метою було лише отримання навичок розробки графічних додатків, що використовують Direct3D, без залучення високорівневих засобів розробки ігор типу Unity. Відповідно, ніяких розмов про впровадження, монетизації та розкрутці гри також не буде. Стаття орієнтована на новачків в програмуванні додатків Direct3D, а також просто на людей, що цікавляться ключовими механізмами роботи подібних додатків. Також в кінці наводжу список літератури геймдеву, ретельно відібраний мною з більш ніж ста книг по програмуванню ігор та комп'ютерної графіки.

Введення

Отже, у вільний від роботи час я вирішив вивчити популярний графічний API. Прочитавши кілька книжок і розібравши купу прикладів і туториалов (у тому числі з DirectX SDK), я усвідомив, що настав той самий момент, коли варто спробувати свої сили самостійно. Основна проблема була в тому, що більшість існуючих прикладів просто демонструють ту або іншу можливість API і реалізовані процедурно мало не в одному cpp-файлі, та ще й з використанням обгортки DXUT, і не дають уявлення про те, яку структуру повинна мати цільовий застосунок, які класи потрібно спроектувати і як це все має один з іншому взаємодіяти, щоб все було красиво, читабельно і ефективно працювало. Цей недолік стосується і книг з Direct3D: наприклад, для багатьох новачків не є очевидним той факт, що фонового стану (render states) не завжди потрібно оновлювати при відображенні кожного кадру, а також, що більшість великовагових операцій (типу заповнення вершинного буфера) слід виконати лише один раз при ініціалізації програми (або при завантаженні ігрового рівня).

Ідея

Першим ділом мені необхідно було визначитися з самою ідеєю гри. На розум відразу прийшла стара гра з 1992 року під MS-DOS, яка, я думаю, багатьом знайома. Це логічна гра Lines компанії Gamos.

Що ж, виклик прийнятий. Ось що ми маємо:
  • є квадратне поле з комірок;
  • у клітинках на полі є різнокольорові кулі;
  • після переміщення кулі з'являються нові кулі;
  • мета гравця в тому, щоб вибудовувати одноколірні кулі в лінії: накопичення в одній лінії певного числа одноколірних куль призводить до їх детонації і нарахування очок;
  • головне завдання — протриматися якомога довше, поки на полі не закінчаться вільні комірки.
Тепер подивимося з точки зору застосування Direct3D:
  • поле клітинок зробимо у вигляді тривимірної платформи з виступами, кожна клітинка буде представляти із себе щось типу подіуму;
  • повинно бути реалізовано три види анімації куль:
    1. поява кулі: спочатку з'являється маленький куля, який за короткий час виростає до дорослого розміру;
    2. переміщення кулі: просто відбувається послідовне переміщення по комірках;

    3. пригающій м'яч: при виборі кулі мишею він повинен активізуватися і почати стрибати на місці;
  • повинна бути реалізована система частинок, яка буде використовуватися при анімації вибуху;
  • повинен бути реалізований висновок тексту: для відображення на екрані зароблених очок;
  • повинно бути реалізовано управління віртуальною камерою: обертання і зум.
По суті справи пункти, перераховані вище, є жалюгідною подобою документа під назвою дизайн-проект. Настійно рекомендую перед початком розробки розписати в ньому все до дрібних подробиць, роздрукувати і тримати перед очима! Забігаючи вперед відразу показую демо-ролик для наочності реалізації пунктів (до речі, відео записано за допомогою програми ezvid, так що не лякайтеся їх сплеш-скрін на початку):



Початок розробки

Досі я не згадував, які інструменти використовувалися. По-перше, необхідний DirectX software development kit (SDK), завжди доступний для вільного скачування на сайті Microsoft: DirectX SDK. Якщо ви збираєтеся використовувати версію Direct3D 9, як я, то після установки необхідно через головне меню відкрити DirectX Control Panel і на вкладці Direct3D 9 вибрати, яка версія бібліотек буде використовуватися при складанні — retail або debug (це впливає на те, чи буде Direct3D повідомляти відладчику про результати своєї діяльності):

Debug або retail

Чому 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 — наприклад, так:
Search directories

2) На вкладці linker додати дві бібліотеки — d3d9.lib і d3dx9.lib:
Linker

Після цього у вихідному коді програми потрібно буде включити відмінності файли Direct3D:

#include "d3d9.h"
#include "d3dx9.h"

Структура програми

Тут я припустився першу помилку: почав роздумувати над тим, який шаблон проектування вибрати. Прийшов до висновку, що краще всього підходить MVC (model-view-controller): моделлю буде клас гри (game), що включає всю логіку — обчислення шляхів переміщення, поява куль, розбір вибухових комбінацій; поданням буде клас двигуна (engine), який відповідає за малювання і взаємодія з Direct3D; контролером буде власне обгортка (app) — сюди входить цикл обробки повідомлень, обробка користувальницького введення, а також, що найголовніше, менеджер станів і забезпечення взаємодії об'єктів game engine і. Начебто все просто, і можна починати писати відмінності файли, але не тут-то було! На цьому етапі виявилося дуже складно зорієнтуватися і зрозуміти, які методи повинні бути в цих класів. Зрозуміло, що позначилося на повну відсутність досвіду, і я вирішив скористатися порадою однієї з книг: «Не намагайтеся з самого початку написати ідеальний код, нехай він буде неоптимальним і сумбурним. Розуміння приходить з часом, і рефакторінгом можна зайнятися потім.» В підсумку після декількох ітерацій рефакторінгу вже працюючого макета визначення трьох основних класів прийняв вигляд:

Клас TGame
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();
};


Клас TEngine
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();
};


Клас TApplication
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 не хочеться докладно зупинятися, оскільки є исходники з коментарями. Скажу тільки, що визначення шляху переміщення кулі реалізовано за допомогою алгоритму Дейкстри неориентированному графу з рівними вагами всіх ребер.

Розглянемо докладніше класи движка і контролера.

TEngine

  • В класі визначені поля для зберігання handle вікна і його прямокутника. Вони використовуються в методі OnResize, який контролер викликає при зміні розмірів вікна, для розрахунку нової матриці проекції.
  • У полі CameraPos зберігаються координати спостерігача у світовому просторі. Вектор напрямку погляду зберігати не потрібно, оскільки за моєю задумом камера завжди спрямована в початок координат, яке, до речі, збігається з центром платформи.
  • Також є покажчики на інтерфейси Direct3D: LPDIRECT3D9, який потрібен тільки для створення пристрою; LPDIRECT3DDEVICE9 — власне, сам пристрій Direct3D, основний інтерфейс, з яким доводиться працювати; LPD3DXFONT і LPDIRECT3DTEXTURE9 для роботи з текстом і текстурою.
  • Поле currentTime використовується для зберігання поточного часу в мілісекундах і необхідно для плавного відтворення анімації. Справа в тому, що відтворення кожного кадру займає різну кількість мілісекунд, тому доводиться кожен раз заміряти ці мілісекунди і використовувати в якості параметра при інтерполяцію анімації. Такий спосіб відомий під назвою синхронізація часом і використовується повсюдно в сучасних графічних додатках.
  • Покажчики на об'єкти класу TGeometry (cellGeometry і ballGeometry) зберігають геометрію однієї комірки і однієї кулі. Сам по собі об'єкт TGeometry, як зрозуміло з назви, призначений для роботи з геометрією і містить вершинні і індексні буфери, а також опис матеріалу (D3DMATERIAL9). При відображенні ми можемо змінювати світову матрицю і викликати метод Render об'єкта TGeometry, що призведе до візуалізації декількох комірок або куль.
  • TParticleSystem — це клас системи частинок, має методи для ініціалізації безлічі частинок, оновлення їх позицій у просторі і, звичайно ж, рендеринга.
  • TBall *balls — масив куль з інформацією про кольорі і статус [підстрибуючи, що переміщається, з'являється].
  • Три об'єкта типу TAnimate — для забезпечення анімації. Клас має метод для ініціалізації ключових кадрів, які являють собою матриці світового перетворення, і методи для обчислення поточної позиції анімації та застосування перетворення. У процедурі візуалізації об'єкт engine послідовно розмальовує кулі і при необхідності викликає метод ApplyTransform потрібної анімації, щоб сдеформировать або перемістити кулю.
  • InitD3d, InitGeometry, InitAnimation викликаються тільки з конструктора TEngine і виділені в окремі методи для наочності. У InitD3d створюється пристрій Direct3D і встановлюються всі необхідні render state'и, включаючи встановлення точкового джерела освітлення з specular-складової прямо над центром платформи.
  • Три методу AppearBalls, MoveBall і DetonateBalls запускають анімації появи, переміщення і вибуху, відповідно.
  • Методи IsSelected, IsMoving, IsAppearing, IsDetonating використовуються у функції менеджера станів для відстеження моменту закінчення анімації.
  • Методи з префіксом On викликаються контролером при виникненні відповідних подій: кліка мишею, обертанні камери і т. д.
Розглянемо основний метод Render:

TEngine::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.0 f,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 і послідовно малюються платформа, кулі і система часток, якщо вона активна. Наостанок виводиться рядок з поточним значенням зароблених очок.

TApplication

  • Клас має поле для зберігання координат миші, оскільки нам необхідно буде обчислювати значення відносних зміщень стрілками, щоб обертати камеру.
  • Булева прапори appearStarted, moveStarted і detonateStarted необхідні для відстеження статусу відповідних анімацій.
  • метод RegWindow винесено код для реєстрації класу вікна.
  • Static-метод MsgProc — так звана віконна процедура.
  • ProcessGame — спрощена версія менеджера станів, в якому оцінюється поточний стан гри і в залежності від нього робляться якісь дії.
  • MainLoop — цикл обробки повідомлень.
Ось такий легкий контролер. Подібний цикл обробки повідомлень можна зустріти в будь-якій книзі по Direct3D:

TApplication::MainLoop()
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, яка виконується при відсутності повідомлень.

А ось і функція менеджера станів:

TApplication::ProcessGame()
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.uk.

Сподіваюся, мої вишукування принесуть комусь користь. До речі, исходники на github.

Дякую за увагу!

Обіцяна література

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, так і прикладні глави, які мають безпосереднє відношення до ігрового движка.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.