Как стать автором
Обновить

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

Время на прочтение7 мин
Количество просмотров2.1K

Это гифка, которую я сделал, чтобы показать вступление и как началась история путешествия птички. У меня есть друг, который не боится рисовать, даже если он не обучался рисованию профессионально. Я общаясь с ним как то вдохновился желанием рисовать и не бояться. В 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 или более часов почти каждый день и не мог уснуть, потому что было интересно. Но теперь нужно отдохнуть перед следующим заходом. Возможно новый уровень в этой игре или новая игра.

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 3: ↑2 и ↓1+1
Комментарии5

Публикации

Истории

Работа

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань