КОТ++
Прикладное программирование
Системное программирование
Программирование микроконтроллеров

Аналогично тому, как написание картины является искусством для души, так и написание программы является искусством для разума. (с) Volnik

Поиск по тегам

Опубликовано
Комментарии 0

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

Почему я не взял готовый движок? Во-первых, NIH, я уже писал про это (там я и сказал пару слов о движке), во-вторых, желание разобраться в работе движков. Движок был назван изначально EEngine – Egor Engine, это название я придумал ещё до момента, когда был выработан концепт движка, после того как уже был готовый движок у меня появилась небольшая капля скромности и название мне перестало нравиться. Но я его не стал менять, просто первую букву E нужно расшифровывать как-то иначе.

Концепт движка заключается в следующем: EEngine отрисовывает графику через GPU – graphics processing unit, на мобилках это видеочип, на компах это видеокарта. GPU по сути работает с 3D графикой, но мой движок 2D, а 2D для GPU это тот же 3D только без манипуляций осью Z, представьте плоский куб, у которого толщина равна 0, на передней и единственной грани рендерится (отрисовывается) текстура – спрайт.

Отрисовка производится шейдерами. Шейдеры – это программы для рендеринга, которые скармливаются GPU, они сообщают видеочипу как отрисовывать каждый пиксель. Так как GPU манипулирует только треугольниками, то в шейдере в общих словах алгоритм таков: рисуем два треугольника, два треугольника это по сути прямоугольник, потом берём текстуру и натягиваем поверх нарисованного треугольника – так мы получим отрисованный 2D спрайт. То есть каждый графический объект является прямоугольником из двух треугольников, такая конструкция называется мешом, меш – набор треугольников определяющих форму графического объекта.

Работа движка осуществлена с помощью паттерна «Игровой цикл», в общем виде игровой цикл должен выглядеть так:

while (1)
{
	processInput(); //обработка пользовательского ввода
	update(); //обработка логики игры 
	render(); //отрисовка
}

В бесконечном цикле вызываются у корневой сцены методы processInput(), render() и update(). Общая суть паттерна игровой цикл в том, что есть бесконечный цикл, а в нём последовательно обрабатываются пользовательский ввод (processInput), отрисовка (render), просчёт логики игры (update) и просчёт физики (физику не разбираю в этой статье). О игровых паттернах очень хорошо рассказано в этой книге, как раз таки в ней я о них и узнал.

Все графические объекты являются нодой (узлом) связного списка, в итоге возможность ноды иметь дочерние объекты порождает дерево графических объектов, отрисовка выполняется обходом по дереву, у каждой ноды берётся текстура и отрисовывается по необходимым координатам шейдером. Аналогичным образом идёт вызов метода update() у объектов. И аналогичным образом реализованы сигналы и слоты у элементов UI, реализовал работу сигналов-слотов будучи вдохновлённым Qt. UI элементы это те же графические элементы, но расширены функционалом сигналов-слотов для работы с пользовательским вводом. У UI элемента достаточно подписаться на определённое событие, добавив слот. Потом когда движок породит сигнал, что событие произошло, то также через обход дерева найдётся объект, который был подписан на этот сигнал.


root – корневая нода, с неё начинается обход дерева

Также был реализован пул объектов, что очень полезно в играх, где требуется постоянное появление одинаковых объектов, вместо удаления и создания, движок кладёт объект в пул и при надобности достаёт из пула, это существенно экономит вычислительные ресурсы.

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

Технические подробности (новичкам можно пропустить абзац): движок был написан на AS3, использован для мобильных игр на Adobe AIR, например эта. Движок я не выполнил отдельной библиотекой, поэтому исходники смешаны с исходниками самой игры по ссылке, потому что разрабатывал его параллельно с написанием игры. Изначально игра была написана чисто на Flash без движков, но настал момент, что потребовалась оптимизация, для оптимизации я решил написать свой движок. Шейдеры были написаны на AGAL – Adobe Graphics Assembly Language, адобовский мини ассемблер. Бесконечный цикл у меня реализован не особо привычном образом, флеш генерирует событие ENTER_FRAME строго соотвествуя FPS и я подключил цикл к этому событию.

Теги , , , , ,
Автор

Опубликовано
Комментарии 0

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

В этой статье как уже было ясно, я бы хотел поговорить о создании файлов на android. И так, андроид предлагает нам два способа записи: на внутреннюю память и на внешнюю. Для записи на внутреннюю нам не нужны никакие разрешения, мы просто должны записать в:

context.getDataDir()
– этот метод вернёт скрытую директорию приложения, то есть файлы потом будут доступны только приложению, которое их записало. Хорошо, безопасность и всё такое, но, а что если нужно записать в галерею или документы, или просто в какое-то общее хранилище? Тогда мы должны писать во внешнюю помять и просить разрешения WRITE_EXTERNAL_STORAGE. В некоторых туториалах пишут, что это разрешение для записи на sd карту, но если следовать здравому смыслу, то под external понимается память, которая не internal, а internal – это память, которая доступна только одному приложению, которое и пишет эти файлы. То есть external – это не то внешнее что на sd карте, а то внешнее что доступно всем, то есть общее хранилище, которое нам и нужно. К слову, в Android Q уже не нужно это разрешение и там «гении» из гугла придумали что-то новое для разработчиков.

Добавляем разрешение, пишем код для записи в файл, но не работает. Ах да, с Android 6.0 у нас же появились runtime разрешения, и для записи во внешнее хранилище нужно запросить разрешение прямо из кода. Пишем:

//проверяем принято ли данное разрешение
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
{   
        //запрашиваем разрешение у пользователя
        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
}

Ага круто, теперь то мы действительно можем записывать файлы. Пишем:
File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
File file = new File(path, FILE_NAME);
FileWriter fileWriter = new FileWriter(file, true);
fileWriter.append(data);
fileWriter.flush();
fileWriter.close();

Проверяем на телефоне, файл есть, так теперь подключаем телефон к компьютеру и ищем наш файл. Файла нет, класс! Windows не видит файл или android не даёт увидеть файл? Ну, конечно, виноват android, кто же ещё. Смысл в том, что android должен просканить media, найти новый файл и тогда каким-то чудом он покажет файл винде. Делается это так:
MediaScannerConnection.scanFile(context, new String[]{filePath}, null, null);

Таким образом мы заставляем андроид просканить медиа и показать файл в windows. К слову при перезаписи файла, надо снова выполнять метод scanFile, иначе windows не видит изменений. Но, конечно, вышесказанное справедливо только для windows 7, на windows 10 я не проверял, а linux вероятно всего и так всё увидит, но линуксом не пользуются рядовые пользователи, поэтому обсуждать тут это не буду.

На этом всё, я думаю в статью ещё есть что добавить, поэтому если есть неясные моменты на данную тему, то спрашивайте в комментариях.

Теги , , ,
Автор

Опубликовано
Комментарии 0

И так мне понадобилось разобрать данные из файла шрифта woff (Web Open Font Format). Я прям с самого начала скажу, что там чёрт ногу сломит, а разработчики формата это представители инопланетной жизни страдающие от старческого маразма. Отправляемся на https://www.w3.org/TR/WOFF/, читаем, в принципе всё понятно (нет). Если кратко, то это бинарный файл, который имеет заголовок с информацией о таблицах. Таблицы – это некоторые структурированные данные, информация в которых отражает только один технический аспект шрифта. И все эти таблицы в woff заархивированы DEFLATE алгоритмом сжатия. На картинках ниже формат woff файла представлен более подробно.

Я буду рассматривать в этой статье только таблицы cmap и CFF, так как основная информация о символах именно в них.

cmap таблица по сути содержит информацию о том, как перейти от unicode байт к глифам (к начертаниям). По каким-то неведомым причинам видов cmap таблицы существует аж 14 штук, что за балаган? Но мне нужен только 4-ый. Документация просто ужасная, но методом проб и ошибок я пришёл к правильному способу перехода от unicode к глифам. Мне повезло в том, что количество сегментов равнялось количеству символов, и соответственно каждый startCode равнялся коду каждого символа. Но glyphIdArray почему-то был пустой. В итоге я сделал так: к startCode прибавил idDelta, и отсортировал полученные результаты по порядку. Этот порядок строго соответствовал порядку глифов в CFF. Може я сделал не совсем правильно, но иного способа не нашёл. Потому что везде пишут о том, что для этой задачи нужны ещё таблицы glyf и loca, но в woff файлах я этих таблиц не видел.

Перейдём к CFF. Если вы открыли документацию по CFF, то думаю вы поняли, что особо ничего и не понятно. CFF таблица состоит из заголовка, индексов (INDEX) и словарей (DICT). Индексами названы данные, которые структурированы как массив, словарями названы данные, которые структурированы как ключ-значение данные. В CFF присутствуют следующие данные: NameIndex, TopDictIndex, StringIndex, GlobalSubrIndex, Charsets, Encodings, CharStringIndex, PrivateDict, FontDictIndex, LocalSubrIndex. Мне понадобилось разобрать именно CharStringIndex, но так как данные предусматривалось находить по offset’ам, а offset говорит о нахождении только следующего блока, то разбирать пришлось практически всю CFF таблицу, чтобы дойти до CharStringIndex. Для идентификации глифов из CharStringIndex я просто сранивал длину каждого CharString из этого массива, дальше расковыривать формат не требовалось.

Кстати, в процессе поиска информации о форматах нашёл блог одного очень умного азиата, его исходники мне очень помогли в работе, парень проделал огромную работу по разбору шрифтов. В его исходниках всё понятно, написано очень классно, в отличие от мудацких исходников opentype.js, по которым понятно только одно, что автору платили за кривость кода.

В заключении вернусь к вопросу о том, что почему же woff формат такой сложный? Этот формат несёт в себе наследие CFF формата. CFF насколько я понимаю по документации был разработан в 2003 году, и вероятно он тоже в себе таит тайны прошлого. Прошлого, когда был важен каждый байт и важна скорость загрузки информации в память (а самая быстрая загрузка это когда просто копирование последовательности байт в память), видимо поэтому вся информация о шрифтах структурировалась в бинарный вид, вид который хорошо понятен машине, а не человеку – это вам не json смотреть.

Теги , ,
Автор