Своя 2D інді гра на Delphi (7) без DirectX – це просто! - Створення ігор - Статті зі створення ігор

Стаття буде поповнюватися частинами і не відразу (вибачте, але створення повноцінної статті дуже важка інтелектуальна праця.)

Частина I.Вступ.Деякі програмісти Delphi (у мене теж таке було) іноді шкодують, що вони вирішили освоювати саме це середовище розробки, а не більш актуальні такі як C++ або ігрові движки на зразок Unity. Від цього у них виникає питання "Чи варто взагалі починати розробляти гру на Object Pascal? Може почати вивчати щось інше?" Відповім одразу - безперечно варто. Почати освоювати щось нове ви завжди встигнете, як то кажуть - немає межі досконалості. А ваші знання Delphi не повинні зникнути задарма. Так, цілком можливо C++ гнучкіший і документації зі створення ігор на ньому набагато більше, але я вас запевняю, за допомогою Delphi теж можна отримати відмінний результат. Один із яскравих прикладів – це гра Soldat 2D. Адже головне в розробці гри - це все-таки результат, а не спосіб його досягнення (але він також дуже важливий).

А тепер почнемо. двоє. Тепер ми розберемося, що найголовніше у грі? Це питання немає загальної відповіді, т.к. кожному у кожній грі подобається щось своє. Комусь подобається сюжет, комусь графіка, комусь музика. Але при створенні гри (а точніше ігрового движка) все-таки одним з головних моментів – це розробка графічної складової. Адже гра – це насамперед комп'ютерна програма, яка відображає всю інформацію на моніторі, а як відомо, людина сприймає понад 90% інформації за допомогою свого зору. Тому в цій статті япостараюся найповніше розповісти про впровадження графіки у свій проект, а решту (звук, наприклад) я залишу на вас. Так що може послужити графічним ядром (крім DirectX) нашого проекту? На це питання також дуже багато відповідей в інтернеті, але я загострю увагу в цій статті на бібліотеці (а точніше наборі модулів) FastLIB.

Але не варто боятися цього мінусу, адже навіть на слабеньких старих процесорах (у мене старенький одноядерний Pentium 4 - 3000 Гц) при кількох важких спецефектах частота тримає норму в 60 fps. А велика (набагато більша) частина користувачів вже давно має кілька-ядерні машини.

Впровадження FastLIB у проект.Так як використовувати бібліотеку FastLIB? Насправді найскладніше, з чим я стикався, це включення альфа-каналу. Для початку скопіюйте всі модулі бібліотеки в папку з проектом і увімкніть їх у проект через Delphi будь-яким звичним способом (це завдання я знову залишу на вас). Після цього можете відкрити та вивчити найголовніший модуль – FastDIB. У ньому описаний найголовніший клас – TFastDIB. По суті це той же TBitmap, але спрямований на отримання більшої швидкості при відмальовуванні. І всі функції бібліотеки використовуються саме до цього класу TFastDIB. Далі, що б ми не використовували - чи то текстура, чи спрайт, чи поверхня, вона обов'язково буде ґрунтуватися на класі TFastDIB.

Частина II. Основні функції класу TFastDIB.Оскільки клас TFastDIB є об'єктом, його необхідно створювати на початку і знищувати наприкінці роботи. Здійснюється це за допомогою функційCreateтаDestroy. Створювати об'єкт необхідно відразу виникає необхідність роботи з ним, але тільки один раз, інакше створиться новий об'єкт, а минулий загубиться в пам'яті комп'ютера.Знищувати так само слід тільки після того, як об'єкт не використовується і ніколи вже не буде використаний.

Після того, як об'єкт було створено, його можна малювати на будь-яку поверхню. Для малювання в класі TFastDIB досить багато функцій (різні способи малювання), але найпростішою є функціяDraw.

Приклад створення, відтворення та знищення змінної типу TFastDIB:

В даному прикладі ми малюємо sprite на дескриптор поверхні вікна в координатах 0x0, але в коді немає прикладу, який заповнював би наш спрайт даними. Тобто. ми дійсно нічого не отримаємо на нашому вікні. Для того щоб щось намалювати, потрібно щоб було що малювати. На цей випадок є два варіанти - або ми спочатку малюємо на самому sprite, або ми завантажуємо картинку з файлу за допомогою функціїLoadFromFile.

Приклад створення, завантаження, відображення та знищення змінної типу TFastDIB:

Примітка: за допомогою цієї функції можна завантажувати графічні дані тільки формату *.bmp.

Також передбачена можливість збереження результату за допомогою функціїSaveToFile.

Розглянемо всі можливі способи відтворення та їх функції:Draw(fDC:HDC;x,y:Integer) - копіює вміст TFastDIB на дескрипторfDCу координатах[x ;y];Stretch(fDC:HDC;x,y,w,h:Integer) - розтягує вміст TFastDIB на дескрипторfDCу координатах[x,y]завширшкиwі висотуh;TransDraw(fDC:HDC;x,y:Integer;c:TFColor) - копіює вміст TFastDIB на дескрипторfDCу координатах[x;y]крім пікселів із кольоромc;TransStretch(fDC:HDC;x,y,w,h:Integer;c:TFColor) - розтягує вміст TFastDIB на дескрипторfDCу координатах[x, y]вширинуwта висотуhкрім пікселів з кольоромc;AlphaDraw(hDC,x,y,a,hasAlpha) - копіює вміст TFastDIB на дескрипторfDCу координатах[x;y]з непрозорістюaз підтримкою альфа-каналу (hasAlpha = true) та без підтримки альфа-каналу (hasAlpha = false);AlphaStretch(fDC:HDC;x,y,w,h:Integer;a:Byte;hasAlpha:Boolean) - розтягує вміст TFastDIB на дескрипторfDCу координатах>[x,y]завширшкиwі висотуhз непрозорістюaз підтримкою альфа-каналу (hasAlpha = true) і без підтримки альфа -каналу (hasAlpha = false);TileDraw(fDC:HDC;x,y,w,h:Integer) - заповнює область поверхні з дескрипторомfDCу координатах[x;y]розміромwнаh;

Використання альфа-каналу.Одним із важливих моментів кожної гри (якщо розглядати тільки графічну складову) є наявність гарних спец-ефектів, будь то епічні вибухи або скляний інтерфейс. Для цього використовують напівпрозорі спрайти, а саме спрайти 32-бітного режиму. Це означає, що на один піксель спрайту виділяється 32 біти пам'яті, а саме 4 байти - r, g, b та альфа-канал. Кожен із байт може містити значення від 0 до 255. Таким чином альфа-канал - це ступінь непрозорості кожного пікселя від 0 до 255. Нижче буде наведено приклад створення спрайту з використанням альфа-каналу та випадковим заповненням кожного пікселя:

Цей код генерує картинку розміром 64x64 пікселя з режимом 32 біта (з підтримкою альфа-каналу) з випадковими кольорами та випадковою маскою прозорості, малює результат на формі.

Примітка: якщо ви хочете використовувати спрайт з альфа-каналом, то малювати його ви зможете лише функціями AlphaDraw та AlphaStretch, т.к. тільки вонипідтримують можливість малювання спрайту з альфа-каналом з прозорістю.

Давайте розглянемо наведений вище код докладніше: зміннаcтипуPFColorA- це покажчик на змінну типуTFColorA, який є записом і складається з 4 байт:

Система ігрового движка.Тепер ми розглянемо як же все-таки робляться всі ігри, і як нам почати робити свій власний робочий движок.

Раніше, коли я був зовсім маленьким, я грав зі своїм братом у настільні паперові ігри з кубиком. Це було дуже захоплююче та забавно, але коли ми грали, то ми самі за всім стежили та регулювали всі правила гри, самі рухали всі фігурки, самі рахували бали. Але потім у старшого брата з'явилася денді. Приставка сама все робила за нас і набагато швидше. Грати в ігри стало цікавіше та цікавіше, т.к. ми не думали ні про що зайве, а були максимально сконцентровані на самому ігровому процесі, а якщо бути простіше - тупо вирячилися в ящик і ламали джостики. Потім у брата з'явилася сега мега драйв 2, потім сього дрімкаст, а потім у кожного будинку вже був ПК. Ігри ставали дедалі красивішими і захоплюючими. Але мене завжди мучило одне й те саме питання - "ЯК?". Як працюють усі ігри? Сама гра здавалася чимось чарівним. Мій склад розуму не дозволяв зрозуміти всю сутність процесів, що відбуваються, поки я сам не став займатися програмуванням. А коли я все-таки дізнався "ЯК", то реальність виявилася набагато простішою за безглузді припущення.

Для "зациклювання" нашого ігрового движка також існує багато способів, але я вам розповім про найпростіше і доступніше - використання стандартного компонента TApplicationEvents (вкладка Additional на панелі компонентів). Додайте цей компонент до форми та створіть обробник подіїOnIdle. Саме вна цьому місці і відбуватиметься зчитування коду до кінця роботи програми.

Примітка: done - змінна, що відповідає за припинення виконання процедури в наступному циклі при неактивності користувача (наприклад, якщо користувач не чіпає пристрою введення - мишу та клавіатуру).

Саме тут перевірятиметься вся система нашого ігрового двигуна. Але щоб щось перевіряти – треба все це створити.

Графічне пристрій.Ось і настав момент ближче познайомитися з графічною складовою нашого проекту. Зараз ми розглянемо один із прикладів системи графіки.

Всю роботу графічної системи можна поділити на три частини: створення графічних пристроїв, їх перевірка та малювання, знищення графічних пристроїв. Тобто. все це можна описати в 3-х функціях:CreateGraphicsDevices; CheckGraphicsDevices; DestroyGraphicsDevices;Розглянемо кожну функцію докладніше:

CreateGraphicsDevices; Тут відбувається встановлення потрібного режиму екрану, створення головної поверхні, завантаження текстур, створення інших необхідних графічних пристроїв. Чим є "головна поверхня"? По суті це той же TFastDIB, але головна вона через те, що всі об'єкти, весь інтерфейс, всі спец-ефекти малюються саме на ній, а вона вже в кінці перевірки графіки малюється на поверхні форми і відображає весь результат на екрані.

Тепер поговоримо трохи про способи завантаження текстури. Найпростіший із них це завантаження всіх текстур гри на початку роботи програми (у створенні графічних пристроїв) та їх звільнення наприкінці роботи. Але цей спосіб поганий тим, що цілком можливо багато текстур будуть не використані протягом усієї роботи двигуна, а це зайва витрата оперативної пам'яті.

Інший спосіб це завантаження понеобхідності. Тобто. якщо виникла потреба використання потрібної текстури, лише після цього вона завантажується. Цей метод економить пам'ять, але додає навантаження процесору, т.к. при необхідності використання потрібно перевіряти чи завантажена текстура, а якщо ні - то завантажувати. При цьому способі також потрібен правильний вибір - або відразу після використання і подальшої непотрібності очищається, або залишається в пам'яті до наступного використання.

А куди завантажувати всі текстури? Найпростіший спосіб завантаження це завантаженнядинамічний масив. Масив - це свого роду таблиця в оперативній пам'яті, кожна комірка якої має свій індекс. Існують одновимірні та багатовимірні масиви, тобто. таблиці різних розмірів.

200?'200px':''+(this.scrollHeight+5)+'px');"> . var // оголошуємо одновимірний цілий масив m1: array of integer; // оголошуємо двовимірний речовий масив m2: array of array of real; . begin . // змінюємо розмір одномірного масиву (10 елементів) SetLength(m1, 10);

// Змінюємо розмір двомірного масиву (10x10=100 елементів) SetLength(m2,10,10);

// задаємо нульовий (найперший) і 9-й (найостанній) елемент одномірного масиву m1[0]: = 1992; m1[9]: = 20;

// додаємо до першого найостанніший m1[Low(m1)] := m1[Low(m1)] + m1[High(m1)];

// задаємо елемент нульового стовпця нульового рядка двовимірного масиву m2[0][0]: = 2013; . end;

Тепер розберемо основні функції до роботи з динамічними масивами.SetLength(array,size) - встановлює новий розмірsizeмасивуarray;Low(array) - повертає індекс найпершого (нижнього) елемента масивуarray;High(array) - повертає індексостаннього (верхнього) елемента масивуarray;

(Тут піде продовження статті, але трохи пізніше)