Как я разрабатывал игру fly bird 2

от автора

Это гифка, которую я сделал, чтобы показать вступление и как началась история путешествия птички. У меня есть друг, который не боится рисовать, даже если он не обучался рисованию профессионально. Я общаясь с ним как то вдохновился желанием рисовать и не бояться. В google play у меня есть старая игра, которую я делал на unity, когда только начинал работать с движком.

https://play.google.com/store/apps/details?id=com.xverizex.fly_bird&hl=ru&gl=US

Два комментария к старой игре дали мне желание сделать новую версию, но уже на C++ + SDL2 + OPENGL ES 3.2 + OPENSLES + glm. То есть я даже рад хотя бы двум комментариям о том что людям нравиться моё творчество, чтобы чувствовать себя прекрасно и продолжать делать игры.

Так как у меня нормального опыта не было делать игры полноценные на sdl2, то я использовал разные виды кода, которые как я думал, что они правильные. Но поработав на работе и изучая код, я увидел что есть помимо того что я знаю (я про очереди сообщений), есть ещё mqueue. И только потом я додумался, что можно с помощью очередей сообщений отправлять из одного потока в другой что-нибудь. Вот пример как выглядела реализация.

/* SDL поток */ static int general_thread (void *p) {   SDL_Event ev;   while (SDL_WaitEvent (&ev)) {     case SDL_MOUSEBUTTON_DOWN:     {       struct event *event = new struct event();       event->type = BUTTON_DOWN;       event->x = ev.x;  // здесь я сократил код, по настоящему здесь надо       event->y = ev.y;  // преобразовать в нужный формат.       mq_send (mq, (const char *) &event, sizeof (void *), 0);       break;     }   } }  int main (int argc, char **argv)  {   ...     game (); }  /* И где то в другом файле где находится функция game */ void game () {   ...     mq_receive (mq, &event, nullptr); }

Перед тем как использовать эту очередь, я удостоверился в том, что в android ndk есть заголовочный файл mqueue.

Я также посмотрел, есть ли OpenAL для android и оказалось, что она не входит в комплект и как почитал в интернете, что лучше писать для android на OpenSLES.

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

static void gen_vao_vbo (Link *link) {         static float v[18] = {                 -0.5f, -0.5f, 0.0f,                 -0.5f, 0.5f, 0.0f,                 0.5f, -0.5f, 0.0f,                 0.5f, -0.5f, 0.0f,                 0.5f, 0.5f, 0.0f,                 -0.5f, 0.5f, 0.0f         };  #if 0         static float t[12] = {                 0.0f, 1.0f,                 0.0f, 0.0f,                 1.0f, 1.0f,                 1.0f, 1.0f,                 1.0f, 0.0f,                 0.0f, 0.0f         }; #else         static float t[12] = {                 0.0f, 0.0f,                 0.0f, 1.0f,                 1.0f, 0.0f,                 1.0f, 0.0f,                 1.0f, 1.0f,                 0.0f, 1.0f         }; #endif 

Я сначала думал что в unity делают правильно, что отсчитывают от центра и исходил из этого и определял экран как.

ortho = glm::ortho (-1.0f * aspect, 1 * aspect, 1.0f, -1.0f, 0.1f, 10.0f);

вроде такой был код.

Позже я понял неудобство и решил отсчитывать от левого верхнего угла и сделал так.

ortho = glm::ortho (0.0f, 2.0f * aspect * aspect, 2.0f, 0.0f, 0.1f, 10.0f);

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

Sprite::Sprite (Common &com) {         aspect = (float) com.screen_width / (float) com.screen_height;         screen_width = com.screen_width;         screen_height = com.screen_height;         ortho = glm::ortho (0.0f, 2.0f * aspect * aspect, 2.0f, 0.0f, 0.1f, 10.0f);         //ortho = glm::ortho (0.0f, (float) com.screen_width, (float) com.screen_height, 0.0f, 0.1f, 10.0f);         pos = glm::translate (glm::mat4 (1.0f), glm::vec3 (0.0f, 0.0f, 0.0f));          program = get_shader (SHADER_MAIN);         glUseProgram (program);          uniform_cam = glGetUniformLocation (program, "cam");         uniform_pos = glGetUniformLocation (program, "pos");         uniform_ortho = glGetUniformLocation (program, "ortho");         uniform_tex = glGetUniformLocation (program, "s_texture");          cur_tex = 0;         play = -1; }

Вообще когда говорят что глобальные переменные это зло, то я думаю что они просто это от кого то услышали и приняли для себя такое же мнение, но мне например не удобно как оказалось передавать объект Common в конструктор. Лучше бы я просто пробросил с помощью extern размеры экрана и всё было бы чище. Да и ещё я по рассуждал, что можно для каждого шейдера отдельный класс создать, чтобы каждый спрайт заново не получал с помощью glGetUniformLocation позиции в шейдере. То есть после компиляции шейдера можно было бы получить все позиции и для спрайта указать например интерфейс к шейдеру или что нибудь подобное, чтобы просто уже было работать. Да и класс шейдера можно было бы интегрировать со спрайтом так, чтобы в рендере спрайта не менять ничего, если ты сменил шейдер. Хотя может я ошибаюсь, но я проработаю этот вопрос.

Еще я столкнулся с проблемами неправильного размера картинки. Спрайты были, одни короче, другие длиннее. Но я путем проб и ошибок выработал правило.

as = (float) com.screen_width / (float) com.screen_height;  /* если ширина больше */ float aspect = w / h;  w = 0.42f; h = w / aspect / as;  /* если высота больше */ aspect = h / w;  h = 0.8f; w = h * aspect;

Вроде бы получилось правильно.

Для загрузки объектов я создал заголовок такого типа.

#pragma once  #define TO_STRING_FILENAME(name) name##_STRING   enum TO_DOWNLOADS {         LINK_BIRD,         LINK_INTRO,         LINK_BLOCK,         LINK_FLY_BIRD,         LINK_END_GAME,         LINK_LOGO,         LINKS_N };  #ifdef __ANDROID__ #define LINK_BIRD_STRING                  "bird.res" #define LINK_INTRO_STRING                 "intro.res" #define LINK_BLOCK_STRING                 "block.res" #define LINK_FLY_BIRD_STRING              "fly_bird.res" #define LINK_END_GAME_STRING              "end_game.res" #define LINK_LOGO_STRING                  "logo.res" #else #define LINK_BIRD_STRING                  "assets/bird.res" #define LINK_INTRO_STRING                 "assets/intro.res" #define LINK_BLOCK_STRING                 "assets/block.res" #define LINK_FLY_BIRD_STRING              "assets/fly_bird.res" #define LINK_END_GAME_STRING              "assets/end_game.res" #define LINK_LOGO_STRING                  "assets/logo.res" #endif 

И если нужно загрузить какой то объект, то мы просто получаем на него ссылку, если он уже был загружен.

Link *downloader_load (const enum TO_DOWNLOADS file) {         switch (file) {                 case LINK_BIRD:                         if (link[LINK_BIRD] == nullptr)                                 link[LINK_BIRD] = load_link (TO_STRING_FILENAME (LINK_BIRD));                         break;                 case LINK_INTRO:                         if (link[LINK_INTRO] == nullptr)                                 link[LINK_INTRO] = load_link (TO_STRING_FILENAME (LINK_INTRO));                         break;                 case LINK_BLOCK:                         if (link[LINK_BLOCK] == nullptr)                                 link[LINK_BLOCK] = load_link (TO_STRING_FILENAME (LINK_BLOCK));                         break;                 case LINK_FLY_BIRD:                         if (link[LINK_FLY_BIRD] == nullptr)                                 link[LINK_FLY_BIRD] = load_link (TO_STRING_FILENAME (LINK_FLY_BIRD));                         break;                 case LINK_END_GAME:                         if (link[LINK_END_GAME] == nullptr)                                 link[LINK_END_GAME] = load_link (TO_STRING_FILENAME (LINK_END_GAME));                         break;                 case LINK_LOGO:                         if (link[LINK_LOGO] == nullptr)                                 link[LINK_LOGO] = load_link (TO_STRING_FILENAME (LINK_LOGO));                         break;         }          return link[file]; }

Да, можно было с помощью текста указывать какой объект загружать, но мне так больше нравиться, и нравиться еще из-за того, что легко получить эту ссылку на объект, если он уже был загружен. В link содержится все vao, vbo[2] и номера всех текстур.

Главное меню игры я сделал из одного спрайта, но на экране изображено две птицы. В момент рендера я отражаю спрайт по горизонтали и рисую в разных частях экрана. Вот как я составил код.

void Sprite::mirror_right () {         glBindVertexArray (link->vao);         glBindBuffer (GL_ARRAY_BUFFER, link->vbo[0]);         float *b = (float *) glMapBufferRange (GL_ARRAY_BUFFER, 0, sizeof (float) * 18, GL_MAP_WRITE_BIT);          float ww = w;         float hh = h;          b[0] = 0;         b[1] = 0;         b[2] = 0;         b[3] = 0;         b[4] = hh;         b[5] = 0;         b[6] = ww;         b[7] = 0;         b[8] = 0;         b[9] = ww;         b[10] = 0;         b[11] = 0;         b[12] = ww;         b[13] = hh;         b[14] = 0;         b[15] = 0;         b[16] = hh;         b[17] = 0;          glUnmapBuffer (GL_ARRAY_BUFFER); }  void Sprite::mirror_left () {         glBindVertexArray (link->vao);         glBindBuffer (GL_ARRAY_BUFFER, link->vbo[0]);         float *b = (float *) glMapBufferRange (GL_ARRAY_BUFFER, 0, sizeof (float) * 18, GL_MAP_WRITE_BIT);          float ww = w;         float hh = h;          b[0] = ww;         b[1] = 0;         b[2] = 0;         b[3] = ww;         b[4] = hh;         b[5] = 0;         b[6] = 0;         b[7] = 0;         b[8] = 0;         b[9] = 0;         b[10] = 0;         b[11] = 0;         b[12] = 0;         b[13] = hh;         b[14] = 0;         b[15] = ww;         b[16] = hh;         b[17] = 0;          glUnmapBuffer (GL_ARRAY_BUFFER); } 

Оказалось не так уж и сложно отражать объект. Также можно отразить по вертикали, например поменяв местами координаты текстуры.

По OpenAL писать нечего, я сделал музыку специально для 44100 частоты и 16 битного формата вроде. По OpenSLES я скачал спецификацию и почитал немного, понял что надо посмотреть примеры реализации и банально переписал код, чтобы заработало на android.

При портировании на android как оказалось, что там нет mqueue реализации. Я нашел только syscall от ядра linux. Но если был syscall для открытия mq_open, то syscall для отправки не было и я подумал что надо искать другое решение. Так как я больше на C писал и на C++ опыта мало, то я конечно же не знал, что в C++ есть контейнер queue. И это было спасением, я сделал её глобальной рядом с функцией main и sdl потоке отправлял в нее event. А в game () файле я пробросил queue с помощью extern и получал события. И вуаля, всё работает.

Так как архитектуры различны, то я просто в ресурс добавил число 1. Если при прочитывании этой переменной, она не равно единице, то делаем смену из littleEngian в bigEngian.

static int swap_little_big_engian (int num) {         return (((num >> 24) & 0xff) | ((num << 8) & 0xff0000) | ((num >> 8) & 0xff00) | ((num << 24) & 0xff000000)); }  static uint8_t **diff_file_to_textures (Link *link, const char *filename) {         int lb = 0;          SDL_RWops *io = SDL_RWFromFile (filename, "rb");         SDL_RWseek (io, 0, RW_SEEK_END);         long pos = SDL_RWtell (io);         SDL_RWseek (io, 0, RW_SEEK_SET);         uint8_t *file = new uint8_t[pos];         SDL_RWread (io, file, pos, 1);         SDL_RWclose (io);          const int LTBE = 0;         const int COUNT = 1;         const int WIDTH = 2;         const int HEIGHT = 3;         int *pack[4];          for (int i = 0; i < 4; i++) {                 pack[i] = (int *) &file[i * 4];         }          if (*pack[LTBE] != 1) {                 for (int i = 1; i < 4; i++) {                         *pack[i] = swap_little_big_engian (*pack[i]);                 }         }          link->size_tex = *pack[COUNT];         link->width = *pack[WIDTH];         link->height = *pack[HEIGHT]; ...

Насчет шрифта freetype2. Я использовал старую сборку freetype, которая у меня на github, потому что новую так и не смог собрать для android.

Также, чтобы скомпилировать с OpenGLESv3, надо обратить внимание, что в ndk библиотеки с такой версией есть не ниже 18 api. Чтобы решить все проблемы с компиляцией, нужно в каталоге app в файле build.gradle сделать типа такого.

android {     compileSdkVersion 31     defaultConfig {         if (buildAsApplication) {             applicationId "com.xverizex.fly_bird_2"         }         minSdkVersion 18         targetSdkVersion 31           ...                       ndkBuild {                 arguments "APP_PLATFORM=android-18"                 abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'             } 

Важно в ndkBuild тоже указать платформу назначение и тогда компиляция сработает.

Ну и указать в app/jni/Application.mk версию api не забыть.

Учитывая прошлый опыт, я не стал на каждую игру заводить отдельный паблик, а сделал один основной и назвал — игры от xverizex.

https://vk.com/xverizex_games

Игра, которую я написал, можно найти по кодовому названию в google play.

com.xverizex.fly_bird_2

Правда я всё ещё жду пока одобрят первую версию и пока она не доступна в маркете. Я хочу сделать её бесплатной в google play, а в huawei маркете, если это вообще возможно, то выставить цену на игру. Хотелось бы ещё зарабатывать на том что нравиться.

Игра по своей сути получилась относительно простой и поэтому её возможно было сделать за 5 дней. Да, на unity можно было бы за дня два или один сделать, но мне нравиться C и C++, разумеется я буду писать на том что мне нравиться.

Это были мои все заметки, которые я запомнил за прошедшие пять дней разработки. Я писал по 12 или более часов почти каждый день и не мог уснуть, потому что было интересно. Но теперь нужно отдохнуть перед следующим заходом. Возможно новый уровень в этой игре или новая игра.


ссылка на оригинал статьи https://habr.com/ru/post/662559/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *