
Это гифка, которую я сделал, чтобы показать вступление и как началась история путешествия птички. У меня есть друг, который не боится рисовать, даже если он не обучался рисованию профессионально. Я общаясь с ним как то вдохновился желанием рисовать и не бояться. В 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.
Игра, которую я написал, можно найти по кодовому названию в google play.
com.xverizex.fly_bird_2
Правда я всё ещё жду пока одобрят первую версию и пока она не доступна в маркете. Я хочу сделать её бесплатной в google play, а в huawei маркете, если это вообще возможно, то выставить цену на игру. Хотелось бы ещё зарабатывать на том что нравиться.
Игра по своей сути получилась относительно простой и поэтому её возможно было сделать за 5 дней. Да, на unity можно было бы за дня два или один сделать, но мне нравиться C и C++, разумеется я буду писать на том что мне нравиться.
Это были мои все заметки, которые я запомнил за прошедшие пять дней разработки. Я писал по 12 или более часов почти каждый день и не мог уснуть, потому что было интересно. Но теперь нужно отдохнуть перед следующим заходом. Возможно новый уровень в этой игре или новая игра.
ссылка на оригинал статьи https://habr.com/ru/post/662559/
Добавить комментарий