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

Для некоторых людей программирование является такой же внутренней потребностью, подобно тому, как коровы дают молоко, или писатели стремятся писать. (с) Николай Безруков

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

Опубликовано
Комментарии 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 смотреть.

Теги , ,
Автор