Як я робив гру «LaserTank» під KolibriOS

Всім привіт. У цій публікації хочу розповісти про те, як робив гру під операційну систему KolibriOS, про існування якої раніше навіть і не підозрював.

Як все почалося?
А почалося все з того, на igdc.uk з'явилося оголошення від адміністрації KolibriOS про початок конкурсу на створення гри під їх операційну систему.

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

Звичайно, я сподівався, що діставшись додому заб'ю на цю ідею (як це зазвичай і буває), але, на моє здивування, цього не сталося. Навіть навпаки. Коли я прийшов додому і встановив KolibriOS на віртуальній машині, загорівся цією ідеєю настільки, що можна вже гасити. Інструменти трохи охолодили запал.

Інструменти
Підготовка до написання гри почалася з вибору мови і середовища розробки. З початку погляд упав на З, але відсутність ООП робить мене сумним пандою. Варіант використовувати Асемблер… Не розглядався. Досить швидко було знайдено варіант написання «Hello World» на З++, і це мене цілком влаштовувало. Правда, трохи заважала нестерпна ненависть до С++, але боротьба з нею таки допомогла зрозуміти таємницю зірочок в мові.

Написавши пару прикладів за різними статтями, я так і не зміг скомпілювати програму. Довелося звертатися за допомогою до експертів на форумі. Добрі люди не змусили довго чекати і швидко наставили мене на шлях істинний.

Зупинився на утиліті автоматичного створення проекту під Visual Studio під компілятором FASM — це один з найпростіших способів, при якому компіляція з середовища відразу ж створювала запускається файл програми для Kolibri. Вказавши шлях компіляції на USB флешку, стало зручно запускати програму на виртуалке. Способів простіше не знайшлося, загальні папки не підтримуються.

На збірку порожнього проекту було витрачено близько тижня, тому, незважаючи на те, що на конкурс давалося майже 2 місяці, на створення гри залишилося всього 20 днів. Але всім відомо — чим ближче дедлайн, тим вище продуктивність.

12-20 Грудня
Настав сокровенний момент вибору, яку ж гру робити в доступних технологічних умовах. Спершу я хотів зробити скроллер, але перші ж експерименти з висновком картинок вбили цю ідею. Відсутність апаратного прискорення і проблеми з точними таймерами не дозволяли малою кров'ю зробити динамічну гру. Висновок картинки здійснюється принципом передачі одновимірного масиву RGB кольорів пікселів, вказівки розмірів зображення і позиції.

struct RGB
{
Byte b;
Byte g;
Byte r;
}
...
void kos_PutImage( RGB * imagePtr, Word sizeX, Word sizeY, Word x, Word y);

Графіка в грі буде спрайтовая, а значить, треба завантажити зображення з файлу, але який формат використовувати? png, jpg або gif? Як завжди вибір припав на png, він безкоштовний, досить економічний, не псує картинку стисненням і має можливість зберігати альфа-канал. Попрацювавши з читанням файлів в KolibriOS, я наступив на десятки граблів і вирішив використовувати простий масив пікселів, занесених в глобальну константу.

const RGB img_water[576] = {
0x0B79BD, 0x0A6DAE, 0x0A69AA, 0x0A6EAD, 0x0B71B0, 0x0A65A8, 0x0A65A6, 0x0B75B4, 0x0B6DAD, 0x0B71B1, 0x0A6AAD, 0x0A6DAD, 0x0C7EBB, 0x0A66AB, 0x0B66AD, 0x0B6DB1, 0x0A61A3, 0x0B79B7, 0x0A72B1, 0x0A6AAD, 0x0C85C1, 0x0A6AAB, 0x0A62A3, 0x0B6EB2, 0x0B7ABC, 0x0B6BAC, 0x0C8BC3, 0x0C9BCE, 0x0C88C1, 0x0B75B4, 0x0B81BD, 0x0B89C1, 0x0B71B1, 0x0B7AB8, 0x0A74B1, 0x0B76B5, 0x0B86BF, 0x0B81BC, 0x0B81BC, 0x0A5B9F, 0x0B70AF, 0x0C86BE, 0x0B76B5, 0x0C94C6, 0x0D9DCB, 0x0A6EAF, 0x0B70B0, 0x0B70B4, 0x0B72B2, 0x0A6EAE, 0x0C8BC4, 0x0DA1D3, 0x0C8CC9, 0x0B7CB9, 0x0B7DBA, 0x0B70AF, 0x0B7CB9, 0x0B89C2, 0x0B80BB, 0x0B7AB9, 0x0B80BD, 0x0C9FCC, 0x0B8DC1, 0x0B73B3, 0x0B79B6, 0x0A61A4, 0x0B81BB, 0x0DAAD3, 0x0EB7D8, 0x0C86C4, 0x0B80BC, 0x0B79BA, 0x0A6EAD, 0x0B81BC, 0x0B8DC5, 0x0C94CA, 0x0C8AC8, 0x0C8DC4, 0x0E90C6, 0x0A64A5, 0x0B71B0, 0x0B81BD, 0x0B87C0, 0x0C8AC6, 0x0D90CB, 0x0D9FCC, 0x0B84BD, 0x0B77B7, 0x0B7DBA, 0x0B80BB, 0x0C97C8, 0x0DA6D3, 0x0EC8E0, 0x0D94CB, 0x0C8AC4, 0x0B79B9, 0x0A64A6, 0x0B7EBA, 0x0B86BF, 0x0B7EBA,
...
};

І ось, нарешті, вийшло вивести зображення у вікні програми.

Але виявилося, що в структурі пікселя відсутня прозорість. Це вставило ноги мені в рот, а в колеса палиці. Виникла ідея створення свого буфера кадру. Виявилося, що його використання досить вимогливо ресурсів системи і при виведенні буфера виникає мерехтіння. Для боротьби з цими мінусами було вирішено використовувати RenderTarget, в яких фон попіксельно змішувався зі спрайтом об'єкта, що має альфу.

alpha = (float)addPixel.a / 255.0 f;
newPixel.r = pixel.r * (1 - alpha) + addPixel.r * alpha;
newPixel.g = pixel.g * (1 - alpha) + addPixel.g * alpha;
newPixel.b = pixel.b * (1 - alpha) + addPixel.b * alpha;

Але й тут було кілька проблем. Довелося створити свою структуру пікселя ARGB (з прозорістю) і ряд функцій по роботі з масивами зображень. Висновок спрайтів з альфою готовий; але тепер я з жахом усвідомив, що поняття не маю, що за гру буду робити.

21-22 Грудня
Сьогодні мій погляд впав на пункт правил конкурсу про автозбиранні проекту. Прочитавши пару статей на цю тему, я заплутався і злякався ще більше, тому знову звернувся до форуму за допомогою. Але відповіді чекати не став і одразу приступив до розробки самої гри. Основа геймплея була взята зі старого прототипу на конкурс IGDC.ru №110, в якому я брав участь пару років тому. Як згадую той проект, так виривається мимовільна усмішка. У цій конкурсній роботі був просто невеликий стьоб над адміністрацією і конкурентами конкурсу: я написав гру в Delphi кирилицею з усіма принципами хорошого тону при формуванні коду. Але це зовсім інша історія.

Виглядало це якось так:
type
Логіка = Boolean;
Число = Integer;
Дріб = Single;
ТикТаймера = Double;
Точка = TPoint;
ТСписок = TList;

const
ПРАВДА: Логіка = True;
БРЕХНЯ: Логіка = False;
...
type
ТЛазер = record
X, Y, Кут, Тайл: Число;
end;

ТИгрок = class(ТОбычныйОбъект)
strict private
FЛазер: array of ТЛазер;
FКнопки: array[0..255] of Логіка;
procedure ДелаемЛазер(Час: ТикТаймера);
public
procedure Процес(Час: ТикТаймера); override;
procedure Малюй(Шар: Число); override;
procedure ЗажатаКнопка(Кнопка: Число);
procedure ОтжатаКнопка(Кнопка: Число);
function ПроверкаСмещения(X, Y: Кількість): Логіка; override;
end;
...
while (Карта.Тип[Тчк.X, Точка.Y] in [тмТрава, тмВода, тмЯщикВВоде]) do
begin
...
if (Карта.Об'єкт[Тчк.X, Точка.Y] is ТЯщик) or (Карта.Об'єкт[Тчк.X, Точка.Y] is ТПушка) then
begin
Карта.Об'єкт[Тчк.X, Точка.Y].СместитьПоВектору(Зміщення.X, Зсув.Y);
Кінець := ПРАВДА;
end


Але повернемося до наших баранів.

Для роботи з графікою була написана невелика програма на Delphi, що дозволяє перетворювати зображення png в C++ масив пікселів. Це дало хороший поштовх до подальшого розвитку. Як підсумок перший прототип рівня був зроблений досить швидко:



Взагалі, у зв'язку з невеликою кількістю вбудованих засобів, довелося робити функції для самих базових операцій з графікою і логікою гри.

У грі відразу була запланована плавна анімація переміщення по рівню — ніяких стрибків по клітинках і смикань.

Чесно кажучи, тут я вирішив трохи зшахраювати, вносячи зміни тільки при діях гравця. Це значно розвантажило систему і креслення, і в моменти простою. Для повороту танка було вирішено не робити заздалегідь заготовлені зображення, а написати алгоритм для повороту зображення. Звичайне відображення по горизонталі і вертикалі зі зміною координат дало простий поворот картинки на кут, кратний 90 градусів. Перший час цього вистачало з лишком. Але ніяк не стикувалося з плавністю руху самого танка. Трохи пізніше буде зроблено і це, але спочатку зробимо штовхання ящиків і скидання їх у воду для створення мосту. Це дозволить займатися не спецефектами, а безпосередньо механікою гри.



23-24 Грудня
Подивившись на те, як виглядає гра, мимоволі прийшла ідея замінити графіком неясного походження на свою, щоб уникнути проблем з авторством. Довелося шукати художника (але, треба сказати, недовго) і замовляти у нього графіком. Погляд упав на знайомого художника Weilard. Обговоривши ідею і показавши прототип, ми домовилися про ціну і почали роботу. (ви ж не думаєте, що професіонали працюють безкоштовно?)

Наступного дня він вже надав мені пару ескізів свого бачення гри:



Четвертий варіант здався мені найбільш стилістично вірним, так і за кольорами сподобався більше, тому, вибравши четвертий варіант, я попросив змінити колір цегли і отримав кінцевий варіант:



Вирізавши шматочки з ескізу, відразу встромив їх у гру замість моїх картинок. Решту графіком майстер обіцяв надати вихідні (27-28 грудня). З цієї графікою відкрилося друге дихання і вся втома пішла геть. Я просто жив цією грою.

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

Наступним кроком був реалізований лазер. Алгоритм простий — рекурсивно йдемо по клітинках рівня по напрямку вектора руху, поки не зіткнемося з перешкодою. Якщо перешкода — дзеркало, перевіряємо його орієнтацію і закінчуємо цикл (дзеркало повернено до нас стінкою), або змінюємо вектор напрямку і йдемо далі (потрапили в саме дзеркало).



Спробувавши знову повернутися до автозбиранні і вивчивши це питання, вирішив, що краще відкласти до вихідних, адже дуже хотілося закінчити геймплей. Він зараз був куди важливіше, інакше здавати буде зовсім нічого.

25-26 грудня
Після додавання руйнування цегляної стіни лазером проект став компілюватися досить довго. Причина ховалася у великій кількості глобальних констант, якими були забиті в код текстури. Зробивши завантаження ресурсів з файлу, я поборов цю проблему. Возитися з завантаженням png-файлів не було часу і бажання, так як формат з стисненням і вимагає пристойного завантажувача. Тому довелося швиденько переробити свою утиліту по створенню масиву пікселів у файл. Я витратив багато часу поки зрозумів, що потрібно вказувати повний шлях до файлу (відносні шляхи не підтримуються), але абсолютно випадковим чином вдалося це зробити. т. к. файл з текстурою був простим масивом RGB або ARGB структур, я вирішив не морочитися і зробив утиліту по упаковці всіх файлів в один.



Прибравши з коду всі глобальні масиви, проект став компілюватися моментально, що мене невимовно тішило.

27 Грудня
І знову ця автозбирання… Автозбирання вийшла не відразу. Благо добрі люди на форумі допомогли врятувати проект від помилок і зібрати дистрибутив KolibriOS з вшитою грою. Тим же днем художник нарешті-таки скинув фони для меню, вікна вибору рівнів і додаткові текстури об'єктів. Якщо раніше в грі була тільки одна сцена (зрозуміло, мова про режим гри), то для впровадження меню, паузи і вибору рівня в проект довелося додавати ще сцени. Спершу з'явилося меню з кнопками початку гри і виходу. При створенні переходу між сценами довелося спершу реалізувати події перемоги і поразки, але тут постала проблема: рівень був тільки один, і той зберігався в константі глобального масиву.

Як завжди, прийшла на допомогу Delphi, яка допомогла швидко накидати скромний редактор рівнів:



Я знову трохи захопився цією справою і зробив супер-редактор, простий і зручний. Хоча і плюс в цьому є — тепер будь-хто, кому не лінь, може відкрити редактор і додати парочку своїх рівнів в гру:



28-29 Грудня
Наступні кілька днів я присвятив додавання лазерних гармат і їх знищення. Перевірку на потрапляння танка в поле зір пушк поставив на момент закінчення руху танка. Щоб знайти активну гармату на лінії вогню, від танка по горизонталі і вертикалі пускалися перевірки клітин на наявність гармати, поверненою в нашу сторону. Якщо вона перебувала, то запускався той же алгоритм лазера, що і у танка, але з іншим кольором. Тут довелося знову змінити алгоритм відтворення в буфер, щоб можна було робити фарбування текстури у вказаний колір.

Знищення гармати було можливо тільки при попаданні лазером їй в дуло, але просте візуальне зникнення мені не подобалося. Взявши в руки Magic Particles, я зробив кадрову розгортку анімації вибуху і ще раз допрацював малювання спрайту в буфер. Тепер вже з урахуванням номера кадру, яких можна стало зберігати в текстурі кілька.



Також замовив художнику намалювати ще один тайл, пляма від вибуху (декаль).

Прикинувши, що дедлайн зовсім скоро, в голову прийшла ідея зробити два види дзеркал (статичні і пересувні), а також пропускають лазер стінки. Вбитий у мені художник ожив і піднісся — взявши текстури стінки ящика, я порізав їх навпіл і склеїв з дзеркалами. Вийшло нове геймплейне рішення, яке допомагає легко визначити можна пересувати дзеркало чи ні.



30 Грудня
Ну от і прийшов час для створення сцени з вибором рівня. Я не міг не врахувати той момент, що редактором карт будь-який бажаючий може додати або видалити рівень. Тому зробив посторінкову навігацію по всім рівням у файлі, тридцять рівнів на сторінці. На даний момент у грі є ліміт на кількість рівнів. Менше тисячі рівнів, і то, тільки з-за того, що чотиризначне число не влазить в квадратик з кнопкою :D.

Настала ніч на 31 число і майже всі баги вже перетворилися в фічі. Як добре, що я взяв відпустку на 3 останні дні, це дозволило пройтися рубанком і напилком за проектом. До речі, один з кумедних багів: після лазера під дзеркалами змінювалася грунт, на ту, що під танком (друга гифка).

31 Грудня
Вирішив присвятити весь день створення рівнів, адже геймплейна частина вже була цілком готова. Це виявилося найскладнішим у всій розробці. На створення 48 рівнів було витрачено близько 9 годин.

До кінця здачі залишається три години, я роблю комміт і заспокоюю нерви. Все, встиг!

Джерело
Вихідні коди на SVN: LaserTank
Обговорення гри на форумі: board.kolibrios.org/viewtopic.php?f=41&t=2934

Голосування
Подивитися інші конкурсні роботи та проголосувати за вподобані можна тут, на хабре.

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

0 коментарів

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