DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі

"Я знаю, що нічого не знаю" Сократ

Для кого: для IT-шників, які плювали на всіх розробників і хочуть пограти у свої ігри!

Про що: про те, як почати писати ігри на C / C ++, якщо раптом вам це потрібно!

Навіщо вам це читати: розробка додатків – це не моя робоча спеціалізація, але я намагаюся щотижня програмувати. Тому що люблю ігри!

Привіт мене звати Андрій Гранкін, я DevOps в компанії Luxoft. Розробка додатків — це не моя спеціалізація, але я намагаюся щотижня програмувати. Тому що люблю ігри!

Індустрія комп'ютерних ігор величезна, з чуток, сьогодні навіть більше, ніж індустрія кіно. Ігри писали з початку розвитку комп'ютерів, використовуючи, за сучасними мірками, складні та базові методи розробки. Згодом стали з'являтися ігрові движки з уже запрограмованою графікою, фізикою, звуком. Вони дозволяють зосередитися на розробці самої гри та не морочитися з приводу її заснування. Але разом із ними, з двигунами, розробники «сліпнуть» і деградують. Саме виробництво ігор ставиться на конвеєр. А кількість продукції починає переважати її якість.

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

… настав час створювати свої світи, підвладні лише мені. Мири, де я є і Отець, і Син, і Святий Дух!

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

У цій статті спробую розповісти, як почав писати невеликі ігри на C/C++, який процес розробки і де я знаходжу час на хобі в умовах великої завантаженості. Вона суб'єктивна і визначає процес індивідуального старту. Матеріал про невігластво і віру, про мою особисту картину світу зараз. Іншими словами, «Адміністрація не несе відповідальності за ваші особисті мізки!».

Практика

«Знання без практики марні, практика без знань небезпечна» Конфуцій

Мій блокнот – моє життя!


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

DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі
Мій (вже списаний) блокнот. Отак він виглядає. У ньому повсякденні завдання, ідеї, малюнки, схеми, рішення, чорна бухгалтерія, код тощо.

На даному етапі я встиг закінчити три проекти (це в моєму розумінні «закінченість», адже будь-який продукт можна розвивати відносно нескінченно).

  • Проект 0: це 3D-сцена Architect Demo, написана на C# з використанням ігрового двигуна Unity. Для платформ macOS та Windows.
  • Гра 1: консольна гра Simple Snake (усім відома як "Змійка") під Windows. Написана C.
  • Гра 2: консольна гра Crazy Tanks (усім відома як "Танчики"), написана вже на C + + (з використанням класів) і теж під Windows.

Project 0. Architect Demo

  • платформа: Windows (Windows 7, 10), Mac OS (OS X El Capitan v. 10.11.6)
  • Мова: C#
  • Ігровий двигун: Єдність
  • Натхнення: Darrin Lile
  • Репозиторій: GitHub

DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі
3D-сцена Architect Demo

Перший проект реалізований не так на C/C++, але в C# з допомогою ігрового движка Unity. Цей двигун був не такий вимогливий до «заліза», як Unreal Engine, а також мені здався легше в установці та використанні. Інші двигуни я не розглядав.

Метою Unity для мене не була розробка якоїсь гри. Я хотів створити 3D-сцену з якимось персонажем. Він, а точніше Вона (я змоделював дівчину, в яку був закоханий =) мала рухатися і взаємодіяти з навколишнім світом. Важливо було тільки зрозуміти, що таке Unity, який процес розробки та скільки зусиль потрібно для створення чогось. Так народився проект Architect Demo (назва придумана майже від балди). Програмування, моделювання, анімація, текстурування зайняло у мене, напевно, два місяці щоденної роботи.

Стартував я з навчальних відео на YouTube зі створення 3D-моделей у змішувач. Blender – це відмінний безкоштовний тул для 3D-моделювання (і не тільки), що не потребує встановлення. І тут на мене чекало потрясіння… Виявляється, моделювання, анімування, текстурування — це величезні окремі теми, на які можна книжки писати. Особливо це стосується персонажів. Щоб моделювати пальці, зуби, очі та інші частини тіла, вам знадобляться знання анатомії. Як улаштовані м'язи обличчя? Як люди рухаються? Мені довелося вставляти кістки в кожну руку, ногу, палець, фаланги пальців!

Моделювати ключиці, додаткові кістки-важелі, щоб анімація виглядала природно. Після таких уроків розумієш, яку велику працю роблять автори анімаційних фільмів, аби створити 30 секунд відео. Адже 3D-фільми тривають годинами! А ми потім виходимо з кінотеатрів і говоримо щось на кшталт: «Та, лайновий мультик/фільм! Могли зробити й краще…» Дурні!

І ще щодо програмування в цьому проекті. Як виявилося, найцікавіша для мене частина була математична. Якщо ви запустите сцену (посилання на репозиторій в описі проекту), то зауважте, що камера обертається навколо персонажа-дівчинки у сфері. Щоб запрограмувати таке обертання камери, мені довелося спочатку вираховувати координати точки положення на колі (2D), а потім на сфері (3D). Найсмішніше, що я ненавидів математику в школі та знав її на три з мінусом. Почасти, напевно, тому що в школі тобі просто не пояснюють, як, чорт забирай, ця математика застосовується в житті. Але коли ти одержимий своєю метою, мрією, то розум очищується, розкривається! І ти починаєш сприймати складні завдання як захоплюючу пригоду. А потім думаєш: "Ну чого ж "улюблена" математичка не могла нормально розповісти, де ці формули притулити можна?".

DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі
Прорахунок формул для обчислення координат точки на колі та сфері (з мого блокнота)

Game 1. Simple Snake

  • платформа: Windows (тестував на Windows 7, 10)
  • Мова: по-моєму, писав на чистому C
  • Ігровий двигун: консоль Windows
  • Натхнення: javidx9
  • Репозиторій: GitHub

DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі
Гра Simple Snake

3D-сцена – це не гра. До того ж моделювати та анімувати 3D-об'єкти (особливо персонажі) довго та складно. Погравшись із Unity, до мене прийшло усвідомлення, що потрібно було продовжувати, а точніше починати, з основ. Чогось простого та швидкого, але в той же час глобального, щоб зрозуміти сам пристрій ігор.

А що у нас просте та швидке? Правильно, консоль та 2D. Точніше навіть консоль та символи. Знову поліз шукати натхнення в інтернет (взагалі вважаю інтернет найбільш революційним і небезпечним винаходом XXI століття). Нарив відео одного програміста, який робив консольний тетріс. І подібно до його гри вирішив запиляти «змійку». З відео я дізнався про дві фундаментальні речі – ігровий цикл (з трьома базовими функціями/частинами) та виведення у буфер.

Ігровий цикл може виглядати приблизно так:

int main()
   {
      Setup();
      // a game loop
      while (!quit)
      {
          Input();
          Logic();
          Draw();
          Sleep(gameSpeed);  // game timing
      }
      return 0;
   }

У коді представлена ​​вся функція main(). Ігровий цикл починається після відповідного коментаря. У циклі три базові функції: Input(), Logic(), Draw(). Спочатку введення даних Input (переважно контроль натискання клавіш), потім обробка введених даних Logic, потім виведення екран — Draw. І так кожний кадр. У такий спосіб створюється анімація. Це як у мальованих мультиках. Зазвичай обробка введених даних відбирає найбільше часу і, наскільки знаю, визначає фреймрейт гри. Але тут функція Logic() виконується дуже швидко. Тому контролювати швидкість зміни кадрів доводиться функцією Sleep() з параметром gameSpeed, який визначає цю швидкість.

DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі
Ігровий цикл. Програмування «змійки» у блокноті

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

Отримання буфера екрану (якщо можна сказати):

// create screen buffer for drawings
   HANDLE hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0,
 							   NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
   DWORD dwBytesWritten = 0;
   SetConsoleActiveScreenBuffer(hConsole);

Безпосередній висновок на екран рядка scoreLine (рядок відображення окулярів):

// draw the score
   WriteConsoleOutputCharacter(hConsole, scoreLine, GAME_WIDTH, {2,3}, &dwBytesWritten);

За ідеєю, нічого складного в цій грі немає, мені вона є гарним прикладом початкового рівня. Код написаний в одному файлі та оформлений у кількох функціях. Немає класів, немає наслідування. Ви і самі все можете подивитися у вихідних гравцях, перейшовши в репозиторій на GitHub.

Game 2. Crazy Tanks

  • платформа: Windows (тестував на Windows 7, 10)
  • Мова: C + +
  • Ігровий двигун: консоль Windows
  • Натхнення: книга Beginning C++ Through Game Programming
  • Репозиторій: GitHub

DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі
Гра Crazy Tanks

Виводити символи в консоль - це, напевно, найпростіше, що ви можете перетворити на гру. Але тоді з'являється одна неприємність: символи мають різну висоту та ширину (висота більша, ніж ширина). Таким чином, все буде виглядати непропорційно, а рух вниз чи вгору здаватиметься набагато швидше, ніж ліворуч чи праворуч. Цей ефект дуже помітний у "Змійці" (Game 1). "Танчики" (Game 2) позбавлені такого недоліку, тому що висновок там організований через зафарбовування екранних пікселів різними кольорами. Можна сказати, що я написав рендерер. Щоправда, це вже трохи складніше, хоч і набагато цікавіше.

За цією грою достатньо описати мою систему виведення пікселів на екран. Вважаю це основною частиною гри. А все інше можна вигадати самостійно.

Отже, те, що ви бачите на екрані - це просто набір різнокольорових прямокутників, що рухаються.

DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі
Набір прямокутників

Кожен прямокутник представлений матрицею заповненої числами. До речі, можу виділити один цікавий нюанс – усі матриці у грі запрограмовані як одновимірний масив. Чи не двомірний, а одномірний! З одновимірними масивами набагато простіше та швидше працювати.

DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі
Приклад матриці ігрового танка

DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі
Подання матриці ігрового танка одновимірним масивом

DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі
Наочніший приклад представлення матриці одновимірним масивом

Але доступ до елементів масиву відбувається у подвійному циклі, начебто це одномірний, а двомірний масив. Так зроблено тому, що ми таки працюємо з матрицями.

DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі
Обхід одновимірного масиву у подвійному циклі. Y – ідентифікатор рядків, X – ідентифікатор стовпців

Зверніть увагу: замість звичних матричних ідентифікаторів i, j використовую ідентифікатори x і y. Так, мені здається, приємніше для очей і зрозуміліше для мозку. До того ж такий запис дозволяє зручно проектувати використовувані матриці на осі координат двовимірного зображення.

Тепер про пікселі, колір та виведення на екран. Для виведення використовується функція StretchDIBits (Header: windows.h; Library: gdi32.lib). У цю функцію, крім усього іншого, передається наступне: пристрій, на яке виводиться зображення (у моєму випадку це консоль Windows), координати старту відображення картинки, її ширина/висота і сама картинка у вигляді бітової картки (bitmap), представленої масивом байтів. Бітова картка у вигляді масиву байт!

Функція StretchDIBits() у роботі:

// screen output for game field
   StretchDIBits(
               deviceContext,
               OFFSET_LEFT, OFFSET_TOP,
               PMATRIX_WIDTH, PMATRIX_HEIGHT,
               0, 0,
               PMATRIX_WIDTH, PMATRIX_HEIGHT,
               m_p_bitmapMemory, &bitmapInfo,
               DIB_RGB_COLORS,
               SRCCOPY
               );

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

Створення бітової картки m_p_bitmapMemory:

// create bitmap
   int bitmapMemorySize = (PMATRIX_WIDTH * PMATRIX_HEIGHT) * BYTES_PER_PIXEL;
   void* m_p_bitmapMemory = VirtualAlloc(0, bitmapMemorySize, MEM_COMMIT, PAGE_READWRITE);

Грубо кажучи, бітова карта складається із набору пікселів. Кожні чотири байти в масиві – це піксель у форматі RGB. Один байт на значення червоного кольору, один байт на значення зеленого кольору (G) та один байт на синій колір (B). Плюс залишається один байт на відступ. Ці три кольори – Red/Green/Blue (RGB) – змішуються один з одним у різних пропорціях – і виходить результуючий колір пікселя.

Тепер, повторюся, кожен прямокутник, або ігровий об'єкт представлений числовою матрицею. Всі ці ігрові об'єкти розміщуються в колекції. І потім розставляються ігровим полем, формуючи одну велику числову матрицю. Кожне число в матриці зіставив із певним кольором. Наприклад, 8 відповідає синій колір, 9 - жовтий, 10 - темно-сірий колір і так далі. Таким чином, можна сказати, у нас вийшла матриця ігрового поля, де кожне число це якийсь колір.

Отже, маємо числову матрицю всього ігрового поля з одного боку і бітову карту для виведення зображення з іншого. Поки що бітова карта «порожня» — в ній ще немає інформації про пікселі потрібного кольору. Це означає, що останнім етапом буде заповнення бітової карти інформацією про кожний пікселі на основі числової матриці ігрового поля. Наочний приклад такого перетворення на зображенні нижче.

DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі
Приклад заповнення бітової карти (Pixel matrix) інформацією на основі числової матриці (Digital matrix) ігрового поля (індекси кольорів не збігаються з індексами у грі)

Також представлю кусок реального коду з гри. Змінним colorIndex на кожній ітерації циклу присвоюється значення (індекс кольору) з числової матриці ігрового поля (mainDigitalMatrix). Потім змінну color записується сам колір з урахуванням індексу. Далі отриманий колір поділяється на співвідношення червоного, зеленого та синього відтінку (RGB). І разом з відступом (pixelPadding) щоразу ця інформація записується в піксель, формуючи кольорове зображення в бітовій карті.

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

Заповнення бітової карти інформацією на основі числової матриці ігрового поля:

// set pixel map variables
   int colorIndex;
   COLORREF color;
   int pitch;
   uint8_t* p_row;
 
   // arrange pixels for game field
   pitch = PMATRIX_WIDTH * BYTES_PER_PIXEL;     // row size in bytes
   p_row = (uint8_t*)m_p_bitmapMemory;       //cast to uint8 for valid pointer arithmetic
   							(to add by 1 byte (8 bits) at a time)   
   for (int y = 0; y < PMATRIX_HEIGHT; ++y)
   {
       uint32_t* p_pixel = (uint32_t*)p_row;
       for (int x = 0; x < PMATRIX_WIDTH; ++x)
       {
           colorIndex = mainDigitalMatrix[y * PMATRIX_WIDTH + x];
           color = Utils::GetColor(colorIndex);
           uint8_t blue = GetBValue(color);
           uint8_t green = GetGValue(color);
           uint8_t red = GetRValue(color);
           uint8_t pixelPadding = 0;
 
           *p_pixel = ((pixelPadding << 24) | (red << 16) | (green << 8) | blue);
           ++p_pixel;
       }
       p_row += pitch;
   }

За описаним вище методом у грі Crazy Tanks формується одна картинка (кадр) і виводиться на екран функції Draw(). Після реєстрації натискання клавіш функції Input() і подальшої їх обробки функції Logic() формується нова картинка (кадр). Щоправда, ігрові об'єкти можуть мати інше становище на ігровому полі і, відповідно, малюються в іншому місці. Так і відбувається анімація (рух).

За ідеєю (якщо нічого не забув), розуміння ігрового циклу з першої гри («Змійка») та системи виведення пікселів на екран з другої гри («Танчики») — це все, що вам потрібно, щоб написати будь-яку свою 2D-гру під Windows. Без звуку! 😉 Інші ж частини - просто політ фантазії.

Звичайно, гра «Танчики» сконструйована набагато складніше, ніж «Змійка». Я вже використовував мову C++, тобто описував різні ігрові об'єкти класами. Створив власну колекцію – код можна подивитися у headers/Box.h. До речі, в колекції, найімовірніше, є витік пам'яті (memory leak). Використовував покажчики. Працював із пам'яттю. Мушу сказати, що мені дуже допомогла книга Beginning C++ Through Game Programming. Це відмінний старт для початківців у C++. Вона невелика, цікава та непогано організована.

На розробку цієї гри пішло десь півроку. Писав, в основному, під час обіду та перекушування на роботі. Сідав на офісній кухні, тупцював харчі і писав код. Або ж за вечерею вдома. От і вийшли у мене такі «кухонні війни». Як завжди, я активно використовував блокнот і всі концептуальні штуки народжувалися саме в ньому.

На завершення практичної частини пульну кілька сканів мого блокнота. Щоб показати, що саме я записував, малював, вважав, проектував.

DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі
Проектування зображення танків. І відзначено того, скільки пікселів кожен танк повинен займати на екрані

DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі
Прорахунок алгоритму та формул з обігу танка навколо своєї осі

DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі
Схема моєї колекції (та сама, в якій є memory leak, швидше за все). Колегкція створена на кшталт Linked List

DevOps'ний C++ та «кухонні війни», або Як я почав писати ігри під час їжі
А це марні спроби прикрутити до гри штучний інтелект.

Теорія

«Навіть шлях у тисячу миль починається з першого кроку» (Давньокитайська мудрість)

Перейдемо від практики до теорії! Як знайти час на своє хобі?

  1. Визначити, чого ви дійсно хочете (на жаль, це найскладніше).
  2. Розставити пріоритети.
  3. Пожертвувати всім «зайвим» для вищих пріоритетів.
  4. Рухатися до цілей щодня.
  5. Не чекати, що з'явиться дві-три години вільного часу на хобі.

З одного боку, вам потрібно визначити, чого ви хочете та розставити пріоритети. А з іншого, можливо, відмовитись від якихось справ/проектів на користь цих пріоритетів. Інакше кажучи, доведеться пожертвувати всім «зайвим». Я десь чув, що в житті має бути максимум три основні заняття. Тоді ви зможете ними займатись найбільш якісно. А додаткові проекти/напрямки почнуть банально перевантажувати. Але це все, мабуть, суб'єктивно та індивідуально.

Існує якесь золоте правило: never have a 0% day! Про нього я дізнався у статті одного indie-розробника. Якщо працюєте над якимось проектом, то робіть по ньому щось щодня. І не має значення, скільки ви зробите. Напишіть одне слово або один рядок коду, подивіться одне навчальне відео або забіть один цвях у дошку - просто зробіть хоч щось. Найскладніше це почати. Як тільки почнете, то, напевно, зробите трохи більше, ніж хотіли. Так ви постійно рухатиметеся до своєї мети і, повірте, дуже швидко. Адже основне гальмо всіх справ — це прокрастинація.

І важливо пам'ятати, що не варто недооцінювати і ігнорувати вільну «тирсу» часу в 5, 10, 15 хвилин, чекати якихось великих «колод» тривалістю в годину або дві. Стоїте у черзі? Продумайте щось за вашим проектом. Піднімаєтесь ескалатором? Запишіть щось у блокнот. Їдете автобусом? Добре, прочитайте якусь статтю. Використовуйте будь-яку можливість. Досить дивитися котиків та собачок на YouTube! Не засмічуйте собі мозок!

І останнє. Якщо, прочитавши цю статтю, вам сподобалася ідея створення ігор без використання ігрових движків, запам'ятайте ім'я Casey Muratori. Цей хлопець має сайт. У секції «watch -> PREVIOUS EPISODES» ви знайдете чудові безкоштовні відеоуроки зі створення професійної гри з цілковитого нуля. За п'ять уроків Intro to C for Windows ви, можливо, дізнаєтесь більше, ніж за п'ять років навчання в університеті (про це хтось написав у коментарях під відео).

Також Кейсі пояснює, що, розробивши свій власний ігровий двигун, ви краще розумітимете будь-які існуючі двигуни. У світі фреймворків, де всі намагаються автоматизувати, ви навчитеся створювати, а не використати. Усвідомлюєте саму природу комп'ютерів. А також станете набагато розумнішим і зрілим програмістом - профі.

Успіхів вам на обраному шляху! І давайте зробимо світ професійнішим.

Автор: Гранкін Андрій, DevOps



Джерело: habr.com