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